fix(sitecallaudit): UpdatedAtUtc index + per-row pull resilience + UTC-convention + first-cycle test (review)
This commit is contained in:
@@ -186,6 +186,42 @@ public class GrpcPullSiteCallsClientTests
|
||||
Assert.False(result.MoreAvailable);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PullAsync_skips_poison_row_and_returns_the_good_rows()
|
||||
{
|
||||
// Poison-row resilience: one malformed operational (an unparseable
|
||||
// TrackedOperationId fails SiteCallDtoMapper.FromDto → Guid.Parse) must be
|
||||
// skipped+logged PER ROW rather than sinking the whole batch through the
|
||||
// outer catch-all. The two good rows survive, re-stamped + oldest-first.
|
||||
var older = Guid.NewGuid();
|
||||
var newer = Guid.NewGuid();
|
||||
|
||||
var proto = new ProtoPullResponse { MoreAvailable = false };
|
||||
proto.Operationals.Add(Dto(newer, BaseTime.AddMinutes(5)));
|
||||
// Malformed row in the middle of the batch.
|
||||
var bad = Dto(Guid.NewGuid(), BaseTime.AddMinutes(2));
|
||||
bad.TrackedOperationId = "not-a-guid";
|
||||
proto.Operationals.Add(bad);
|
||||
proto.Operationals.Add(Dto(older, BaseTime));
|
||||
|
||||
var invoker = FakeInvoker.Returning(proto);
|
||||
var sut = new GrpcPullSiteCallsClient(
|
||||
new StaticEnumerator(new SiteEntry("site-a", "http://site-a:8083")),
|
||||
invoker,
|
||||
NullLogger<GrpcPullSiteCallsClient>.Instance);
|
||||
|
||||
// Must NOT throw — the bad row is dropped, the good rows are returned.
|
||||
var result = await sut.PullAsync("site-a", BaseTime, batchSize: 256, CancellationToken.None);
|
||||
|
||||
Assert.Equal(2, result.SiteCalls.Count);
|
||||
// Survivors are oldest-first and SourceSite re-stamped from the dialed siteId.
|
||||
Assert.Equal(older, result.SiteCalls[0].TrackedOperationId.Value);
|
||||
Assert.Equal(newer, result.SiteCalls[1].TrackedOperationId.Value);
|
||||
Assert.Equal("site-a", result.SiteCalls[0].SourceSite);
|
||||
Assert.Equal("site-a", result.SiteCalls[1].SourceSite);
|
||||
Assert.False(result.MoreAvailable);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PullAsync_with_minvalue_unspecified_cursor_does_not_throw_and_dials()
|
||||
{
|
||||
|
||||
@@ -121,6 +121,38 @@ public class SiteStreamPullSiteCallsTests : TestKit
|
||||
Assert.Equal(since, capturedSince);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PullSiteCalls_SinceUtcUnset_PassesDateTimeMinValue()
|
||||
{
|
||||
// First reconciliation cycle: central has no cursor yet, so the request's
|
||||
// SinceUtc wrapper is absent (null). The handler must default to
|
||||
// DateTime.MinValue ("pull from the beginning of recorded history")
|
||||
// without a null-deref — this proves the very first cycle doesn't crash.
|
||||
var store = Substitute.For<IOperationTrackingStore>();
|
||||
var captured = new DateTime(2099, 1, 1, 0, 0, 0, DateTimeKind.Utc); // sentinel
|
||||
store.ReadChangedSinceAsync(Arg.Any<DateTime>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
|
||||
.Returns(call =>
|
||||
{
|
||||
captured = call.ArgAt<DateTime>(0);
|
||||
return (IReadOnlyList<SiteCallOperational>)Array.Empty<SiteCallOperational>();
|
||||
});
|
||||
|
||||
var server = CreateServer();
|
||||
server.SetOperationTrackingStore(store);
|
||||
|
||||
// SinceUtc intentionally left unset (null) — the proto wrapper is absent.
|
||||
var request = new PullSiteCallsRequest
|
||||
{
|
||||
BatchSize = 100,
|
||||
};
|
||||
|
||||
var response = await server.PullSiteCalls(request, NewContext());
|
||||
|
||||
Assert.Empty(response.Operationals);
|
||||
Assert.False(response.MoreAvailable);
|
||||
Assert.Equal(DateTime.MinValue, captured);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PullSiteCalls_BatchSize3_Returns3Rows_MoreAvailableTrue()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user