diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/AuditLogExportService.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/AuditLogExportService.cs
index b38e46d8..1f4ee709 100644
--- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/AuditLogExportService.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Services/AuditLogExportService.cs
@@ -179,7 +179,7 @@ public sealed class AuditLogExportService : IAuditLogExportService
/// cleanly on another. Free-text columns are run through
/// before RFC 4180 quoting so a
/// leading =/+/-/@/TAB/CR can't be interpreted as
- /// a formula or DDE trigger by a spreadsheet application (CWE-1236). GUID/enum/numeric/date/bool columns this
+ /// a formula or DDE trigger by a spreadsheet application (CSV formula injection). GUID/enum/numeric/date/bool columns this
/// formatter generates itself are never neutralized.
///
/// The audit event view to format as a CSV row.
@@ -228,7 +228,7 @@ public sealed class AuditLogExportService : IAuditLogExportService
return;
}
- // OWASP CSV-injection neutralization (CWE-1236): prefix a literal single
+ // OWASP CSV-injection neutralization: prefix a literal single
// quote before RFC 4180 quoting so a free-text cell starting with a
// formula/DDE trigger character renders as inert text in Excel.
if (freeText && NeedsFormulaNeutralization(value))
diff --git a/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/SiteAlarmAggregatorActor.cs b/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/SiteAlarmAggregatorActor.cs
index 8eee78f6..ceb49fa3 100644
--- a/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/SiteAlarmAggregatorActor.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.Communication/Actors/SiteAlarmAggregatorActor.cs
@@ -708,7 +708,7 @@ public sealed class SiteAlarmAggregatorActor : ReceiveActor, IWithTimers
}
///
- /// Arms the backoff retry for a failed fan-out: reconnectDelay × 2^(failures-1),
+ /// Arms the backoff retry for a failed fan-out: reconnectDelay × 2^(failures − 1),
/// capped at × the reconcile interval. Once
/// the cap is reached the periodic reconcile tick is doing the same work anyway, so no
/// extra timer is armed.
diff --git a/src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/SiteHealthState.cs b/src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/SiteHealthState.cs
index 56572e57..b1e23afe 100644
--- a/src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/SiteHealthState.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/SiteHealthState.cs
@@ -26,7 +26,7 @@ public sealed record SiteHealthState
/// null if the site is known only via heartbeats and has not yet sent
/// a report. Used by the UI to surface report staleness during failover;
/// the null case must be rendered as "no report yet" rather than as a
- /// timestamp (a default sentinel would display as year-0001).
+ /// timestamp (a default sentinel would display as year 0001).
///
public DateTimeOffset? LastReportReceivedAt { get; init; }
diff --git a/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs b/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs
index 63d0dbfb..9e9002f7 100644
--- a/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs
+++ b/src/ZB.MOM.WW.ScadaBridge.ManagementService/ManagementActor.cs
@@ -2560,7 +2560,7 @@ public class ManagementActor : ReceiveActor
if (cmd.TlsMode is not null) config.TlsMode = cmd.TlsMode;
if (cmd.Credentials is not null) config.Credentials = cmd.Credentials;
// Preserve-if-null likewise for the OAuth2 token-endpoint overrides: an
- // omitted authority/scope leaves the stored value (or its M365 default) intact.
+ // omitted authority/scope leaves the stored value (or its Microsoft 365 default) intact.
if (cmd.OAuth2Authority is not null) config.OAuth2Authority = cmd.OAuth2Authority;
if (cmd.OAuth2Scope is not null) config.OAuth2Scope = cmd.OAuth2Scope;
// Preserve-if-null for the transport too: an omitted value keeps the stored
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/AddAuditLogTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/AddAuditLogTests.cs
index 2e08f66d..34999fb4 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/AddAuditLogTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/AddAuditLogTests.cs
@@ -15,8 +15,8 @@ using ZB.MOM.WW.ScadaBridge.HealthMonitoring;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests;
///
-/// Bundle E (M2 Task E1) DI surface tests for AddAuditLog. M1 shipped
-/// the options-only scaffold; M2 extends it with the site writer chain
+/// DI surface tests for AddAuditLog. The initial version was an
+/// options-only scaffold; it was then extended with the site writer chain
/// ( + +
/// ) and the telemetry collaborators
/// (, ,
@@ -41,6 +41,8 @@ public class AddAuditLogTests
return services.BuildServiceProvider();
}
+ /// Verifies that AddAuditLog registers the audit log options.
+ /// The result.
[Fact]
public void AddAuditLog_RegistersAuditLogOptions()
{
@@ -52,6 +54,8 @@ public class AddAuditLogTests
Assert.NotNull(opts!.Value);
}
+ /// Verifies that AddAuditLog throws when the service collection is null.
+ /// The result.
[Fact]
public void AddAuditLog_NullServices_Throws()
{
@@ -61,6 +65,8 @@ public class AddAuditLogTests
() => ServiceCollectionExtensions.AddAuditLog(null!, config));
}
+ /// Verifies that AddAuditLog throws when the configuration is null.
+ /// The result.
[Fact]
public void AddAuditLog_NullConfig_Throws()
{
@@ -70,8 +76,10 @@ public class AddAuditLogTests
() => services.AddAuditLog(null!));
}
- // -- Bundle E (M2 Task E1) ---------------------------------------------
+ // -- Site writer chain DI registrations ---------------------------------------------
+ /// Verifies that AddAuditLog registers the SQLite audit writer as a singleton resolvable from DI.
+ /// The result.
[Fact]
public void AddAuditLog_Registers_SqliteAuditWriter_Singleton_FromDI()
{
@@ -90,6 +98,8 @@ public class AddAuditLogTests
Assert.Same(writer, provider.GetService());
}
+ /// Verifies that AddAuditLog registers IAuditWriter as the fallback audit writer.
+ /// The result.
[Fact]
public void AddAuditLog_Registers_IAuditWriter_AsFallbackAuditWriter()
{
@@ -104,6 +114,8 @@ public class AddAuditLogTests
Assert.IsType(writer);
}
+ /// Verifies that AddAuditLog registers ISiteAuditQueue as the same instance as the SQLite audit writer.
+ /// The result.
[Fact]
public void AddAuditLog_Registers_ISiteAuditQueue_AsSameInstance_As_SqliteAuditWriter()
{
@@ -123,6 +135,8 @@ public class AddAuditLogTests
Assert.Same(writer, queue);
}
+ /// Verifies that AddAuditLog registers the ring-buffer fallback as a singleton.
+ /// The result.
[Fact]
public void AddAuditLog_Registers_RingBufferFallback_Singleton()
{
@@ -136,6 +150,8 @@ public class AddAuditLogTests
Assert.Same(ring, provider.GetService());
}
+ /// Verifies that AddAuditLog registers a no-op audit write failure counter by default.
+ /// The result.
[Fact]
public void AddAuditLog_Registers_AuditWriteFailureCounter_AsNoOpDefault()
{
@@ -149,6 +165,8 @@ public class AddAuditLogTests
Assert.IsType(counter);
}
+ /// Verifies that AddAuditLog registers a no-op site stream audit client by default.
+ /// The result.
[Fact]
public void AddAuditLog_Registers_SiteStreamAuditClient_AsNoOpDefault()
{
@@ -162,8 +180,10 @@ public class AddAuditLogTests
Assert.IsType(client);
}
- // -- M4 Bundle B (B1) central direct-write audit writer -----------------
+ // -- Central direct-write audit writer -----------------
+ /// Verifies that AddAuditLog registers ICentralAuditWriter as the central audit writer.
+ /// The result.
[Fact]
public void AddAuditLog_Registers_ICentralAuditWriter_AsCentralAuditWriter()
{
@@ -177,6 +197,8 @@ public class AddAuditLogTests
Assert.IsType(writer);
}
+ /// Verifies that the ICentralAuditWriter registration is a singleton.
+ /// The result.
[Fact]
public void AddAuditLog_ICentralAuditWriter_IsSingleton()
{
@@ -190,6 +212,8 @@ public class AddAuditLogTests
Assert.Same(w1, w2);
}
+ /// Verifies that the SQLite writer options bind from configuration and round-trip.
+ /// The result.
[Fact]
public void AddAuditLog_Options_Bind_RoundTrip_SqliteWriter()
{
@@ -208,6 +232,8 @@ public class AddAuditLogTests
Assert.Equal(75, opts.FlushIntervalMs);
}
+ /// Verifies that the site telemetry options bind from configuration and round-trip.
+ /// The result.
[Fact]
public void AddAuditLog_Options_Bind_RoundTrip_SiteTelemetry()
{
@@ -224,8 +250,10 @@ public class AddAuditLogTests
Assert.Equal(60, opts.IdleIntervalSeconds);
}
- // -- Bundle G (M2 Task G1) Site Health Monitoring bridge ----------------
+ // -- Site Health Monitoring bridge ----------------
+ /// Verifies that the health metrics bridge swaps the failure counter for the health metrics implementation.
+ /// The result.
[Fact]
public void AddAuditLogHealthMetricsBridge_Swaps_FailureCounter_To_HealthMetrics_Implementation()
{
@@ -251,6 +279,8 @@ public class AddAuditLogTests
Assert.IsType(counter);
}
+ /// Verifies that the health metrics bridge still resolves without health monitoring but errors when used.
+ /// The result.
[Fact]
public void AddAuditLogHealthMetricsBridge_Without_HealthMonitoring_Still_Resolves_But_Errors_On_Use()
{
@@ -276,10 +306,12 @@ public class AddAuditLogTests
() => provider.GetRequiredService());
}
+ /// Verifies that registering the health metrics bridge twice does not register the hosted service twice.
+ /// The result.
[Fact]
public void AddAuditLogHealthMetricsBridge_IsIdempotent_DoesNotDoubleRegister_HostedService()
{
- // AuditLog-011: AddHostedService has no TryAdd variant, so a second
+ // AddHostedService has no TryAdd variant, so a second
// call without the sentinel guard would spin up a second
// SiteAuditBacklogReporter on the same SQLite file. The helper must
// be a no-op on the second call — exactly one hosted-service
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/AuditLogIngestActorCombinedTelemetryTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/AuditLogIngestActorCombinedTelemetryTests.cs
index 6fd62f45..92c02d93 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/AuditLogIngestActorCombinedTelemetryTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/AuditLogIngestActorCombinedTelemetryTests.cs
@@ -19,9 +19,9 @@ using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests.Migrations;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Central;
///
-/// Bundle D D2 tests for 's M3 combined-
-/// telemetry dual-write transaction. Uses the same
-/// as the M1 + M2 repository tests so the actor exercises real
+/// Tests for 's combined-telemetry
+/// dual-write transaction. Uses the same
+/// as the audit-log and site-call repository tests so the actor exercises real
/// +
/// against a per-test MSSQL
/// database. The transaction commits or rolls back inside one
@@ -31,6 +31,8 @@ public class AuditLogIngestActorCombinedTelemetryTests : TestKit, IClassFixture<
{
private readonly MsSqlMigrationFixture _fixture;
+ /// Initializes the test class with the shared MSSQL migration fixture.
+ /// The shared MSSQL migration fixture.
public AuditLogIngestActorCombinedTelemetryTests(MsSqlMigrationFixture fixture)
{
_fixture = fixture;
@@ -125,6 +127,8 @@ public class AuditLogIngestActorCombinedTelemetryTests : TestKit, IClassFixture<
serviceProvider,
NullLogger.Instance)));
+ /// Verifies that one cached-telemetry packet writes both an audit row and a site-call row and acknowledges the event id.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task Receive_OneCachedPacket_WritesAuditRow_AND_SiteCallRow_AcksId()
{
@@ -156,6 +160,8 @@ public class AuditLogIngestActorCombinedTelemetryTests : TestKit, IClassFixture<
Assert.Equal(siteCall.Status, siteCallRow!.Status);
}
+ /// Verifies that a duplicate event id with an unchanged status is a no-op that leaves the row counts unchanged and still acknowledges the id.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task Receive_DuplicateEventId_SameStatus_NoOp_RowCountUnchanged_AcksId()
{
@@ -195,6 +201,8 @@ public class AuditLogIngestActorCombinedTelemetryTests : TestKit, IClassFixture<
Assert.Equal(1, siteCallCount);
}
+ /// Verifies that a duplicate event id carrying an advanced site-call status updates the existing site-call row.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task Receive_DuplicateEventId_AdvancedSiteCallStatus_UpdatesSiteCall()
{
@@ -206,7 +214,7 @@ public class AuditLogIngestActorCombinedTelemetryTests : TestKit, IClassFixture<
var sp = BuildServiceProvider();
var actor = CreateActor(sp);
- // 1st packet — Submitted (audit EventId #1, SiteCalls Status=Submitted).
+ // 1st packet — Submitted (first audit EventId, SiteCalls Status=Submitted).
var (auditSubmit, siteCallSubmit) = NewEntry(
siteId, trackedId, status: "Submitted", auditStatus: AuditStatus.Submitted);
actor.Tell(
@@ -214,7 +222,7 @@ public class AuditLogIngestActorCombinedTelemetryTests : TestKit, IClassFixture<
TestActor);
ExpectMsg(TimeSpan.FromSeconds(15));
- // 2nd packet — Attempted with retry count 1 (audit EventId #2,
+ // 2nd packet — Attempted with retry count 1 (second audit EventId,
// SiteCalls Status=Attempted — monotonic upsert wins). Same
// TrackedOperationId throughout.
var (auditAttempt, siteCallAttempt) = NewEntry(
@@ -242,6 +250,8 @@ public class AuditLogIngestActorCombinedTelemetryTests : TestKit, IClassFixture<
Assert.Equal(1, siteCallRow.RetryCount);
}
+ /// Verifies that when the site-call upsert throws after the audit insert succeeds, both writes are rolled back and no orphan audit row remains.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task Receive_AuditInsertSucceeds_SiteCallThrows_BothRolledBack_NoOrphanRow()
{
@@ -273,6 +283,8 @@ public class AuditLogIngestActorCombinedTelemetryTests : TestKit, IClassFixture<
Assert.Null(siteCallRow);
}
+ /// Verifies that five cached-telemetry packets are each persisted separately and all are acknowledged.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task Receive_FiveCachedPackets_AllPersistedSeparately_AllAcked()
{
@@ -303,6 +315,8 @@ public class AuditLogIngestActorCombinedTelemetryTests : TestKit, IClassFixture<
Assert.Equal(5, siteCallCount);
}
+ /// Verifies that a packet failing after an earlier packet succeeded leaves the first one committed and lets the batch continue.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task Receive_OnePacketSucceeds_NextPacketThrows_FirstStillCommitted_BatchContinues()
{
@@ -348,7 +362,7 @@ public class AuditLogIngestActorCombinedTelemetryTests : TestKit, IClassFixture<
}
///
- /// arch-review 2026-08-19 P5-1: the per-entry fallback loop (used when the
+ /// The per-entry fallback loop (used when the
/// batch fast path fails) must run under its own short
/// , not an unbounded
/// wait — otherwise a single slow/wedged downstream call can freeze this
@@ -368,6 +382,7 @@ public class AuditLogIngestActorCombinedTelemetryTests : TestKit, IClassFixture<
/// ExpectMsg below would time out and fail, which is the fail-unfixed
/// proof for this fix.
///
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task Receive_CachedTelemetry_PerEntryFallbackHangs_BudgetBoundsWait_RemainderSkipped()
{
@@ -430,38 +445,51 @@ public class AuditLogIngestActorCombinedTelemetryTests : TestKit, IClassFixture<
///
private sealed class BatchFailingAuditRepo : IAuditLogRepository
{
+ ///
public Task> GetTraceEventsAsync(Guid executionId, CancellationToken ct = default, IReadOnlyList? permittedSiteIds = null)
=> throw new NotSupportedException("GetTraceEventsAsync is not exercised by this test double.");
private readonly IAuditLogRepository _inner;
+ /// Initializes the double with the real repository that every non-failing member delegates to.
+ /// The real repository to delegate to.
public BatchFailingAuditRepo(IAuditLogRepository inner) { _inner = inner; }
+ ///
public Task InsertManyIfNotExistsAsync(
IReadOnlyList events, TimeSpan? commandTimeout = null, CancellationToken ct = default) =>
throw new InvalidOperationException("simulated batch ingest failure — forces the per-entry fallback");
+ ///
public Task InsertIfNotExistsAsync(AuditEvent evt, CancellationToken ct = default) =>
_inner.InsertIfNotExistsAsync(evt, ct);
+ ///
public Task> QueryAsync(
AuditLogQueryFilter filter, AuditLogPaging paging, CancellationToken ct = default) =>
_inner.QueryAsync(filter, paging, ct);
+ ///
public Task SwitchOutPartitionAsync(
DateTime monthBoundary, TimeSpan? commandTimeout = null, CancellationToken ct = default) =>
_inner.SwitchOutPartitionAsync(monthBoundary, commandTimeout, ct);
+ ///
public Task PurgeChannelOlderThanAsync(
string channel, DateTime threshold, int batchSize, TimeSpan? commandTimeout = null, CancellationToken ct = default, int maxRows = 50_000) =>
_inner.PurgeChannelOlderThanAsync(channel, threshold, batchSize, commandTimeout, ct, maxRows);
+ ///
public Task> GetPartitionBoundariesOlderThanAsync(
DateTime threshold, CancellationToken ct = default) =>
_inner.GetPartitionBoundariesOlderThanAsync(threshold, ct);
+ ///
public Task GetKpiSnapshotAsync(
TimeSpan window, DateTime? nowUtc = null, CancellationToken ct = default) =>
_inner.GetKpiSnapshotAsync(window, nowUtc, ct);
+ ///
public Task> GetExecutionTreeAsync(
Guid executionId, CancellationToken ct = default, IReadOnlyList? permittedSiteIds = null) =>
_inner.GetExecutionTreeAsync(executionId, ct);
+ ///
public Task> GetDistinctSourceNodesAsync(CancellationToken ct = default) =>
_inner.GetDistinctSourceNodesAsync(ct);
+ ///
public Task BackfillSourceNodeAsync(
string sentinel, DateTime before, int batchSize, CancellationToken ct = default) =>
_inner.BackfillSourceNodeAsync(sentinel, before, batchSize, ct);
@@ -471,15 +499,17 @@ public class AuditLogIngestActorCombinedTelemetryTests : TestKit, IClassFixture<
/// Test double — hangs on
/// Task.Delay(Timeout.InfiniteTimeSpan, ct) until the caller's token
/// cancels, recording the call count and the token it was handed. Used to
- /// prove the per-entry fallback loop's own budget bounds the wait (arch-review
- /// 2026-08-19 P5-1) rather than blocking forever on a wedged downstream call.
+ /// prove the per-entry fallback loop's own budget bounds the wait rather blocking forever on a wedged downstream call.
///
private sealed class HangingSiteCallRepo : ISiteCallAuditRepository
{
private int _callCount;
+ /// Gets the number of times has been invoked.
public int CallCount => _callCount;
+ /// Gets the cancellation token most recently passed to .
public CancellationToken? ReceivedToken { get; private set; }
+ ///
public async Task UpsertAsync(SiteCall siteCall, CancellationToken ct = default)
{
Interlocked.Increment(ref _callCount);
@@ -487,19 +517,25 @@ public class AuditLogIngestActorCombinedTelemetryTests : TestKit, IClassFixture<
await Task.Delay(Timeout.InfiniteTimeSpan, ct).ConfigureAwait(false);
}
+ ///
public Task GetAsync(TrackedOperationId id, CancellationToken ct = default) =>
throw new NotSupportedException();
+ ///
public Task> QueryAsync(
SiteCallQueryFilter filter, SiteCallPaging paging, CancellationToken ct = default) =>
throw new NotSupportedException();
+ ///
public Task PurgeTerminalAsync(DateTime olderThanUtc, CancellationToken ct = default) =>
throw new NotSupportedException();
+ ///
public Task ComputeKpisAsync(
DateTime stuckCutoff, DateTime intervalSince, CancellationToken ct = default) =>
throw new NotSupportedException();
+ ///
public Task> ComputePerSiteKpisAsync(
DateTime stuckCutoff, DateTime intervalSince, CancellationToken ct = default) =>
throw new NotSupportedException();
+ ///
public Task> ComputePerNodeKpisAsync(
DateTime stuckCutoff, DateTime intervalSince, CancellationToken ct = default) =>
throw new NotSupportedException();
@@ -513,22 +549,31 @@ public class AuditLogIngestActorCombinedTelemetryTests : TestKit, IClassFixture<
private sealed class ThrowingSiteCallRepo : ISiteCallAuditRepository
{
private readonly ISiteCallAuditRepository _inner;
+ /// Initializes the double with the real repository that read members delegate to.
+ /// The real repository to delegate to.
public ThrowingSiteCallRepo(ISiteCallAuditRepository inner) { _inner = inner; }
+ ///
public Task UpsertAsync(SiteCall siteCall, CancellationToken ct = default) =>
throw new InvalidOperationException("simulated SiteCalls upsert failure");
+ ///
public Task GetAsync(TrackedOperationId id, CancellationToken ct = default) =>
_inner.GetAsync(id, ct);
+ ///
public Task> QueryAsync(
SiteCallQueryFilter filter, SiteCallPaging paging, CancellationToken ct = default) =>
_inner.QueryAsync(filter, paging, ct);
+ ///
public Task PurgeTerminalAsync(DateTime olderThanUtc, CancellationToken ct = default) =>
_inner.PurgeTerminalAsync(olderThanUtc, ct);
+ ///
public Task ComputeKpisAsync(
DateTime stuckCutoff, DateTime intervalSince, CancellationToken ct = default) =>
_inner.ComputeKpisAsync(stuckCutoff, intervalSince, ct);
+ ///
public Task> ComputePerSiteKpisAsync(
DateTime stuckCutoff, DateTime intervalSince, CancellationToken ct = default) =>
_inner.ComputePerSiteKpisAsync(stuckCutoff, intervalSince, ct);
+ ///
public Task> ComputePerNodeKpisAsync(
DateTime stuckCutoff, DateTime intervalSince, CancellationToken ct = default) =>
_inner.ComputePerNodeKpisAsync(stuckCutoff, intervalSince, ct);
@@ -543,11 +588,15 @@ public class AuditLogIngestActorCombinedTelemetryTests : TestKit, IClassFixture<
{
private readonly ISiteCallAuditRepository _inner;
private readonly TrackedOperationId _poisonId;
+ /// Initializes the double with the real repository and the id whose upsert should fail.
+ /// The real repository to delegate to.
+ /// The tracked operation id whose upsert throws.
public PoisonOnIdSiteCallRepo(ISiteCallAuditRepository inner, TrackedOperationId poisonId)
{
_inner = inner;
_poisonId = poisonId;
}
+ ///
public Task UpsertAsync(SiteCall siteCall, CancellationToken ct = default)
{
if (siteCall.TrackedOperationId == _poisonId)
@@ -556,19 +605,25 @@ public class AuditLogIngestActorCombinedTelemetryTests : TestKit, IClassFixture<
}
return _inner.UpsertAsync(siteCall, ct);
}
+ ///
public Task GetAsync(TrackedOperationId id, CancellationToken ct = default) =>
_inner.GetAsync(id, ct);
+ ///
public Task> QueryAsync(
SiteCallQueryFilter filter, SiteCallPaging paging, CancellationToken ct = default) =>
_inner.QueryAsync(filter, paging, ct);
+ ///
public Task PurgeTerminalAsync(DateTime olderThanUtc, CancellationToken ct = default) =>
_inner.PurgeTerminalAsync(olderThanUtc, ct);
+ ///
public Task ComputeKpisAsync(
DateTime stuckCutoff, DateTime intervalSince, CancellationToken ct = default) =>
_inner.ComputeKpisAsync(stuckCutoff, intervalSince, ct);
+ ///
public Task> ComputePerSiteKpisAsync(
DateTime stuckCutoff, DateTime intervalSince, CancellationToken ct = default) =>
_inner.ComputePerSiteKpisAsync(stuckCutoff, intervalSince, ct);
+ ///
public Task> ComputePerNodeKpisAsync(
DateTime stuckCutoff, DateTime intervalSince, CancellationToken ct = default) =>
_inner.ComputePerNodeKpisAsync(stuckCutoff, intervalSince, ct);
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/AuditLogIngestActorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/AuditLogIngestActorTests.cs
index de464437..a0e97181 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/AuditLogIngestActorTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/AuditLogIngestActorTests.cs
@@ -18,7 +18,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Central;
///
/// Bundle D D2 tests for . Uses the same
-/// as the M1 repository tests so the actor
+/// as the repository tests so the actor
/// exercises real
/// against a partitioned MSSQL schema (the only way to verify the
/// IngestedAtUtc stamp + duplicate-key idempotency end to end).
@@ -27,6 +27,8 @@ public class AuditLogIngestActorTests : TestKit, IClassFixtureInitializes the test class with the shared MSSQL migration fixture.
+ /// The shared MSSQL migration fixture.
public AuditLogIngestActorTests(MsSqlMigrationFixture fixture)
{
_fixture = fixture;
@@ -61,6 +63,8 @@ public class AuditLogIngestActorTests : TestKit, IClassFixture.Instance)));
}
+ /// Verifies that a batch of five events calls the repository five times and acknowledges all five.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task Receive_BatchOf5_Calls_Repo_5Times_Acks_All_5()
{
@@ -87,6 +91,8 @@ public class AuditLogIngestActorTests : TestKit, IClassFixtureVerifies that a batch containing an already-existing event acknowledges every event without inserting a duplicate.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task Receive_BatchWith_AlreadyExistingEvent_AcksAll_NoDoubleInsert()
{
@@ -126,12 +132,14 @@ public class AuditLogIngestActorTests : TestKit, IClassFixtureVerifies that duplicate event ids within one packet produce a single row and are all acknowledged.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task Receive_DuplicateEventIds_WithinOnePacket_ProduceOneRow_AndAreAllAcked()
{
Skip.IfNot(_fixture.Available, _fixture.SkipReason);
- // WP2.2 writes each packet as ONE set-based statement, whose anti-semi-join
+ // The repository writes each packet as ONE set-based statement, whose anti-semi-join
// only sees already-COMMITTED rows. Two copies of an EventId inside one
// packet would therefore both pass it and collide on the clustered PK,
// taking the whole packet down — the repository de-duplicates first.
@@ -161,6 +169,8 @@ public class AuditLogIngestActorTests : TestKit, IClassFixtureVerifies that sending the same packet twice is idempotent across packets.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task Receive_SamePacketTwice_IsIdempotent_AcrossPackets()
{
@@ -194,6 +204,8 @@ public class AuditLogIngestActorTests : TestKit, IClassFixtureVerifies that overlapping packets insert only the new rows.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task Receive_OverlappingPackets_InsertOnlyTheNewRows()
{
@@ -225,6 +237,8 @@ public class AuditLogIngestActorTests : TestKit, IClassFixtureVerifies that the actor stamps IngestedAtUtc before inserting an event.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task Receive_Sets_IngestedAtUtc_Before_Insert()
{
@@ -257,6 +271,8 @@ public class AuditLogIngestActorTests : TestKit, IClassFixtureVerifies that when the repository throws for one event, the other four are still persisted.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task Receive_RepoThrowsForOneEvent_Other4StillPersisted()
{
@@ -289,10 +305,12 @@ public class AuditLogIngestActorTests : TestKit, IClassFixture r.EventId == poisonId);
}
+ /// Verifies that when repository resolution throws, the actor survives, replies with an empty acknowledgement, and counts the failure.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Receive_WhenRepositoryResolutionThrows_ActorSurvives_RepliesEmpty_CountsFailure()
{
- // AuditLog-017 (covers the AuditLog-014 guard): the production ctor resolves the
+ // The production ctor resolves the
// scoped repository per message. If scope creation / repository resolution throws
// (transient DI or DbContext-factory fault, pooled-context init, a resolution race
// during host churn), the outer guard must keep the singleton ALIVE, increment the
@@ -343,6 +361,7 @@ public class AuditLogIngestActorTests : TestKit, IClassFixture
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Receive_WhenBatchFails_PerRowFallbackUsesAFreshToken()
{
@@ -371,7 +390,9 @@ public class AuditLogIngestActorTests : TestKit, IClassFixtureCounts how many times the guard's catch surfaced a write failure.
private sealed class CountingFailureCounter : ICentralAuditWriteFailureCounter
{
+ /// Gets the number of failures counted so far.
public int Count { get; private set; }
+ ///
public void Increment() => Count++;
}
@@ -382,13 +403,17 @@ public class AuditLogIngestActorTests : TestKit, IClassFixture
private sealed class TokenRecordingRepository : IAuditLogRepository
{
+ ///
public Task> GetTraceEventsAsync(Guid executionId, CancellationToken ct = default, IReadOnlyList? permittedSiteIds = null)
=> throw new NotSupportedException("GetTraceEventsAsync is not exercised by this test double.");
+ /// Gets the cancellation token handed to the set-based batch insert.
public CancellationToken? BatchToken { get; private set; }
+ /// Gets the cancellation tokens handed to each per-row insert.
public List RowTokens { get; } = new();
+ ///
public Task InsertManyIfNotExistsAsync(
IReadOnlyList events, TimeSpan? commandTimeout = null, CancellationToken ct = default)
{
@@ -396,40 +421,49 @@ public class AuditLogIngestActorTests : TestKit, IClassFixture
public Task InsertIfNotExistsAsync(AuditEvent evt, CancellationToken ct = default)
{
RowTokens.Add(ct);
return Task.CompletedTask;
}
+ ///
public Task> QueryAsync(
AuditLogQueryFilter filter, AuditLogPaging paging, CancellationToken ct = default) =>
throw new NotSupportedException();
+ ///
public Task SwitchOutPartitionAsync(
DateTime monthBoundary, TimeSpan? commandTimeout = null, CancellationToken ct = default) =>
throw new NotSupportedException();
+ ///
public Task PurgeChannelOlderThanAsync(
string channel, DateTime threshold, int batchSize, TimeSpan? commandTimeout = null, CancellationToken ct = default, int maxRows = 50_000) =>
throw new NotSupportedException();
+ ///
public Task BackfillSourceNodeAsync(
string sentinel, DateTime before, int batchSize, CancellationToken ct = default) =>
throw new NotSupportedException();
+ ///
public Task> GetPartitionBoundariesOlderThanAsync(
DateTime threshold, CancellationToken ct = default) =>
throw new NotSupportedException();
+ ///
public Task GetKpiSnapshotAsync(
TimeSpan window, DateTime? nowUtc = null, CancellationToken ct = default) =>
throw new NotSupportedException();
+ ///
public Task> GetExecutionTreeAsync(
Guid executionId, CancellationToken ct = default, IReadOnlyList? permittedSiteIds = null) =>
throw new NotSupportedException();
+ ///
public Task> GetDistinctSourceNodesAsync(CancellationToken ct = default) =>
throw new NotSupportedException();
}
@@ -441,18 +475,23 @@ public class AuditLogIngestActorTests : TestKit, IClassFixture
private sealed class ThrowingRepository : IAuditLogRepository
{
+ ///
public Task> GetTraceEventsAsync(Guid executionId, CancellationToken ct = default, IReadOnlyList? permittedSiteIds = null)
=> throw new NotSupportedException("GetTraceEventsAsync is not exercised by this test double.");
private readonly IAuditLogRepository _inner;
private readonly Guid _poisonId;
+ /// Initializes the double with an inner repository and the id of the event that must fail.
+ /// The real repository to delegate to.
+ /// The event id for which inserts throw.
public ThrowingRepository(IAuditLogRepository inner, Guid poisonId)
{
_inner = inner;
_poisonId = poisonId;
}
+ ///
public Task InsertIfNotExistsAsync(AuditEvent evt, CancellationToken ct = default)
{
if (evt.EventId == _poisonId)
@@ -462,33 +501,41 @@ public class AuditLogIngestActorTests : TestKit, IClassFixture
public Task> QueryAsync(
AuditLogQueryFilter filter, AuditLogPaging paging, CancellationToken ct = default) =>
_inner.QueryAsync(filter, paging, ct);
+ ///
public Task SwitchOutPartitionAsync(DateTime monthBoundary, TimeSpan? commandTimeout = null, CancellationToken ct = default) =>
_inner.SwitchOutPartitionAsync(monthBoundary, commandTimeout, ct);
+ ///
public Task PurgeChannelOlderThanAsync(
string channel, DateTime threshold, int batchSize, TimeSpan? commandTimeout = null, CancellationToken ct = default, int maxRows = 50_000) =>
_inner.PurgeChannelOlderThanAsync(channel, threshold, batchSize, commandTimeout, ct, maxRows);
+ ///
public Task BackfillSourceNodeAsync(
string sentinel, DateTime before, int batchSize, CancellationToken ct = default) =>
_inner.BackfillSourceNodeAsync(sentinel, before, batchSize, ct);
+ ///
public Task> GetPartitionBoundariesOlderThanAsync(
DateTime threshold, CancellationToken ct = default) =>
_inner.GetPartitionBoundariesOlderThanAsync(threshold, ct);
+ ///
public Task GetKpiSnapshotAsync(
TimeSpan window, DateTime? nowUtc = null, CancellationToken ct = default) =>
_inner.GetKpiSnapshotAsync(window, nowUtc, ct);
+ ///
public Task> GetExecutionTreeAsync(
Guid executionId, CancellationToken ct = default, IReadOnlyList? permittedSiteIds = null) =>
_inner.GetExecutionTreeAsync(executionId, ct);
+ ///
public Task> GetDistinctSourceNodesAsync(CancellationToken ct = default) =>
_inner.GetDistinctSourceNodesAsync(ct);
}
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/AuditLogPartitionMaintenanceOptionsValidatorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/AuditLogPartitionMaintenanceOptionsValidatorTests.cs
index 2b45be34..c278099e 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/AuditLogPartitionMaintenanceOptionsValidatorTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/AuditLogPartitionMaintenanceOptionsValidatorTests.cs
@@ -10,6 +10,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Central;
///
public class AuditLogPartitionMaintenanceOptionsValidatorTests
{
+ /// Verifies that the default options pass validation.
[Fact]
public void DefaultOptions_AreValid()
{
@@ -17,6 +18,7 @@ public class AuditLogPartitionMaintenanceOptionsValidatorTests
Assert.True(validator.Validate(null, new AuditLogPartitionMaintenanceOptions()).Succeeded);
}
+ /// Verifies that a zero interval fails validation and names the interval property.
[Fact]
public void ZeroIntervalSeconds_Fails()
{
@@ -28,6 +30,7 @@ public class AuditLogPartitionMaintenanceOptionsValidatorTests
f => f.Contains(nameof(AuditLogPartitionMaintenanceOptions.IntervalSeconds), StringComparison.Ordinal));
}
+ /// Verifies that a zero look-ahead fails validation and names the look-ahead property.
[Fact]
public void ZeroLookaheadMonths_Fails()
{
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/AuditLogPartitionMaintenanceServiceTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/AuditLogPartitionMaintenanceServiceTests.cs
index 69055d58..cd63f9d8 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/AuditLogPartitionMaintenanceServiceTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/AuditLogPartitionMaintenanceServiceTests.cs
@@ -9,7 +9,7 @@ using Xunit;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Central;
///
-/// Bundle D (#23 M6-T5) tests for .
+/// Tests for .
/// All tests use an in-memory stub —
/// the real EF/MSSQL implementation is exercised by the
/// AuditLogPartitionMaintenanceTests integration suite in
@@ -28,6 +28,7 @@ public class AuditLogPartitionMaintenanceServiceTests
public int EnsureCallCount;
public Exception? ThrowOnce;
+ ///
public Task> EnsureLookaheadAsync(int lookaheadMonths, CancellationToken ct = default)
{
Interlocked.Increment(ref EnsureCallCount);
@@ -39,6 +40,7 @@ public class AuditLogPartitionMaintenanceServiceTests
return Task.FromResult>(Array.Empty());
}
+ ///
public Task GetMaxBoundaryAsync(CancellationToken ct = default) =>
Task.FromResult(DateTime.UtcNow.AddMonths(6));
}
@@ -50,12 +52,27 @@ public class AuditLogPartitionMaintenanceServiceTests
///
private sealed class CapturingLogger : ILogger
{
+ /// Gets the log entries captured so far.
public List<(LogLevel Level, Exception? Exception, string Message)> Entries { get; } = new();
+ /// Returns no scope; the fake does not track scopes.
+ /// The type of the scope state.
+ /// The scope state (ignored).
+ /// Always null.
public IDisposable? BeginScope(TState state) where TState : notnull => null;
+ /// Reports every log level as enabled.
+ /// The log level to check.
+ /// Always true.
public bool IsEnabled(LogLevel logLevel) => true;
+ /// Records the log entry for later assertions.
+ /// The type of the log state.
+ /// The severity of the entry.
+ /// The event identifier.
+ /// The state object to format.
+ /// The exception associated with the entry, if any.
+ /// Formats the state and exception into the message.
public void Log(
LogLevel logLevel,
EventId eventId,
@@ -77,6 +94,8 @@ public class AuditLogPartitionMaintenanceServiceTests
return services.BuildServiceProvider();
}
+ /// Verifies that the service starts and stops without throwing.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task StartStop_NoExceptions()
{
@@ -113,6 +132,8 @@ public class AuditLogPartitionMaintenanceServiceTests
Assert.True(maintenance.EnsureCallCount >= 1, $"expected at least 1 ensure call, got {maintenance.EnsureCallCount}");
}
+ /// Verifies that an exception thrown during maintenance is logged and not propagated.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task SafeMaintain_ExceptionLogged_NotPropagated()
{
@@ -152,10 +173,12 @@ public class AuditLogPartitionMaintenanceServiceTests
Assert.Equal(1, maintenance.EnsureCallCount);
}
+ /// Verifies that stopping the service after it has been disposed does not throw.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task StopAsync_AfterDispose_DoesNotThrow()
{
- // Regression (Gitea #15 follow-up): Dispose tears down the CTS that
+ // Regression: Dispose tears down the CTS that
// StopAsync cancels, and the host does not guarantee that every
// IHostedService.StopAsync is driven before the DI container is
// disposed — WebApplicationFactory's teardown reaches Dispose first and
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/AuditLogPurgeActorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/AuditLogPurgeActorTests.cs
index 9db8dba8..f1e0a0fe 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/AuditLogPurgeActorTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/AuditLogPurgeActorTests.cs
@@ -18,18 +18,20 @@ using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests.Migrations;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Central;
///
-/// Bundle C (#23 M6-T4) tests for . The fast,
+/// Tests for . The fast,
/// schedule-only tests substitute a recording stub for
/// so the timer + per-boundary error-isolation
/// + event-publish machinery can be exercised without an MSSQL container.
/// The end-to-end "real partition gets switched out" assertion lives in the
-/// repository tests (Bundle C of M6-T4); this actor file is purely about the
+/// repository tests; this actor file is purely about the
/// actor's policy decisions.
///
public class AuditLogPurgeActorTests : TestKit, IClassFixture
{
private readonly MsSqlMigrationFixture _fixture;
+ /// Initializes the test class with the shared MSSQL migration fixture.
+ /// The shared MSSQL migration fixture.
public AuditLogPurgeActorTests(MsSqlMigrationFixture fixture)
{
_fixture = fixture;
@@ -45,38 +47,52 @@ public class AuditLogPurgeActorTests : TestKit, IClassFixture
private sealed class RecordingRepo : IAuditLogRepository
{
+ ///
public Task> GetTraceEventsAsync(Guid executionId, CancellationToken ct = default, IReadOnlyList? permittedSiteIds = null)
=> throw new NotSupportedException("GetTraceEventsAsync is not exercised by this test double.");
+ /// Gets the thresholds passed to each partition-boundary query.
public List ThresholdQueries { get; } = new();
+ /// Gets the partition boundaries that were switched out, in call order.
public List SwitchedBoundaries { get; } = new();
+ /// Gets or sets the function that returns the row count reported for a switched boundary.
public Func RowsPerBoundary { get; set; } = _ => 0L;
+ /// Gets or sets the boundary whose switch-out should throw, if any.
public DateTime? ThrowOnBoundary { get; set; }
+ /// Gets or sets the exception thrown for ; a default failure is used when null.
public Exception? BoundaryException { get; set; }
- // Task 4: captures the maintenance command timeout the actor threads through to the
+ // Captures the maintenance command timeout the actor threads through to the
// switch-out + per-channel purge, so tests can assert the options value reaches the repo.
+ /// Gets the command timeouts passed to each partition switch-out call.
public List SwitchTimeouts { get; } = new();
+ /// Gets the command timeouts passed to each per-channel purge call.
public List ChannelTimeouts { get; } = new();
- // M5.5 (T3): records every per-channel purge call as
+ // Records every per-channel purge call as
// (channel, threshold, batchSize) so tests can assert which channels the
// actor chose to purge and with what window.
+ /// Gets every per-channel purge call as a channel, threshold, batch size and row ceiling.
public List<(string Channel, DateTime Threshold, int BatchSize, int MaxRows)> ChannelPurges { get; } = new();
+ /// Gets or sets the function that returns the row count reported for a purged channel.
public Func RowsPerChannel { get; set; } = _ => 0L;
// The actor enumerator returns whichever list is configured here.
// Mutating this between ticks lets tests simulate "no longer
// eligible" boundaries on the second tick.
+ /// Gets or sets the partition boundaries returned by the next threshold query.
public List Boundaries { get; set; } = new();
+ ///
public Task InsertIfNotExistsAsync(AuditEvent evt, CancellationToken ct = default) =>
Task.CompletedTask;
+ ///
public Task> QueryAsync(
AuditLogQueryFilter filter, AuditLogPaging paging, CancellationToken ct = default) =>
Task.FromResult>(Array.Empty());
+ ///
public Task SwitchOutPartitionAsync(DateTime monthBoundary, TimeSpan? commandTimeout = null, CancellationToken ct = default)
{
SwitchTimeouts.Add(commandTimeout);
@@ -88,6 +104,7 @@ public class AuditLogPurgeActorTests : TestKit, IClassFixture
public Task> GetPartitionBoundariesOlderThanAsync(
DateTime threshold, CancellationToken ct = default)
{
@@ -95,6 +112,7 @@ public class AuditLogPurgeActorTests : TestKit, IClassFixture>(Boundaries.ToArray());
}
+ ///
public Task PurgeChannelOlderThanAsync(
string channel, DateTime threshold, int batchSize, TimeSpan? commandTimeout = null, CancellationToken ct = default, int maxRows = 50_000)
{
@@ -103,18 +121,22 @@ public class AuditLogPurgeActorTests : TestKit, IClassFixture
public Task BackfillSourceNodeAsync(
string sentinel, DateTime before, int batchSize, CancellationToken ct = default) =>
Task.FromResult(0L);
+ ///
public Task GetKpiSnapshotAsync(
TimeSpan window, DateTime? nowUtc = null, CancellationToken ct = default) =>
Task.FromResult(new ZB.MOM.WW.ScadaBridge.Commons.Types.AuditLogKpiSnapshot(0L, 0L, 0L, nowUtc ?? DateTime.UtcNow));
+ ///
public Task> GetExecutionTreeAsync(
Guid executionId, CancellationToken ct = default, IReadOnlyList? permittedSiteIds = null) =>
Task.FromResult>(Array.Empty());
+ ///
public Task> GetDistinctSourceNodesAsync(CancellationToken ct = default) =>
Task.FromResult>(Array.Empty());
}
@@ -194,6 +216,7 @@ public class AuditLogPurgeActorTests : TestKit, IClassFixtureVerifies that the purge actor runs a purge pass on each daily interval tick.
[Fact]
public void Tick_Fires_OnDailyInterval()
{
@@ -216,6 +239,7 @@ public class AuditLogPurgeActorTests : TestKit, IClassFixtureVerifies that a tick switches out partitions older than the retention threshold.
[Fact]
public void Tick_OldPartitions_SwitchedOut()
{
@@ -247,6 +271,7 @@ public class AuditLogPurgeActorTests : TestKit, IClassFixtureVerifies that a tick leaves partitions newer than the retention threshold untouched.
[Fact]
public void Tick_NewerPartitions_Untouched()
{
@@ -276,6 +301,7 @@ public class AuditLogPurgeActorTests : TestKit, IClassFixtureVerifies that a tick publishes a purged event carrying the number of rows removed.
[Fact]
public void Tick_PublishesPurgedEvent_WithRowCount()
{
@@ -301,6 +327,7 @@ public class AuditLogPurgeActorTests : TestKit, IClassFixtureVerifies that a failed switch-out does not prevent the remaining partitions from being processed.
[Fact]
public void Tick_SwitchThrows_OtherPartitionsStillProcessed()
{
@@ -333,6 +360,8 @@ public class AuditLogPurgeActorTests : TestKit, IClassFixtureVerifies that a real partition is switched out end to end, its rows are removed and a purged event is published.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task EndToEnd_RealPartition_RowsRemoved_PurgedEventPublished()
{
@@ -401,7 +430,7 @@ public class AuditLogPurgeActorTests : TestKit, IClassFixture.Instance)));
- // Fish for the Jan-2026 partition boundary — the only eligible one in this
+ // Fish for the January 2026 partition boundary — the only eligible one in this
// fixture DB. The generous timeout covers the real drop-and-rebuild dance
// against MSSQL which routinely takes a couple of seconds on a busy dev container.
var janBoundary = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
@@ -436,6 +465,7 @@ public class AuditLogPurgeActorTests : TestKit, IClassFixtureVerifies that the purge threshold is derived from the configured audit log retention days.
[Fact]
public void Threshold_UsesAuditLogOptionsRetentionDays()
{
@@ -467,9 +497,10 @@ public class AuditLogPurgeActorTests : TestKit, IClassFixtureVerifies that a per-channel retention override shorter than the global window triggers a channel purge.
[Fact]
public void PerChannelOverride_ShorterThanGlobal_TriggersChannelPurge()
{
@@ -515,9 +546,10 @@ public class AuditLogPurgeActorTests : TestKit, IClassFixtureVerifies that a per-channel retention override equal to or longer than the global window does not trigger a channel purge.
[Fact]
public void PerChannelOverride_EqualOrLongerThanGlobal_SkipsChannelPurge()
{
@@ -561,9 +593,10 @@ public class AuditLogPurgeActorTests : TestKit, IClassFixtureVerifies that a purge tick passes the configured maintenance command timeout to the repository.
[Fact]
public void PurgeTick_Passes_MaintenanceCommandTimeout_To_Repository()
{
@@ -608,9 +641,10 @@ public class AuditLogPurgeActorTests : TestKit, IClassFixtureVerifies that a failing switch-out publishes a purge-failed event and increments the health failure counter.
[Fact]
public void SwitchThrows_PublishesPurgeFailedEvent_AndIncrementsHealthCounter()
{
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/AuditLogPurgeOptionsTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/AuditLogPurgeOptionsTests.cs
index fddf8298..5400ad0c 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/AuditLogPurgeOptionsTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/AuditLogPurgeOptionsTests.cs
@@ -3,13 +3,14 @@ using ZB.MOM.WW.ScadaBridge.AuditLog.Central;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Central;
///
-/// Task 4 tests for /
+/// Tests for /
/// : the maintenance timeout
/// defaults to 30 minutes and is clamped to a 1-minute floor so a misconfigured 0/negative value
/// cannot yield a zero/negative ADO.NET command timeout (arch-review 04, S2).
///
public class AuditLogPurgeOptionsTests
{
+ /// Verifies that the maintenance command timeout defaults to 30 minutes.
[Fact]
public void MaintenanceCommandTimeout_Defaults_To_30_Minutes()
{
@@ -18,6 +19,9 @@ public class AuditLogPurgeOptionsTests
Assert.Equal(TimeSpan.FromMinutes(30), options.ResolvedMaintenanceCommandTimeout);
}
+ /// Verifies that the resolved maintenance command timeout is clamped to a minimum of one minute.
+ /// The configured timeout in minutes.
+ /// The expected resolved timeout in minutes.
[Theory]
[InlineData(0, 1)] // clamp floor — a zero config value cannot become a zero timeout
[InlineData(-5, 1)] // clamp floor — negatives are rejected by ADO.NET
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/AuditLogPurgeOptionsValidatorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/AuditLogPurgeOptionsValidatorTests.cs
index d2c05964..6aac7bad 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/AuditLogPurgeOptionsValidatorTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/AuditLogPurgeOptionsValidatorTests.cs
@@ -12,6 +12,8 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Central;
///
public class AuditLogPurgeOptionsValidatorTests
{
+ /// Verifies that the default options are valid.
+ /// The result.
[Fact]
public void DefaultOptions_AreValid()
{
@@ -19,6 +21,8 @@ public class AuditLogPurgeOptionsValidatorTests
Assert.True(validator.Validate(null, new AuditLogPurgeOptions()).Succeeded);
}
+ /// Verifies that a zero interval in hours fails validation.
+ /// The result.
[Fact]
public void ZeroIntervalHours_Fails()
{
@@ -29,6 +33,8 @@ public class AuditLogPurgeOptionsValidatorTests
f => f.Contains(nameof(AuditLogPurgeOptions.IntervalHours), StringComparison.Ordinal));
}
+ /// Verifies that a zero channel purge batch size fails validation.
+ /// The result.
[Fact]
public void ZeroChannelPurgeBatchSize_Fails()
{
@@ -40,6 +46,8 @@ public class AuditLogPurgeOptionsValidatorTests
f => f.Contains(nameof(AuditLogPurgeOptions.ChannelPurgeBatchSizeConfigured), StringComparison.Ordinal));
}
+ /// Verifies that a zero maintenance command timeout fails validation.
+ /// The result.
[Fact]
public void ZeroMaintenanceCommandTimeout_Fails()
{
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/CentralAuditRedactionFailureCounterTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/CentralAuditRedactionFailureCounterTests.cs
index 1932241f..834a6059 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/CentralAuditRedactionFailureCounterTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/CentralAuditRedactionFailureCounterTests.cs
@@ -11,16 +11,17 @@ using ZB.MOM.WW.ScadaBridge.AuditLog.Payload;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Central;
///
-/// Bundle E (M6-T9) coverage for the central-side payload-filter redactor
-/// failure bridge. M5 wired the SITE bridge
+/// Coverage for the central-side payload-filter redactor
+/// failure bridge. The SITE bridge
/// (HealthMetricsAuditRedactionFailureCounter) that pushes increments
-/// into the site health report; M6 mirrors that with
+/// into the site health report; the central bridge mirrors that with
/// so the same payload
/// filter — when it runs on the central writer paths — surfaces failures on
/// the central .
///
public class CentralAuditRedactionFailureCounterTests : TestKit
{
+ /// Verifies that incrementing the counter routes the failure to the central health snapshot.
[Fact]
public void Increment_Routes_To_Snapshot()
{
@@ -34,6 +35,7 @@ public class CentralAuditRedactionFailureCounterTests : TestKit
Assert.Equal(3, snapshot.AuditRedactionFailure);
}
+ /// Verifies that constructing the counter with a null snapshot throws.
[Fact]
public void Construction_With_Null_Snapshot_Throws()
{
@@ -41,6 +43,7 @@ public class CentralAuditRedactionFailureCounterTests : TestKit
() => new CentralAuditRedactionFailureCounter(null!));
}
+ /// Verifies that registering central maintenance replaces the redaction failure counter with the central implementation.
[Fact]
public void AddAuditLogCentralMaintenance_Replaces_IAuditRedactionFailureCounter_With_CentralImpl()
{
@@ -72,11 +75,12 @@ public class CentralAuditRedactionFailureCounterTests : TestKit
Assert.IsType(counter);
}
+ /// Verifies that the default redaction failure counter registered by the audit log is a no-op.
[Fact]
public void AddAuditLog_Default_IAuditRedactionFailureCounter_Is_NoOp()
{
// Sanity check: without AddAuditLogCentralMaintenance the default
- // remains the NoOp from M5 — the central bridge only takes effect
+ // remains the NoOp — the central bridge only takes effect
// when the central-only registration runs.
var config = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/CentralAuditWriteFailuresTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/CentralAuditWriteFailuresTests.cs
index ee4594d5..5221c689 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/CentralAuditWriteFailuresTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/CentralAuditWriteFailuresTests.cs
@@ -13,7 +13,7 @@ using IAuditInboundCeilingHitsCounter = ZB.MOM.WW.ScadaBridge.AuditLog.Central.I
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Central;
///
-/// Bundle E (M6-T8) regression coverage for the central-side audit-write
+/// Regression coverage for the central-side audit-write
/// failure counter. and
/// both swallow repository throws (audit
/// must NEVER abort the user-facing action, alog.md §13) but bump the
@@ -37,33 +37,43 @@ public class CentralAuditWriteFailuresTests : TestKit
///
private sealed class ThrowingRepo : IAuditLogRepository
{
+ ///
public Task> GetTraceEventsAsync(Guid executionId, CancellationToken ct = default, IReadOnlyList? permittedSiteIds = null)
=> throw new NotSupportedException("GetTraceEventsAsync is not exercised by this test double.");
+ ///
public Task InsertIfNotExistsAsync(AuditEvent evt, CancellationToken ct = default) =>
throw new InvalidOperationException("simulated repo failure");
+ ///
public Task> QueryAsync(
AuditLogQueryFilter filter, AuditLogPaging paging, CancellationToken ct = default) =>
Task.FromResult>(Array.Empty());
+ ///
public Task SwitchOutPartitionAsync(DateTime monthBoundary, TimeSpan? commandTimeout = null, CancellationToken ct = default) =>
Task.FromResult(0L);
+ ///
public Task PurgeChannelOlderThanAsync(
string channel, DateTime threshold, int batchSize, TimeSpan? commandTimeout = null, CancellationToken ct = default, int maxRows = 50_000) =>
Task.FromResult(0L);
+ ///
public Task BackfillSourceNodeAsync(
string sentinel, DateTime before, int batchSize, CancellationToken ct = default) =>
Task.FromResult(0L);
+ ///
public Task> GetPartitionBoundariesOlderThanAsync(
DateTime threshold, CancellationToken ct = default) =>
Task.FromResult>(Array.Empty());
+ ///
public Task GetKpiSnapshotAsync(
TimeSpan window, DateTime? nowUtc = null, CancellationToken ct = default) =>
Task.FromResult(new ZB.MOM.WW.ScadaBridge.Commons.Types.AuditLogKpiSnapshot(0L, 0L, 0L, nowUtc ?? DateTime.UtcNow));
+ ///
public Task> GetExecutionTreeAsync(
Guid executionId, CancellationToken ct = default, IReadOnlyList? permittedSiteIds = null) =>
Task.FromResult>(Array.Empty());
+ ///
public Task> GetDistinctSourceNodesAsync(CancellationToken ct = default) =>
Task.FromResult>(Array.Empty());
}
@@ -75,10 +85,14 @@ public class CentralAuditWriteFailuresTests : TestKit
private sealed class RecordingFailureCounter : ICentralAuditWriteFailureCounter
{
private int _count;
+ /// Gets the number of increments recorded so far.
public int Count => Volatile.Read(ref _count);
+ ///
public void Increment() => Interlocked.Increment(ref _count);
}
+ /// Verifies that a forced repository failure increments the audit-write failure counter.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Forced_Failure_Increments_Counter()
{
@@ -101,6 +115,8 @@ public class CentralAuditWriteFailuresTests : TestKit
Assert.Equal(1, counter.Count);
}
+ /// Verifies that a repository failure inside the ingest actor increments the audit-write failure counter.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task AuditLogIngestActor_Failure_Increments_Counter()
{
@@ -128,6 +144,7 @@ public class CentralAuditWriteFailuresTests : TestKit
Assert.Equal(batch.Length, counter.Count);
}
+ /// Verifies that the health snapshot aggregates the failure counters together with the stalled-telemetry state.
[Fact]
public void Snapshot_Aggregates_Counters_And_StalledState()
{
@@ -165,6 +182,7 @@ public class CentralAuditWriteFailuresTests : TestKit
Assert.Equal(1, snapshot.AuditRedactionFailure);
}
+ /// Verifies that a newly constructed health snapshot reports empty counters.
[Fact]
public void Snapshot_Empty_OnConstruction()
{
@@ -178,11 +196,12 @@ public class CentralAuditWriteFailuresTests : TestKit
}
// ---------------------------------------------------------------------
- // M5.3 (T7) AuditInboundCeilingHits counter
+ // AuditInboundCeilingHits counter
// AuditCentralHealthSnapshot implements IAuditInboundCeilingHitsCounter.
// Incrementing through the interface surface is reflected on the snapshot.
// ---------------------------------------------------------------------
+ /// Verifies that the inbound ceiling-hit counter starts at zero.
[Fact]
public void AuditInboundCeilingHits_StartsAtZero()
{
@@ -190,6 +209,7 @@ public class CentralAuditWriteFailuresTests : TestKit
Assert.Equal(0, snapshot.AuditInboundCeilingHits);
}
+ /// Verifies that incrementing the ceiling-hit counter through its interface is reflected on the snapshot.
[Fact]
public void AuditInboundCeilingHits_IncrementedThroughInterface_ReflectedOnSnapshot()
{
@@ -203,6 +223,7 @@ public class CentralAuditWriteFailuresTests : TestKit
Assert.Equal(3, snapshot.AuditInboundCeilingHits);
}
+ /// Verifies that concurrent increments of the ceiling-hit counter are not lost.
[Fact]
public void AuditInboundCeilingHits_IsThreadSafe()
{
@@ -217,6 +238,7 @@ public class CentralAuditWriteFailuresTests : TestKit
Assert.Equal(incrementCount, snapshot.AuditInboundCeilingHits);
}
+ /// Verifies that the ceiling-hit counter does not affect the other health counters.
[Fact]
public void AuditInboundCeilingHits_IsIndependentOfOtherCounters()
{
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/CentralAuditWriterTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/CentralAuditWriterTests.cs
index dbff7744..1f17dc70 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/CentralAuditWriterTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/CentralAuditWriterTests.cs
@@ -13,7 +13,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Central;
///
-/// M4 Bundle B1 — unit tests for , the
+/// Unit tests for , the
/// central-only direct-write implementation of .
/// The writer is a thin wrapper around
/// : it stamps
@@ -23,7 +23,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Central;
///
public class CentralAuditWriterTests
{
- // C3 (Task 2.5): canonical ZB.MOM.WW.Audit.AuditEvent via the shared factory.
+ // Canonical ZB.MOM.WW.Audit.AuditEvent via the shared factory.
private static AuditEvent NewEvent(Guid? eventId = null) =>
ScadaBridgeAuditEventFactory.Create(
channel: AuditChannel.Notification,
@@ -43,6 +43,8 @@ public class CentralAuditWriterTests
return (new CentralAuditWriter(provider, NullLogger.Instance), repo);
}
+ /// Verifies that WriteAsync passes the event to the repository's InsertIfNotExistsAsync.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task WriteAsync_PassesEvent_To_InsertIfNotExistsAsync()
{
@@ -56,6 +58,8 @@ public class CentralAuditWriterTests
Arg.Any());
}
+ /// Verifies that WriteAsync stamps the ingested-at UTC time on the event before inserting it.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task WriteAsync_Stamps_IngestedAtUtc_Before_Insert()
{
@@ -66,7 +70,7 @@ public class CentralAuditWriterTests
var after = DateTime.UtcNow;
await repo.Received(1).InsertIfNotExistsAsync(
- // C3 (Task 2.5): IngestedAtUtc now rides in DetailsJson on the canonical
+ // IngestedAtUtc now rides in DetailsJson on the canonical
// record — read it back via the decomposed row view.
Arg.Is(e =>
e.AsRow().IngestedAtUtc != null &&
@@ -75,6 +79,8 @@ public class CentralAuditWriterTests
Arg.Any());
}
+ /// Verifies that an exception thrown by the repository during WriteAsync does not propagate to the caller.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task WriteAsync_Repository_Throws_DoesNotPropagate()
{
@@ -90,6 +96,8 @@ public class CentralAuditWriterTests
await writer.WriteAsync(NewEvent());
}
+ /// Verifies that WriteAsync resolves the repository from a fresh scope on every call.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task WriteAsync_Resolves_Repository_PerCall_From_Fresh_Scope()
{
@@ -114,6 +122,7 @@ public class CentralAuditWriterTests
Assert.NotSame(instances[0], instances[1]);
}
+ /// Verifies that constructing the writer with a null service provider throws.
[Fact]
public void Constructor_NullServices_Throws()
{
@@ -121,6 +130,7 @@ public class CentralAuditWriterTests
() => new CentralAuditWriter(null!, NullLogger.Instance));
}
+ /// Verifies that constructing the writer with a null logger throws.
[Fact]
public void Constructor_NullLogger_Throws()
{
@@ -129,7 +139,7 @@ public class CentralAuditWriterTests
() => new CentralAuditWriter(services, null!));
}
- // ----- SourceNode stamping (Task 12) ----- //
+ // ----- SourceNode stamping ----- //
private static (CentralAuditWriter writer, IAuditLogRepository repo) BuildWriterWithIdentity(
INodeIdentityProvider? nodeIdentity)
@@ -147,6 +157,8 @@ public class CentralAuditWriterTests
return (writer, repo);
}
+ /// Verifies that WriteAsync stamps the source node from the identity provider when the event has none.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task WriteAsync_StampsSourceNodeFromProvider_WhenEventHasNone()
{
@@ -159,6 +171,8 @@ public class CentralAuditWriterTests
Arg.Any());
}
+ /// Verifies that WriteAsync preserves a source node supplied by the caller.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task WriteAsync_PreservesCallerProvidedSourceNode()
{
@@ -172,6 +186,8 @@ public class CentralAuditWriterTests
Arg.Any());
}
+ /// Verifies that WriteAsync leaves the source node null when the identity provider returns null.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task WriteAsync_LeavesSourceNodeNull_WhenProviderReturnsNull()
{
@@ -184,11 +200,13 @@ public class CentralAuditWriterTests
Arg.Any());
}
+ /// Verifies that WriteAsync passes the caller's source node through unchanged when no identity provider is injected.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task WriteAsync_PassesThroughCallerSourceNode_WhenNoProviderInjected()
{
// Locks the back-compat contract for the optional `nodeIdentity = null`
- // ctor parameter: when no provider is wired (e.g. legacy M4 test
+ // ctor parameter: when no provider is wired (e.g. legacy test
// composition roots), the writer must not stamp — caller value passes
// through unmodified. Distinct code path from
// "provider supplied, returns null", which the test above covers.
@@ -201,6 +219,8 @@ public class CentralAuditWriterTests
Arg.Any());
}
+ /// Verifies that WriteAsync leaves the source node null when neither the caller nor an identity provider supplies one.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task WriteAsync_LeavesSourceNodeNull_WhenNoProviderInjected()
{
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/GrpcPullAuditEventsClientTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/GrpcPullAuditEventsClientTests.cs
index 61cba883..48eedc62 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/GrpcPullAuditEventsClientTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/GrpcPullAuditEventsClientTests.cs
@@ -12,7 +12,7 @@ using ProtoPullResponse = ZB.MOM.WW.ScadaBridge.Communication.Grpc.PullAuditEven
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Central;
///
-/// Bundle (M6) tests for — the
+/// Tests for — the
/// production that dials a site over gRPC
/// and issues the PullAuditEvents unary RPC for the reconciliation loop.
/// The real GrpcChannel is replaced by an injected
@@ -29,7 +29,10 @@ public class GrpcPullAuditEventsClientTests
private sealed class StaticEnumerator : ISiteEnumerator
{
private readonly IReadOnlyList _sites;
+ /// Initializes the enumerator with the fixed set of sites it will return.
+ /// The sites the enumerator returns on every call.
public StaticEnumerator(params SiteEntry[] sites) => _sites = sites;
+ ///
public Task> EnumerateAsync(CancellationToken ct = default) =>
Task.FromResult(_sites);
}
@@ -41,8 +44,11 @@ public class GrpcPullAuditEventsClientTests
///
private sealed class FakeInvoker : GrpcPullAuditEventsClient.IPullAuditEventsInvoker
{
+ /// Gets the endpoint most recently dialed through this invoker.
public string? Endpoint { get; private set; }
+ /// Gets the request most recently passed to this invoker.
public ProtoPullRequest? Request { get; private set; }
+ /// Gets the number of times this invoker has been called.
public int CallCount { get; private set; }
private readonly ProtoPullResponse? _response;
@@ -54,9 +60,17 @@ public class GrpcPullAuditEventsClientTests
_throw = toThrow;
}
+ /// Creates an invoker that returns the given response.
+ /// The response returned from every invocation.
+ /// An invoker that returns the given response.
public static FakeInvoker Returning(ProtoPullResponse response) => new(response, null);
+ /// Creates an invoker that throws the given exception.
+ /// The exception thrown from every invocation.
+ /// An invoker that throws the given exception.
public static FakeInvoker Throwing(Exception ex) => new(null, ex);
+ // Records the call and returns the canned response or throws the canned exception.
+ ///
public Task InvokeAsync(
string siteId, string endpoint, ProtoPullRequest request, CancellationToken ct)
{
@@ -72,17 +86,22 @@ public class GrpcPullAuditEventsClientTests
}
///
- /// Per-endpoint fake invoker (Task 18): routes dials by endpoint string so a
+ /// Per-endpoint fake invoker: routes dials by endpoint string so a
/// NodeA-down / NodeB-up failover can be scripted. Records dialed endpoints.
///
private sealed class PerEndpointInvoker : GrpcPullAuditEventsClient.IPullAuditEventsInvoker
{
private readonly Dictionary> _byEndpoint;
+ /// Gets the endpoints dialed so far, in call order.
public List Dialed { get; } = new();
+ /// Initializes the invoker with the scripted response factory for each endpoint.
+ /// The response factory keyed by endpoint address.
public PerEndpointInvoker(Dictionary> byEndpoint) =>
_byEndpoint = byEndpoint;
+ // Records the dialed endpoint and returns the response scripted for it.
+ ///
public Task InvokeAsync(
string siteId, string endpoint, ProtoPullRequest request, CancellationToken ct)
{
@@ -100,6 +119,8 @@ public class GrpcPullAuditEventsClientTests
status: AuditStatus.Delivered,
sourceSiteId: "site-a"));
+ /// Verifies that the pull dials the resolved endpoint and maps the returned events oldest first.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task PullAsync_dials_the_resolved_endpoint_and_maps_events_oldest_first()
{
@@ -132,6 +153,8 @@ public class GrpcPullAuditEventsClientTests
Assert.Equal(newer, result.Events[1].EventId);
}
+ /// Verifies that pulling from a site with no known endpoint returns an empty response.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task PullAsync_returns_empty_when_site_endpoint_is_unknown()
{
@@ -148,6 +171,9 @@ public class GrpcPullAuditEventsClientTests
Assert.Equal(0, invoker.CallCount); // never dialled — nothing to dial
}
+ /// Verifies that tolerable transport faults are swallowed and produce an empty response.
+ /// The gRPC status code the invoker fails with.
+ /// A task that represents the asynchronous operation.
[Theory]
[InlineData(StatusCode.Unavailable)] // connection refused / site offline
[InlineData(StatusCode.DeadlineExceeded)] // slow site / network blip
@@ -167,6 +193,8 @@ public class GrpcPullAuditEventsClientTests
Assert.False(result.MoreAvailable);
}
+ /// Verifies that connection-layer faults are swallowed and produce an empty response.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task PullAsync_swallows_connection_layer_faults_to_empty_response()
{
@@ -184,6 +212,8 @@ public class GrpcPullAuditEventsClientTests
Assert.False(result.MoreAvailable);
}
+ /// Verifies that unexpected faults are swallowed and produce an empty response.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task PullAsync_swallows_unexpected_faults_to_empty_response()
{
@@ -203,6 +233,8 @@ public class GrpcPullAuditEventsClientTests
Assert.False(result.MoreAvailable);
}
+ /// Verifies that an unspecified minimum-value cursor does not throw and the site is still dialed.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task PullAsync_with_minvalue_unspecified_cursor_does_not_throw_and_dials()
{
@@ -233,10 +265,12 @@ public class GrpcPullAuditEventsClientTests
Assert.False(result.MoreAvailable);
}
+ /// Verifies that the pull fails over to the secondary node when the primary raises a transport fault.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task PullAsync_fails_over_to_NodeB_when_primary_raises_transport_fault()
{
- // Task 18: NodeA (primary) is unreachable, NodeB (fallback) answers — the
+ // NodeA (primary) is unreachable, NodeB (fallback) answers — the
// pull must return NodeB's events rather than collapsing to empty.
var id = Guid.NewGuid();
var fallbackResponse = new ProtoPullResponse { MoreAvailable = false };
@@ -259,6 +293,8 @@ public class GrpcPullAuditEventsClientTests
Assert.Equal(id, evt.EventId);
}
+ /// Verifies that no failover is attempted when the site has no fallback endpoint configured.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task PullAsync_does_not_fail_over_when_no_fallback_configured()
{
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/GrpcPullSiteCallsClientTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/GrpcPullSiteCallsClientTests.cs
index 4daa0a21..df3b7d18 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/GrpcPullSiteCallsClientTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/GrpcPullSiteCallsClientTests.cs
@@ -11,7 +11,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Central;
///
/// Tests for — the production
/// that dials a site over gRPC and issues the
-/// PullSiteCalls unary RPC for the Site Call Audit (#22) reconciliation
+/// PullSiteCalls unary RPC for the Site Call Audit reconciliation
/// loop. The real GrpcChannel is replaced by an injected
/// seam so the
/// client's mapping / ordering / SourceSite-restamp / fault-swallowing behaviour
@@ -26,15 +26,21 @@ public class GrpcPullSiteCallsClientTests
private sealed class StaticEnumerator : ISiteEnumerator
{
private readonly IReadOnlyList _sites;
+ /// Initializes a new instance of the StaticEnumerator class.
+ /// The sites.
public StaticEnumerator(params SiteEntry[] sites) => _sites = sites;
+ ///
public Task> EnumerateAsync(CancellationToken ct = default) =>
Task.FromResult(_sites);
}
private sealed class FakeInvoker : GrpcPullSiteCallsClient.IPullSiteCallsInvoker
{
+ /// Gets the endpoint most recently dialed.
public string? Endpoint { get; private set; }
+ /// Gets the request most recently received.
public ProtoPullRequest? Request { get; private set; }
+ /// Gets the number of calls made.
public int CallCount { get; private set; }
private readonly ProtoPullResponse? _response;
@@ -46,9 +52,16 @@ public class GrpcPullSiteCallsClientTests
_throw = toThrow;
}
+ /// Creates an invoker that returns the given response.
+ /// The response to return.
+ /// A fake invoker returning .
public static FakeInvoker Returning(ProtoPullResponse response) => new(response, null);
+ /// Creates an invoker that throws the given exception.
+ /// The exception to throw.
+ /// A fake invoker that throws .
public static FakeInvoker Throwing(Exception ex) => new(null, ex);
+ ///
public Task InvokeAsync(
string siteId, string endpoint, ProtoPullRequest request, CancellationToken ct)
{
@@ -64,18 +77,22 @@ public class GrpcPullSiteCallsClientTests
}
///
- /// Per-endpoint fake invoker (Task 18): dials are routed by endpoint string so
+ /// Per-endpoint fake invoker: dials are routed by endpoint string so
/// a NodeA-down / NodeB-up failover can be scripted. Records every dialed
/// endpoint in order.
///
private sealed class PerEndpointInvoker : GrpcPullSiteCallsClient.IPullSiteCallsInvoker
{
private readonly Dictionary> _byEndpoint;
+ /// Gets the endpoints dialed, in order.
public List Dialed { get; } = new();
+ /// Initializes a new instance of the PerEndpointInvoker class.
+ /// The by endpoint.
public PerEndpointInvoker(Dictionary> byEndpoint) =>
_byEndpoint = byEndpoint;
+ ///
public Task InvokeAsync(
string siteId, string endpoint, ProtoPullRequest request, CancellationToken ct)
{
@@ -103,6 +120,8 @@ public class GrpcPullSiteCallsClientTests
UpdatedAtUtc = Timestamp.FromDateTime(updatedAtUtc),
};
+ /// Verifies that PullAsync dials the resolved endpoint, maps rows oldest first and restamps the source site.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task PullAsync_dials_resolved_endpoint_maps_oldest_first_and_restamps_source_site()
{
@@ -144,6 +163,8 @@ public class GrpcPullSiteCallsClientTests
Assert.Equal(1, result.SiteCalls[0].RetryCount);
}
+ /// Verifies that PullAsync returns an empty response when the site endpoint is unknown.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task PullAsync_returns_empty_when_site_endpoint_is_unknown()
{
@@ -160,6 +181,9 @@ public class GrpcPullSiteCallsClientTests
Assert.Equal(0, invoker.CallCount); // never dialled — nothing to dial
}
+ /// Verifies that PullAsync turns tolerable transport faults into an empty response.
+ /// The code.
+ /// A task that represents the asynchronous operation.
[Theory]
[InlineData(StatusCode.Unavailable)]
[InlineData(StatusCode.DeadlineExceeded)]
@@ -178,6 +202,8 @@ public class GrpcPullSiteCallsClientTests
Assert.False(result.MoreAvailable);
}
+ /// Verifies that PullAsync turns connection-layer faults into an empty response.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task PullAsync_swallows_connection_layer_faults_to_empty_response()
{
@@ -193,6 +219,8 @@ public class GrpcPullSiteCallsClientTests
Assert.False(result.MoreAvailable);
}
+ /// Verifies that PullAsync turns unexpected faults into an empty response.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task PullAsync_swallows_unexpected_faults_to_empty_response()
{
@@ -208,6 +236,8 @@ public class GrpcPullSiteCallsClientTests
Assert.False(result.MoreAvailable);
}
+ /// Verifies that PullAsync skips a poison row and still returns the good rows.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task PullAsync_skips_poison_row_and_returns_the_good_rows()
{
@@ -244,6 +274,8 @@ public class GrpcPullSiteCallsClientTests
Assert.False(result.MoreAvailable);
}
+ /// Verifies that PullAsync with a MinValue unspecified-kind cursor does not throw and still dials the site.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task PullAsync_with_minvalue_unspecified_cursor_does_not_throw_and_dials()
{
@@ -271,10 +303,12 @@ public class GrpcPullSiteCallsClientTests
Assert.False(result.MoreAvailable);
}
+ /// Verifies that PullAsync fails over to node B when the primary raises a transport fault.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task PullAsync_fails_over_to_NodeB_when_primary_raises_transport_fault()
{
- // Task 18: NodeA (primary) is unreachable, NodeB (fallback) answers — the
+ // NodeA (primary) is unreachable, NodeB (fallback) answers — the
// pull must return NodeB's rows rather than collapsing to empty.
var id = Guid.NewGuid();
var fallbackResponse = new ProtoPullResponse();
@@ -299,6 +333,8 @@ public class GrpcPullSiteCallsClientTests
Assert.Equal("site-a", row.SourceSite);
}
+ /// Verifies that PullAsync does not fail over when no fallback is configured.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task PullAsync_does_not_fail_over_when_no_fallback_configured()
{
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/SiteAuditReconciliationActorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/SiteAuditReconciliationActorTests.cs
index c145a9a9..deb2a259 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/SiteAuditReconciliationActorTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/SiteAuditReconciliationActorTests.cs
@@ -18,19 +18,21 @@ using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests.Migrations;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Central;
///
-/// Bundle B (M6-T3) tests for . Most
+/// Tests for . Most
/// tests substitute the with an in-memory
/// recording stub so the actor's tick / cursor / stalled state machinery can
/// be exercised in milliseconds without an MSSQL container. The duplicate /
/// idempotency assertion uses the real against
/// the so we verify InsertIfNotExistsAsync
-/// actually swallows duplicate-key collisions (the M2 Bundle A race-fix the
+/// actually swallows duplicate-key collisions (the race fix the
/// reconciliation puller depends on).
///
public class SiteAuditReconciliationActorTests : TestKit, IClassFixture
{
private readonly MsSqlMigrationFixture _fixture;
+ /// Initializes the test class with the shared MSSQL migration fixture.
+ /// The shared MSSQL migration fixture.
public SiteAuditReconciliationActorTests(MsSqlMigrationFixture fixture)
{
_fixture = fixture;
@@ -68,13 +70,17 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixture
private sealed class RecordingRepo : IAuditLogRepository
{
+ ///
public Task> GetTraceEventsAsync(Guid executionId, CancellationToken ct = default, IReadOnlyList? permittedSiteIds = null)
=> throw new NotSupportedException("GetTraceEventsAsync is not exercised by this test double.");
+ /// Gets the audit events recorded by this stub.
public List Inserted { get; } = new();
private readonly HashSet _seen = new();
+ /// Gets the number of times InsertIfNotExistsAsync was called.
public int InsertCallCount { get; private set; }
+ ///
public Task InsertIfNotExistsAsync(AuditEvent evt, CancellationToken ct = default)
{
InsertCallCount++;
@@ -85,33 +91,41 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixture
public Task> QueryAsync(
AuditLogQueryFilter filter, AuditLogPaging paging, CancellationToken ct = default) =>
Task.FromResult>(Inserted);
+ ///
public Task SwitchOutPartitionAsync(DateTime monthBoundary, TimeSpan? commandTimeout = null, CancellationToken ct = default) =>
Task.FromResult(0L);
+ ///
public Task PurgeChannelOlderThanAsync(
string channel, DateTime threshold, int batchSize, TimeSpan? commandTimeout = null, CancellationToken ct = default, int maxRows = 50_000) =>
Task.FromResult(0L);
+ ///
public Task BackfillSourceNodeAsync(
string sentinel, DateTime before, int batchSize, CancellationToken ct = default) =>
Task.FromResult(0L);
+ ///
public Task> GetPartitionBoundariesOlderThanAsync(
DateTime threshold, CancellationToken ct = default) =>
Task.FromResult>(Array.Empty());
+ ///
public Task GetKpiSnapshotAsync(
TimeSpan window, DateTime? nowUtc = null, CancellationToken ct = default) =>
Task.FromResult(new ZB.MOM.WW.ScadaBridge.Commons.Types.AuditLogKpiSnapshot(0L, 0L, 0L, nowUtc ?? DateTime.UtcNow));
+ ///
public Task> GetExecutionTreeAsync(
Guid executionId, CancellationToken ct = default, IReadOnlyList? permittedSiteIds = null) =>
Task.FromResult>(Array.Empty());
+ ///
public Task> GetDistinctSourceNodesAsync(CancellationToken ct = default) =>
Task.FromResult>(Array.Empty());
}
@@ -120,24 +134,30 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixture throws for ONE
/// poison (every attempt) but records every
/// other row — including the synthetic ReconciliationAbandoned row the actor
- /// emits once it gives up on the poison row (Task 17).
+ /// emits once it gives up on the poison row.
///
private sealed class PoisonInsertRepo : IAuditLogRepository
{
+ ///
public Task> GetTraceEventsAsync(Guid executionId, CancellationToken ct = default, IReadOnlyList? permittedSiteIds = null)
=> throw new NotSupportedException("GetTraceEventsAsync is not exercised by this test double.");
private readonly Guid _poisonId;
private readonly Exception _fault;
+ /// Gets the audit events recorded by this stub.
public List Inserted { get; } = new();
private readonly HashSet _seen = new();
+ /// Initializes the repository so that inserting the given event id always throws the given fault.
+ /// The event id whose insert always fails.
+ /// The exception thrown on every attempt to insert the poison row.
public PoisonInsertRepo(Guid poisonId, Exception fault)
{
_poisonId = poisonId;
_fault = fault;
}
+ ///
public Task InsertIfNotExistsAsync(AuditEvent evt, CancellationToken ct = default)
{
if (evt.EventId == _poisonId)
@@ -151,33 +171,41 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixture
public Task> QueryAsync(
AuditLogQueryFilter filter, AuditLogPaging paging, CancellationToken ct = default) =>
Task.FromResult>(Inserted);
+ ///
public Task SwitchOutPartitionAsync(DateTime monthBoundary, TimeSpan? commandTimeout = null, CancellationToken ct = default) =>
Task.FromResult(0L);
+ ///
public Task PurgeChannelOlderThanAsync(
string channel, DateTime threshold, int batchSize, TimeSpan? commandTimeout = null, CancellationToken ct = default, int maxRows = 50_000) =>
Task.FromResult(0L);
+ ///
public Task BackfillSourceNodeAsync(
string sentinel, DateTime before, int batchSize, CancellationToken ct = default) =>
Task.FromResult(0L);
+ ///
public Task> GetPartitionBoundariesOlderThanAsync(
DateTime threshold, CancellationToken ct = default) =>
Task.FromResult>(Array.Empty());
+ ///
public Task GetKpiSnapshotAsync(
TimeSpan window, DateTime? nowUtc = null, CancellationToken ct = default) =>
Task.FromResult(new ZB.MOM.WW.ScadaBridge.Commons.Types.AuditLogKpiSnapshot(0L, 0L, 0L, nowUtc ?? DateTime.UtcNow));
+ ///
public Task> GetExecutionTreeAsync(
Guid executionId, CancellationToken ct = default, IReadOnlyList? permittedSiteIds = null) =>
Task.FromResult>(Array.Empty());
+ ///
public Task> GetDistinctSourceNodesAsync(CancellationToken ct = default) =>
Task.FromResult>(Array.Empty());
}
@@ -191,19 +219,22 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixtureMaxPermanentInsertAttempts, so it keeps
/// hasUnresolvedFailure true and holds the cursor at since forever
- /// (P5-6's defect trigger). Dedupes inserted rows on
+ /// (the defect trigger). Dedupes inserted rows on
/// exactly like the real InsertIfNotExistsAsync, so a test asserting on
/// 's count is exercising the same idempotency contract
/// production relies on.
///
private sealed class AlwaysFailUnlessSyntheticRepo : IAuditLogRepository
{
+ ///
public Task> GetTraceEventsAsync(Guid executionId, CancellationToken ct = default, IReadOnlyList? permittedSiteIds = null)
=> throw new NotSupportedException("GetTraceEventsAsync is not exercised by this test double.");
+ /// Gets the audit events recorded by this stub.
public List Inserted { get; } = new();
private readonly HashSet _seen = new();
+ ///
public Task InsertIfNotExistsAsync(AuditEvent evt, CancellationToken ct = default)
{
if (!evt.Action.EndsWith(".ReconciliationAbandoned", StringComparison.Ordinal))
@@ -217,33 +248,41 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixture
public Task> QueryAsync(
AuditLogQueryFilter filter, AuditLogPaging paging, CancellationToken ct = default) =>
Task.FromResult>(Inserted);
+ ///
public Task SwitchOutPartitionAsync(DateTime monthBoundary, TimeSpan? commandTimeout = null, CancellationToken ct = default) =>
Task.FromResult(0L);
+ ///
public Task PurgeChannelOlderThanAsync(
string channel, DateTime threshold, int batchSize, TimeSpan? commandTimeout = null, CancellationToken ct = default, int maxRows = 50_000) =>
Task.FromResult(0L);
+ ///
public Task BackfillSourceNodeAsync(
string sentinel, DateTime before, int batchSize, CancellationToken ct = default) =>
Task.FromResult(0L);
+ ///
public Task> GetPartitionBoundariesOlderThanAsync(
DateTime threshold, CancellationToken ct = default) =>
Task.FromResult>(Array.Empty());
+ ///
public Task GetKpiSnapshotAsync(
TimeSpan window, DateTime? nowUtc = null, CancellationToken ct = default) =>
Task.FromResult(new ZB.MOM.WW.ScadaBridge.Commons.Types.AuditLogKpiSnapshot(0L, 0L, 0L, nowUtc ?? DateTime.UtcNow));
+ ///
public Task> GetExecutionTreeAsync(
Guid executionId, CancellationToken ct = default, IReadOnlyList? permittedSiteIds = null) =>
Task.FromResult>(Array.Empty());
+ ///
public Task> GetDistinctSourceNodesAsync(CancellationToken ct = default) =>
Task.FromResult>(Array.Empty());
}
@@ -254,7 +293,10 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixture _sites;
+ /// Initializes the enumerator with a fixed set of sites.
+ /// The sites returned on every enumeration.
public StaticEnumerator(params SiteEntry[] sites) => _sites = sites;
+ ///
public Task> EnumerateAsync(CancellationToken ct = default) =>
Task.FromResult(_sites);
}
@@ -266,22 +308,32 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixture
private sealed class ScriptedPullClient : IPullAuditEventsClient
{
+ /// Gets the recorded pull calls in the order they were made.
public List<(string SiteId, DateTime SinceUtc, string? AfterId, int BatchSize)> Calls { get; } = new();
private readonly Dictionary> _scripted = new();
private readonly Dictionary _throwOnSite = new();
+ /// Queues scripted responses for the given site.
+ /// The site whose pulls receive the scripted responses.
+ /// The responses returned in order on successive pulls.
+ /// This client, for chaining.
public ScriptedPullClient Script(string siteId, params PullAuditEventsResponse[] responses)
{
_scripted[siteId] = new Queue(responses);
return this;
}
+ /// Makes every pull for the given site throw the given exception.
+ /// The site whose pulls fail.
+ /// The exception to throw.
+ /// This client, for chaining.
public ScriptedPullClient ThrowFor(string siteId, Exception ex)
{
_throwOnSite[siteId] = ex;
return this;
}
+ ///
public Task PullAsync(
string siteId, DateTime sinceUtc, string? afterId, int batchSize, CancellationToken ct)
{
@@ -373,6 +425,7 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixtureVerifies that the reconciliation timer fires on the configured interval.
[Fact]
public void Timer_Fires_OnConfiguredInterval()
{
@@ -400,6 +453,7 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixtureVerifies that each tick pulls from every known site.
[Fact]
public void Tick_PullsFromEachKnownSite()
{
@@ -426,6 +480,7 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixtureVerifies that pulled events are ingested through the repository insert-if-not-exists call.
[Fact]
public void Tick_IngestEvents_ViaInsertIfNotExistsAsync()
{
@@ -456,6 +511,8 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixture()
.UseSqlServer(_fixture.ConnectionString).Options);
+ /// Verifies that duplicate events are not inserted twice.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task Tick_Duplicates_NotDoubleInserted()
{
@@ -509,6 +566,7 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixtureVerifies that the cursor advances to the maximum occurred-at timestamp of the pulled events.
[Fact]
public void Cursor_Advances_ToMaxOccurredAtUtc()
{
@@ -545,13 +603,14 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixtureVerifies that the cursor advances on the event id tiebreak when every row shares one instant.
[Fact]
public void Cursor_AdvancesOnTheIdTiebreak_WhenEveryRowSharesOneInstant()
{
@@ -583,6 +642,7 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixtureVerifies that both cursor halves are held back while a row is still being retried.
[Fact]
public void BothCursorHalves_AreHeldBack_WhileARowIsStillBeingRetried()
{
@@ -612,41 +672,51 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixtureRepository whose every insert throws, so the retry hold-back path is taken.
private sealed class AlwaysThrowingRepo : IAuditLogRepository
{
+ ///
public Task> GetTraceEventsAsync(Guid executionId, CancellationToken ct = default, IReadOnlyList? permittedSiteIds = null)
=> throw new NotSupportedException("GetTraceEventsAsync is not exercised by this test double.");
+ ///
public Task InsertIfNotExistsAsync(AuditEvent evt, CancellationToken ct = default) =>
throw new InvalidOperationException("central insert failed");
+ ///
public Task> QueryAsync(
AuditLogQueryFilter filter, AuditLogPaging paging, CancellationToken ct = default)
=> throw new NotSupportedException();
+ ///
public Task SwitchOutPartitionAsync(
DateTime monthBoundary, TimeSpan? commandTimeout = null, CancellationToken ct = default)
=> throw new NotSupportedException();
+ ///
public Task PurgeChannelOlderThanAsync(
string channel, DateTime threshold, int batchSize, TimeSpan? commandTimeout = null,
CancellationToken ct = default, int maxRows = 50_000)
=> throw new NotSupportedException();
+ ///
public Task BackfillSourceNodeAsync(
string sentinel, DateTime before, int batchSize, CancellationToken ct = default)
=> throw new NotSupportedException();
+ ///
public Task> GetPartitionBoundariesOlderThanAsync(
DateTime threshold, CancellationToken ct = default)
=> throw new NotSupportedException();
+ ///
public Task GetKpiSnapshotAsync(
TimeSpan window, DateTime? nowUtc = null, CancellationToken ct = default)
=> throw new NotSupportedException();
+ ///
public Task> GetExecutionTreeAsync(
Guid executionId, CancellationToken ct = default, IReadOnlyList? permittedSiteIds = null)
=> throw new NotSupportedException();
+ ///
public Task> GetDistinctSourceNodesAsync(CancellationToken ct = default)
=> throw new NotSupportedException();
}
@@ -655,6 +725,7 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixtureVerifies that a failure pulling from one site does not stop the other sites from being processed.
[Fact]
public void Tick_OneSiteThrows_OtherSitesStillProcessed()
{
@@ -686,6 +757,7 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixtureVerifies that two consecutive non-draining cycles publish a stalled state of true.
[Fact]
public void StalledDetection_TwoConsecutiveNonDrainingCycles_PublishesStalledTrue()
{
@@ -717,6 +789,7 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixtureVerifies that a draining cycle publishes a stalled state of false.
[Fact]
public void StalledDetection_DrainingCycle_PublishesStalledFalse()
{
@@ -750,11 +823,12 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixtureVerifies that a permanently abandoned row emits a ReconciliationAbandoned row and advances the cursor.
[Fact]
public void PermanentAbandonment_EmitsReconciliationAbandonedRow_AndAdvancesCursor()
{
@@ -804,11 +878,11 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixture
- /// Reproduces the P5-6 defect end to end: a permanently-poisoned row sits
+ /// Reproduces the rotating-poison defect end to end: a permanently-poisoned row sits
/// in a batch alongside a co-batch row that ALSO fails on every pull but
/// carries a fresh each time (modelling a
/// site that keeps producing new events with transient insert failures).
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/SiteAuditReconciliationOptionsValidatorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/SiteAuditReconciliationOptionsValidatorTests.cs
index 67fa7229..b2b70472 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/SiteAuditReconciliationOptionsValidatorTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/SiteAuditReconciliationOptionsValidatorTests.cs
@@ -12,6 +12,9 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Central;
///
public class SiteAuditReconciliationOptionsValidatorTests
{
+ /// Verifies that the batch size is valid only up to the RPC ceiling.
+ /// The batch size under test.
+ /// Whether the batch size is expected to validate.
[Theory]
[InlineData(1, true)]
[InlineData(2000, true)]
@@ -21,6 +24,7 @@ public class SiteAuditReconciliationOptionsValidatorTests
=> Assert.Equal(valid, new SiteAuditReconciliationOptionsValidator().Validate(null,
new SiteAuditReconciliationOptions { BatchSize = size }).Succeeded);
+ /// Verifies that the default options pass validation.
[Fact]
public void DefaultOptions_AreValid()
{
@@ -28,6 +32,7 @@ public class SiteAuditReconciliationOptionsValidatorTests
Assert.True(validator.Validate(null, new SiteAuditReconciliationOptions()).Succeeded);
}
+ /// Verifies that a zero reconciliation interval fails validation.
[Fact]
public void ZeroReconciliationInterval_Fails()
{
@@ -39,6 +44,7 @@ public class SiteAuditReconciliationOptionsValidatorTests
f => f.Contains(nameof(SiteAuditReconciliationOptions.ReconciliationIntervalSeconds), StringComparison.Ordinal));
}
+ /// Verifies that a zero batch size fails validation.
[Fact]
public void ZeroBatchSize_Fails()
{
@@ -50,6 +56,7 @@ public class SiteAuditReconciliationOptionsValidatorTests
f => f.Contains(nameof(SiteAuditReconciliationOptions.BatchSize), StringComparison.Ordinal));
}
+ /// Verifies that a zero stalled-cycle threshold fails validation.
[Fact]
public void ZeroStalledCycles_Fails()
{
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/SiteAuditTelemetryStalledTrackerTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/SiteAuditTelemetryStalledTrackerTests.cs
index 8171e5d5..485689c8 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/SiteAuditTelemetryStalledTrackerTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/SiteAuditTelemetryStalledTrackerTests.cs
@@ -5,7 +5,7 @@ using ZB.MOM.WW.ScadaBridge.AuditLog.Central;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Central;
///
-/// Bundle E (M6-T7) tests for .
+/// Tests for .
/// The tracker subscribes to the actor system's EventStream for
/// publications and maintains a
/// per-site latch the central health surface can read. Since reconciliation is
@@ -36,6 +36,7 @@ public class SiteAuditTelemetryStalledTrackerTests : TestKit
interval: TimeSpan.FromMilliseconds(20));
}
+ /// Verifies that a newly created tracker reports an empty snapshot.
[Fact]
public void Initial_Snapshot_IsEmpty()
{
@@ -46,6 +47,7 @@ public class SiteAuditTelemetryStalledTrackerTests : TestKit
Assert.Empty(snapshot);
}
+ /// Verifies that a stalled event makes the tracker report the site as stalled.
[Fact]
public void StalledTrue_Event_TrackerReports_Stalled()
{
@@ -57,6 +59,7 @@ public class SiteAuditTelemetryStalledTrackerTests : TestKit
Assert.True(snapshot["siteA"]);
}
+ /// Verifies that a not-stalled event makes the tracker report the site as not stalled.
[Fact]
public void StalledFalse_Event_TrackerReports_NotStalled()
{
@@ -72,6 +75,7 @@ public class SiteAuditTelemetryStalledTrackerTests : TestKit
Assert.False(snapshot["siteA"]);
}
+ /// Verifies that multiple sites are tracked independently of one another.
[Fact]
public void Multiple_Sites_Tracked_Independently()
{
@@ -88,6 +92,7 @@ public class SiteAuditTelemetryStalledTrackerTests : TestKit
Assert.True(snapshot["siteC"]);
}
+ /// Verifies that constructing the tracker with a null actor system throws.
[Fact]
public void Constructor_With_Null_ActorSystem_Throws()
{
@@ -95,6 +100,7 @@ public class SiteAuditTelemetryStalledTrackerTests : TestKit
() => new SiteAuditTelemetryStalledTracker((ActorSystem)null!));
}
+ /// Verifies that disposing the tracker unsubscribes it from the event stream.
[Fact]
public void Dispose_Unsubscribes_From_EventStream()
{
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/SiteEnumeratorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/SiteEnumeratorTests.cs
index 81febddb..448d1991 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/SiteEnumeratorTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Central/SiteEnumeratorTests.cs
@@ -41,6 +41,8 @@ public class SiteEnumeratorTests
return services.BuildServiceProvider();
}
+ /// Verifies that sites with a NodeA address are projected and sites with a blank address are skipped.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task EnumerateAsync_ProjectsSitesWithNodeAAddress_AndSkipsBlankOnes()
{
@@ -60,6 +62,8 @@ public class SiteEnumeratorTests
Assert.Equal("http://site-a:8083", entry.GrpcEndpoint);
}
+ /// Verifies that a site with a null NodeA address is skipped.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task EnumerateAsync_SkipsNullNodeAAddress()
{
@@ -76,10 +80,12 @@ public class SiteEnumeratorTests
Assert.Empty(result);
}
+ /// Verifies that a site with distinct NodeA and NodeB addresses projects NodeB as the fallback endpoint.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task EnumerateAsync_BothAddresses_ProjectsNodeBAsFallback()
{
- // Task 18: a site with both NodeA and a DISTINCT NodeB yields
+ // A site with both NodeA and a DISTINCT NodeB yields
// SiteEntry(primary=NodeA, FallbackGrpcEndpoint=NodeB).
var repository = Substitute.For();
repository.GetAllSitesAsync(Arg.Any()).Returns(new List
@@ -95,10 +101,12 @@ public class SiteEnumeratorTests
Assert.Equal("http://site-b:8083", entry.FallbackGrpcEndpoint);
}
+ /// Verifies that a site with only a NodeB address is not skipped and has no fallback endpoint.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task EnumerateAsync_OnlyNodeB_IsNotSkipped_AndHasNoFallback()
{
- // Task 18: a site with a blank NodeA but a valid NodeB is no longer
+ // A site with a blank NodeA but a valid NodeB is no longer
// skipped — NodeB becomes the primary and there is no separate fallback.
var repository = Substitute.For();
repository.GetAllSitesAsync(Arg.Any()).Returns(new List
@@ -114,6 +122,8 @@ public class SiteEnumeratorTests
Assert.Null(entry.FallbackGrpcEndpoint);
}
+ /// Verifies that enumeration returns an empty list when there are no sites.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task EnumerateAsync_ReturnsEmpty_WhenNoSites()
{
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Configuration/AuditLogOptionsBindingTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Configuration/AuditLogOptionsBindingTests.cs
index f1393d16..f362c03f 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Configuration/AuditLogOptionsBindingTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Configuration/AuditLogOptionsBindingTests.cs
@@ -13,7 +13,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Configuration;
///
-/// Bundle D (M5-T8) tests for hot-reloadable
+/// Tests for hot-reloadable
/// binding. The first test pins the JSON-realistic binding shape end-to-end
/// (scalars, lists, per-target overrides) so accidental drift in the section
/// layout breaks the build. The second test exercises the live hot-reload
@@ -23,13 +23,14 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Configuration;
/// flowing through without a restart.
///
///
-/// Distinct from (M1-T9) which covered
+/// Distinct from which covered
/// section binding + validator failures via single-key in-memory config — those
/// tests exist; these add (a) end-to-end binding from a realistic JSON literal
-/// and (b) the hot-reload behavioural contract the M5-T8 spec calls out.
+/// and (b) the hot-reload behavioural contract the spec calls out.
///
public class AuditLogOptionsBindingTests
{
+ /// Verifies that the AuditLog configuration section binds every field.
[Fact]
public void AuditLog_Section_Binds_AllFields()
{
@@ -71,7 +72,7 @@ public class AuditLogOptionsBindingTests
// HeaderRedactList: the Microsoft.Extensions.Configuration list binder
// APPENDS to the default list, so we assert containment rather than
- // exact equality (see M1-T9 AuditLogOptionsTests for the rationale).
+ // exact equality (see AuditLogOptionsTests for the rationale).
Assert.Contains("Authorization", opts.HeaderRedactList);
Assert.Contains("Custom-Token", opts.HeaderRedactList);
@@ -90,10 +91,11 @@ public class AuditLogOptionsBindingTests
Assert.Equal("@token|@secret", ov.RedactSqlParamsMatching);
}
+ /// Verifies that the purge options bind from the documented section and keys.
[Fact]
public void PurgeOptions_Bind_FromDocumentedSectionAndKeys()
{
- // AuditLog-013: the design doc (Component-AuditLog.md §Configuration)
+ // The design doc (Component-AuditLog.md §Configuration)
// documents the purge tuning as the nested `AuditLog:Purge` section with
// keys `IntervalHours` + `ChannelPurgeBatchSize`. This test pins that the
// code binds from EXACTLY that shape — the section path the production
@@ -139,6 +141,7 @@ public class AuditLogOptionsBindingTests
Assert.Equal(1000, opts.ChannelPurgeBatchSize);
}
+ /// Verifies that the filter behavior updates when the configuration reloads.
[Fact]
public void Filter_Behavior_Updates_OnConfigReload()
{
@@ -165,7 +168,7 @@ public class AuditLogOptionsBindingTests
Assert.True(Encoding.UTF8.GetByteCount(resultBefore.AsRow().RequestSummary!) <= 4096);
// Reload: cap raised to 16384 — next event must NOT truncate. This is
- // the M5-T8 contract: the filter sees the new value on the very next
+ // the hot-reload contract: the filter sees the new value on the very next
// Apply, without process restart.
monitor.Set(new AuditLogOptions { DefaultCapBytes = 16384 });
@@ -174,6 +177,7 @@ public class AuditLogOptionsBindingTests
Assert.Equal(body, resultAfter.AsRow().RequestSummary);
}
+ /// Verifies that the filter picks up a new body redactor when the configuration reloads.
[Fact]
public void Filter_PicksUp_NewBodyRedactor_OnConfigReload()
{
@@ -219,12 +223,21 @@ public class AuditLogOptionsBindingTests
private T _current;
private readonly List> _listeners = new();
+ /// Initializes a new instance of the TestOptionsMonitor class.
+ /// The initial.
public TestOptionsMonitor(T initial) => _current = initial;
+ /// Gets the current value value.
public T CurrentValue => _current;
+ /// Returns the current options value regardless of the name.
+ /// The options name; ignored.
+ /// The current options value.
public T Get(string? name) => _current;
+ /// Registers a listener to be notified when the options change.
+ /// The change callback.
+ /// A disposable that removes the listener, or null when none is tracked.
public IDisposable? OnChange(Action listener)
{
lock (_listeners)
@@ -234,6 +247,8 @@ public class AuditLogOptionsBindingTests
return new Unsubscribe(_listeners, listener);
}
+ /// Replaces the current value and notifies registered listeners.
+ /// The value.
public void Set(T value)
{
_current = value;
@@ -252,11 +267,15 @@ public class AuditLogOptionsBindingTests
{
private readonly List> _listeners;
private readonly Action _listener;
+ /// Initializes a new instance of the Unsubscribe class.
+ /// The listeners.
+ /// The listener.
public Unsubscribe(List> listeners, Action listener)
{
_listeners = listeners;
_listener = listener;
}
+ /// Releases the resources held by the test fixture.
public void Dispose()
{
lock (_listeners)
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Configuration/AuditLogOptionsTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Configuration/AuditLogOptionsTests.cs
index 19c27774..1471f7f3 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Configuration/AuditLogOptionsTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Configuration/AuditLogOptionsTests.cs
@@ -6,8 +6,8 @@ using ZB.MOM.WW.ScadaBridge.AuditLog.Configuration;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Configuration;
///
-/// Task 9 (Bundle E): binding + validator
-/// behavior. The validator enforces invariants used by M2+ writers
+/// Tests for binding and validator
+/// behavior. The validator enforces invariants used by the audit writers
/// (per docs/plans/2026-05-20-auditlog-m1-foundation.md):
/// DefaultCapBytes > 0, ErrorCapBytes >= DefaultCapBytes,
/// RetentionDays in [30, 3650]. Header-redact defaults match the
@@ -25,6 +25,7 @@ public class AuditLogOptionsTests
return services.BuildServiceProvider().GetRequiredService>();
}
+ /// Verifies that a fully populated configuration section binds every scalar field onto the options.
[Fact]
public void ValidBinding_PopulatesAllScalarFields()
{
@@ -40,6 +41,7 @@ public class AuditLogOptionsTests
Assert.Equal(180, opts.RetentionDays);
}
+ /// Verifies that an empty configuration section yields reasonable default option values.
[Fact]
public void DefaultsAreReasonable_WhenSectionEmpty()
{
@@ -56,6 +58,7 @@ public class AuditLogOptionsTests
Assert.Empty(opts.PerTargetOverrides);
}
+ /// Verifies that header redaction entries bound from configuration are appended to the default list.
[Fact]
public void HeaderRedactList_BindsFromConfig_AppendsToDefaults()
{
@@ -77,6 +80,7 @@ public class AuditLogOptionsTests
Assert.Contains("X-Api-Key", opts.HeaderRedactList);
}
+ /// Verifies that per-target override settings bind from configuration.
[Fact]
public void PerTargetOverrides_BindsFromConfig()
{
@@ -93,6 +97,7 @@ public class AuditLogOptionsTests
Assert.Contains(@"\d{16}", crm.AdditionalBodyRedactors!);
}
+ /// Verifies that an invalid default payload cap fails validation.
[Fact]
public void InvalidDefaultCapBytes_FailsValidation()
{
@@ -104,6 +109,7 @@ public class AuditLogOptionsTests
Assert.Contains("DefaultCapBytes", result.FailureMessage);
}
+ /// Verifies that an invalid error payload cap fails validation.
[Fact]
public void InvalidErrorCapBytes_FailsValidation()
{
@@ -115,6 +121,7 @@ public class AuditLogOptionsTests
Assert.Contains("ErrorCapBytes", result.FailureMessage);
}
+ /// Verifies that a retention period below the minimum fails validation.
[Fact]
public void RetentionDaysBelowMinimum_FailsValidation()
{
@@ -126,6 +133,7 @@ public class AuditLogOptionsTests
Assert.Contains("RetentionDays", result.FailureMessage);
}
+ /// Verifies that a retention period above the maximum fails validation.
[Fact]
public void RetentionDaysAboveMaximum_FailsValidation()
{
@@ -137,6 +145,7 @@ public class AuditLogOptionsTests
Assert.Contains("RetentionDays", result.FailureMessage);
}
+ /// Verifies that the default options pass validation.
[Fact]
public void DefaultOptions_PassValidation()
{
@@ -147,6 +156,7 @@ public class AuditLogOptionsTests
Assert.True(result.Succeeded, result.FailureMessage);
}
+ /// Verifies that an invalid retention value bound from configuration is rejected when the options value is accessed.
[Fact]
public void InvalidRetention_BoundViaConfig_RejectedOnValueAccess()
{
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Configuration/AuditLogOptionsValidatorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Configuration/AuditLogOptionsValidatorTests.cs
index b3207e42..d65958df 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Configuration/AuditLogOptionsValidatorTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Configuration/AuditLogOptionsValidatorTests.cs
@@ -3,7 +3,7 @@ using ZB.MOM.WW.ScadaBridge.AuditLog.Configuration;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Configuration;
///
-/// Task 1 of docs/plans/2026-05-23-inbound-api-full-response-audit.md:
+/// Per docs/plans/2026-05-23-inbound-api-full-response-audit.md, this class
/// pins the default to 1 MiB and
/// the validator bounds to [8 KiB, 16 MiB]. The inbound channel needs a
/// much larger ceiling than the 8 KiB / 64 KiB default/error caps that other
@@ -14,6 +14,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Configuration;
///
public class AuditLogOptionsValidatorTests
{
+ /// Verifies that the default options set the inbound maximum body size to one mebibyte.
[Fact]
public void Validate_InboundMaxBytes_DefaultOptions_IsOneMebibyte()
{
@@ -24,6 +25,8 @@ public class AuditLogOptionsValidatorTests
Assert.Equal(1_048_576, opts.InboundMaxBytes);
}
+ /// Verifies that an inbound maximum body size within the documented range passes validation.
+ /// The inbound maximum body size in bytes.
[Theory]
[InlineData(8_192)] // documented min
[InlineData(1_048_576)] // default
@@ -35,6 +38,8 @@ public class AuditLogOptionsValidatorTests
Assert.True(validator.Validate(null, opts).Succeeded);
}
+ /// Verifies that an inbound maximum body size outside the documented range fails validation.
+ /// The inbound maximum body size in bytes.
[Theory]
[InlineData(0)]
[InlineData(8_191)]
@@ -52,9 +57,10 @@ public class AuditLogOptionsValidatorTests
}
// ---------------------------------------------------------------------
- // M5.5 (T3) per-channel retention overrides
+ // Per-channel retention overrides
// ---------------------------------------------------------------------
+ /// Verifies that a per-channel retention shorter than the global retention passes validation.
[Fact]
public void Validate_PerChannelRetention_ShorterThanGlobal_Passes()
{
@@ -75,6 +81,7 @@ public class AuditLogOptionsValidatorTests
Assert.True(validator.Validate(null, opts).Succeeded);
}
+ /// Verifies that a per-channel retention equal to the global retention passes validation.
[Fact]
public void Validate_PerChannelRetention_EqualToGlobal_Passes()
{
@@ -90,6 +97,7 @@ public class AuditLogOptionsValidatorTests
Assert.True(validator.Validate(null, opts).Succeeded);
}
+ /// Verifies that a per-channel retention longer than the global retention fails validation.
[Fact]
public void Validate_PerChannelRetention_LongerThanGlobal_Fails()
{
@@ -110,6 +118,7 @@ public class AuditLogOptionsValidatorTests
&& f.Contains("ApiInbound", StringComparison.Ordinal));
}
+ /// Verifies that a per-channel retention below the minimum fails validation.
[Fact]
public void Validate_PerChannelRetention_BelowMinimum_Fails()
{
@@ -127,6 +136,7 @@ public class AuditLogOptionsValidatorTests
f => f.Contains(nameof(AuditLogOptions.PerChannelRetentionDays), StringComparison.Ordinal));
}
+ /// Verifies that a per-channel retention keyed by an unknown channel fails validation.
[Fact]
public void Validate_PerChannelRetention_UnknownChannelKey_Fails()
{
@@ -146,6 +156,7 @@ public class AuditLogOptionsValidatorTests
f => f.Contains("NotAChannel", StringComparison.Ordinal));
}
+ /// Verifies that the default empty per-channel retention map passes validation.
[Fact]
public void Validate_PerChannelRetention_DefaultEmpty_Passes()
{
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/AuditWriteFailureSafetyTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/AuditWriteFailureSafetyTests.cs
index 48214a9b..278a5c4a 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/AuditWriteFailureSafetyTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/AuditWriteFailureSafetyTests.cs
@@ -36,17 +36,16 @@ using System.Text.Encodings.Web;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Integration;
///
-/// Audit Log #23 — M4 Bundle E (Task E4) cross-boundary safety suite verifying
+/// Cross-boundary safety suite verifying
/// the alog.md §13 contract: an always-throwing audit writer NEVER aborts the
-/// user-facing action. Exercises every boundary that emits audit rows in M2,
-/// M3, and M4:
+/// user-facing action. Exercises every boundary that emits audit rows:
///
///
-/// - External system sync call (M2 Bundle F).
-/// - External system cached call (M3 Bundle E).
-/// - Database sync write (M4 Bundle A).
-/// - Inbound API request (M4 Bundle D).
-/// - Notification dispatcher (M4 Bundle B).
+/// - External system sync call (sync).
+/// - External system cached call (cached).
+/// - Database sync write (sync write).
+/// - Inbound API request (request).
+/// - Notification dispatcher (dispatch).
///
///
///
@@ -64,6 +63,8 @@ public class AuditWriteFailureSafetyTests : TestKit, IClassFixtureInitializes the suite with the shared MS SQL migration fixture.
+ /// The shared migrated MS SQL database fixture.
public AuditWriteFailureSafetyTests(MsSqlMigrationFixture fixture)
{
_fixture = fixture;
@@ -81,8 +82,10 @@ public class AuditWriteFailureSafetyTests : TestKit, IClassFixtureGets the number of times WriteAsync has been attempted.
public int Attempts => Volatile.Read(ref _attempts);
+ ///
public Task WriteAsync(AuditEvent evt, CancellationToken ct = default)
{
Interlocked.Increment(ref _attempts);
@@ -100,8 +103,10 @@ public class AuditWriteFailureSafetyTests : TestKit, IClassFixtureGets the number of times WriteAsync has been attempted.
public int Attempts => Volatile.Read(ref _attempts);
+ ///
public Task WriteAsync(AuditEvent evt, CancellationToken ct = default)
{
Interlocked.Increment(ref _attempts);
@@ -118,8 +123,10 @@ public class AuditWriteFailureSafetyTests : TestKit, IClassFixtureGets the number of times ForwardAsync has been attempted.
public int Attempts => Volatile.Read(ref _attempts);
+ ///
public Task ForwardAsync(CachedCallTelemetry telemetry, CancellationToken ct = default)
{
Interlocked.Increment(ref _attempts);
@@ -132,6 +139,8 @@ public class AuditWriteFailureSafetyTests : TestKit, IClassFixtureVerifies that a synchronous external system call still returns its result when the audit writer throws.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task EsgSyncCall_BrokenAuditWriter_StillReturnsResult()
{
@@ -171,6 +180,8 @@ public class AuditWriteFailureSafetyTests : TestKit, IClassFixtureVerifies that a cached external system call still returns a tracked operation id when the audit writer throws.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task EsgCachedCall_BrokenAuditWriter_StillReturnsTrackedOperationId()
{
@@ -214,6 +225,8 @@ public class AuditWriteFailureSafetyTests : TestKit, IClassFixtureVerifies that a synchronous database write still returns the rows-affected count when the audit writer throws.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DbSyncWrite_BrokenAuditWriter_StillReturnsRowsAffected()
{
@@ -269,6 +282,8 @@ public class AuditWriteFailureSafetyTests : TestKit, IClassFixtureVerifies that an inbound API request still returns HTTP 200 when the audit writer throws.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task InboundApi_BrokenAuditWriter_StillReturns200()
{
@@ -290,6 +305,8 @@ public class AuditWriteFailureSafetyTests : TestKit, IClassFixtureVerifies that a notification dispatch still transitions to delivered when the audit writer throws.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task NotificationDispatch_BrokenAuditWriter_StillTransitionsToDelivered()
{
@@ -381,7 +398,7 @@ public class AuditWriteFailureSafetyTests : TestKit, IClassFixtureInitializes the adapter with the outcome to return for every delivery.
+ /// The delivery outcome returned by every call.
public SingleOutcomeAdapter(DeliveryOutcome outcome) { _outcome = outcome; }
+ ///
public NotificationType Type => NotificationType.Email;
+ ///
public Task DeliverAsync(
Notification notification, CancellationToken cancellationToken = default)
=> Task.FromResult(_outcome);
@@ -472,12 +493,17 @@ public class AuditWriteFailureSafetyTests : TestKit, IClassFixture
{
+ /// Initializes the handler with the standard authentication handler dependencies.
+ /// The authentication scheme options monitor.
+ /// The logger factory.
+ /// The URL encoder.
public AlwaysAuthenticatedHandler(
IOptionsMonitor options,
Microsoft.Extensions.Logging.ILoggerFactory logger,
UrlEncoder encoder)
: base(options, logger, encoder) { }
+ ///
protected override Task
HandleAuthenticateAsync()
{
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/CachedCallCombinedTelemetryTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/CachedCallCombinedTelemetryTests.cs
index 3a37cbc9..aa723ca7 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/CachedCallCombinedTelemetryTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/CachedCallCombinedTelemetryTests.cs
@@ -15,7 +15,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Integration;
///
/// Bundle G G2 end-to-end suite for cached ExternalSystem.CachedCall
-/// lifecycle telemetry (Audit Log #23 / M3). Wires the full M3 pipeline:
+/// lifecycle telemetry. Wires the full pipeline:
/// site-local SQLite audit writer + operation tracking store + the production
/// + the test-side
/// that ALSO pushes each combined
@@ -28,7 +28,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Integration;
///
/// The bridge is driven directly via
/// — these tests do NOT spin up the actual S&F retry loop; that would
-/// require a full SiteRuntime host and is out of scope for M3 (the S&F
+/// require a full SiteRuntime host and is out of scope here (the S&F
/// observer hooks are exercised in ZB.MOM.WW.ScadaBridge.StoreAndForward.Tests at
/// unit level). The submit row is emitted via
/// because the
@@ -45,6 +45,8 @@ public class CachedCallCombinedTelemetryTests : TestKit, IClassFixtureInitializes the test with the shared SQL Server migration fixture.
+ /// The shared MS SQL migration fixture.
public CachedCallCombinedTelemetryTests(MsSqlMigrationFixture fixture)
{
_fixture = fixture;
@@ -106,6 +108,8 @@ public class CachedCallCombinedTelemetryTests : TestKit, IClassFixtureVerifies that two failed attempts followed by a success emit five audit rows and one delivered site call.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task CachedCall_FailFailSuccess_Emits_5_AuditRows_AND_1_SiteCall_Delivered()
{
@@ -177,6 +181,8 @@ public class CachedCallCombinedTelemetryTests : TestKit, IClassFixtureVerifies that a cached call whose attempts all fail and are parked emits a terminal Parked row.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task CachedCall_AllAttemptsFailedAndParked_Emits_Terminal_Parked()
{
@@ -227,6 +233,8 @@ public class CachedCallCombinedTelemetryTests : TestKit, IClassFixtureVerifies that an immediate success without store-and-forward emits an attempted row and resolves as delivered.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task CachedCall_ImmediateSuccess_NoSF_Emits_Attempted_And_Resolve_Delivered()
{
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/CachedWriteCombinedTelemetryTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/CachedWriteCombinedTelemetryTests.cs
index 43e0a87c..7abd6101 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/CachedWriteCombinedTelemetryTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/CachedWriteCombinedTelemetryTests.cs
@@ -34,6 +34,8 @@ public class CachedWriteCombinedTelemetryTests : TestKit, IClassFixtureInitializes the test class with the shared SQL Server migration fixture.
+ /// The shared SQL Server migration fixture.
public CachedWriteCombinedTelemetryTests(MsSqlMigrationFixture fixture)
{
_fixture = fixture;
@@ -93,6 +95,8 @@ public class CachedWriteCombinedTelemetryTests : TestKit, IClassFixtureVerifies that a successful cached write emits the full lifecycle ending in a delivered row.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task CachedWrite_Success_Emits_Delivered_Lifecycle()
{
@@ -144,6 +148,8 @@ public class CachedWriteCombinedTelemetryTests : TestKit, IClassFixtureVerifies that a parked cached write emits a terminal parked row.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task CachedWrite_Parked_Emits_Terminal_Parked()
{
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/CombinedTelemetryIdempotencyTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/CombinedTelemetryIdempotencyTests.cs
index acf143ca..6ea91e4b 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/CombinedTelemetryIdempotencyTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/CombinedTelemetryIdempotencyTests.cs
@@ -34,6 +34,8 @@ public class CombinedTelemetryIdempotencyTests : TestKit, IClassFixtureInitializes the suite with the shared MS SQL migration fixture.
+ /// The shared migrated MS SQL database fixture.
public CombinedTelemetryIdempotencyTests(MsSqlMigrationFixture fixture)
{
_fixture = fixture;
@@ -100,6 +102,8 @@ public class CombinedTelemetryIdempotencyTests : TestKit, IClassFixtureVerifies that a duplicate telemetry packet leaves a single audit row and upserts the site call monotonically.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task DuplicatePacket_AuditLogStaysAtOneRow_SiteCallsUpserted_Monotonically()
{
@@ -144,6 +148,8 @@ public class CombinedTelemetryIdempotencyTests : TestKit, IClassFixtureVerifies that an older status arriving after a newer one is a no-op.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task OutOfOrderPackets_OlderStatus_ArrivesAfterNewer_IsNoOp()
{
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/DatabaseSyncEmissionEndToEndTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/DatabaseSyncEmissionEndToEndTests.cs
index 0a7997b8..2e46f13c 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/DatabaseSyncEmissionEndToEndTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/DatabaseSyncEmissionEndToEndTests.cs
@@ -23,7 +23,7 @@ using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Integration;
///
-/// Audit Log #23 — M4 Bundle E (Task E1) end-to-end suite verifying every
+/// End-to-end suite verifying every
/// synchronous Database.Connection(name).Execute* /
/// ExecuteReader call made via the Bundle A
/// emits exactly one
@@ -33,7 +33,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Integration;
///
///
///
-/// Composes the same pipeline as the M2 :
+/// Composes the same pipeline as the :
/// in-memory + +
/// on the site, drained by a real
/// through a
@@ -57,6 +57,8 @@ public class DatabaseSyncEmissionEndToEndTests : TestKit, IClassFixtureInitializes the suite with the shared MS SQL migration fixture.
+ /// The shared migrated MS SQL database fixture.
public DatabaseSyncEmissionEndToEndTests(MsSqlMigrationFixture fixture)
{
_fixture = fixture;
@@ -170,6 +172,8 @@ public class DatabaseSyncEmissionEndToEndTests : TestKit, IClassFixtureVerifies that a database insert emits one central audit row carrying a write operation marker and the rows-affected count.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task DbWrite_Insert_Emits_OneCentralRow_WithExtraOpWrite_AndRowsAffected()
{
@@ -240,6 +244,8 @@ public class DatabaseSyncEmissionEndToEndTests : TestKit, IClassFixtureVerifies that a database select emits one central audit row carrying a read operation marker and the rows-returned count.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task DbWrite_Select_Emits_OneCentralRow_WithExtraOpRead_AndRowsReturned()
{
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/ExecutionIdCorrelationTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/ExecutionIdCorrelationTests.cs
index a936f0ee..f16ca3ce 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/ExecutionIdCorrelationTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/ExecutionIdCorrelationTests.cs
@@ -22,7 +22,7 @@ using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Integration;
///
-/// Audit Log #23 — ExecutionId end-to-end correlation suite verifying the
+/// ExecutionId end-to-end correlation suite verifying the
/// universal per-run correlation promise: every audit row produced by one
/// script execution carries the same non-null .
///
@@ -39,8 +39,8 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Integration;
/// reads the rows back from the central store and asserts the shared id.
///
///
-/// Composes the same pipeline as the M2
-/// and the M4 : an in-memory
+/// Composes the same pipeline as the sync-call
+/// and the database-write : an in-memory
/// + +
/// on the site, drained by a real
/// through the shared
@@ -57,6 +57,8 @@ public class ExecutionIdCorrelationTests : TestKit, IClassFixtureInitializes the suite with the shared MS SQL migration fixture.
+ /// The shared MS SQL migration fixture.
public ExecutionIdCorrelationTests(MsSqlMigrationFixture fixture)
{
_fixture = fixture;
@@ -146,6 +148,8 @@ public class ExecutionIdCorrelationTests : TestKit, IClassFixture.Instance)));
+ /// Verifies that one execution performing an API call and a database write produces central rows that all share one non-null execution id.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task OneExecution_ApiCallAndDbWrite_AllCentralRows_ShareOneNonNullExecutionId()
{
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/InboundApiAuditTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/InboundApiAuditTests.cs
index c5c84506..84f48fc7 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/InboundApiAuditTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/InboundApiAuditTests.cs
@@ -24,7 +24,7 @@ using System.Text.Encodings.Web;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Integration;
///
-/// Audit Log #23 — M4 Bundle E (Task E3) end-to-end audit trail for the
+/// End-to-end audit trail for the
/// inbound API surface. Wires the production
/// into a Microsoft.AspNetCore.TestHost
/// pipeline that mirrors the production
@@ -53,6 +53,8 @@ public class InboundApiAuditTests : IClassFixture
{
private readonly MsSqlMigrationFixture _fixture;
+ /// Initializes the test class with the shared MS SQL migration fixture.
+ /// The shared MS SQL migration fixture.
public InboundApiAuditTests(MsSqlMigrationFixture fixture)
{
_fixture = fixture;
@@ -144,12 +146,17 @@ public class InboundApiAuditTests : IClassFixture
///
private sealed class AlwaysAuthenticatedHandler : AuthenticationHandler
{
+ /// Initializes the handler with the standard authentication-handler dependencies.
+ /// The authentication scheme options monitor.
+ /// The logger factory.
+ /// The URL encoder.
public AlwaysAuthenticatedHandler(
Microsoft.Extensions.Options.IOptionsMonitor options,
Microsoft.Extensions.Logging.ILoggerFactory logger,
UrlEncoder encoder)
: base(options, logger, encoder) { }
+ ///
protected override Task HandleAuthenticateAsync()
{
var identity = new ClaimsIdentity(
@@ -200,6 +207,8 @@ public class InboundApiAuditTests : IClassFixture
return Assert.Single(finalRows);
}
+ /// Verifies that a post with a valid actor emits a delivered inbound-request audit row with HTTP status 200 and the actor populated.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task PostToApi_WithValidActor_Emits_InboundRequest_StatusDelivered_HttpStatus200_ActorPopulated()
{
@@ -232,6 +241,8 @@ public class InboundApiAuditTests : IClassFixture
Assert.NotNull(evt.AsRow().IngestedAtUtc);
}
+ /// Verifies that a post without authentication emits a failed inbound-auth-failure audit row with HTTP status 401 and no actor.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task PostToApi_Without_Auth_Emits_InboundAuthFailure_StatusFailed_HttpStatus401_ActorNull()
{
@@ -260,12 +271,14 @@ public class InboundApiAuditTests : IClassFixture
Assert.Equal(AuditStatus.Failed, evt.AsRow().Status);
Assert.Equal(401, evt.AsRow().HttpStatus);
// Never echo back an unauthenticated principal — middleware suppresses
- // the framework user resolution on 401/403 paths. C3 (Task 2.5): the
+ // the framework user resolution on 401/403 paths. The
// canonical Actor is a non-null string (empty when absent); the row view
// maps empty → null, preserving the "no principal" assertion.
Assert.Null(evt.AsRow().Actor);
}
+ /// Verifies that a post whose endpoint returns 500 emits a failed inbound-request audit row with HTTP status 500.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task PostToApi_Returning500_Emits_InboundRequest_StatusFailed_HttpStatus500()
{
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/Infrastructure/CombinedTelemetryDispatcher.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/Infrastructure/CombinedTelemetryDispatcher.cs
index 37e1a3e1..5675816b 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/Infrastructure/CombinedTelemetryDispatcher.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/Infrastructure/CombinedTelemetryDispatcher.cs
@@ -20,12 +20,12 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Integration.Infrastructure;
///
///
///
-/// Production wiring keeps the wire push deferred to M6 — the site SQLite hot
-/// path is the source of truth and a future M6 drain will push the rows
+/// Production wiring keeps the wire push deferred — the site SQLite hot
+/// path is the source of truth and a future drain will push the rows
/// through the gRPC client. For end-to-end testing today we need a way to
/// exercise the central dual-write transaction immediately, so this
/// dispatcher synthesises the wire packet inline and round-trips it through
-/// the stub client. The shape mirrors what the M6 drain will eventually emit.
+/// the stub client. The shape mirrors what the future drain will eventually emit.
///
///
/// Best-effort: both the inner forwarder call and the wire push are
@@ -40,6 +40,9 @@ public sealed class CombinedTelemetryDispatcher : ICachedCallTelemetryForwarder
private readonly ICachedCallTelemetryForwarder _inner;
private readonly ISiteStreamAuditClient _wireClient;
+ /// Initializes the dispatcher with the inner forwarder and the wire client.
+ /// The forwarder that handles site-local telemetry.
+ /// The client used to push audit rows over the wire.
public CombinedTelemetryDispatcher(
ICachedCallTelemetryForwarder inner,
ISiteStreamAuditClient wireClient)
@@ -48,7 +51,8 @@ public sealed class CombinedTelemetryDispatcher : ICachedCallTelemetryForwarder
_wireClient = wireClient ?? throw new ArgumentNullException(nameof(wireClient));
}
- ///
+ // Forwards the telemetry through the inner forwarder and then pushes its audit rows to the wire client.
+ ///
public async Task ForwardAsync(CachedCallTelemetry telemetry, CancellationToken ct = default)
{
ArgumentNullException.ThrowIfNull(telemetry);
@@ -67,7 +71,7 @@ public sealed class CombinedTelemetryDispatcher : ICachedCallTelemetryForwarder
}
// Project the same packet onto the wire and push it through the stub
- // client. This is the bit a future M6 drain will subsume — until
+ // client. This is the bit a future drain will subsume — until
// then the test wraps the two halves into one observer-driven step.
try
{
@@ -79,7 +83,7 @@ public sealed class CombinedTelemetryDispatcher : ICachedCallTelemetryForwarder
{
// Swallow — the audit row is still in SQLite for a future drain;
// the central row will materialise the next time the wire path
- // is exercised (or via the M6 reconciliation pull).
+ // is exercised (or via the reconciliation pull).
}
}
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/Infrastructure/CombinedTelemetryHarness.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/Infrastructure/CombinedTelemetryHarness.cs
index c7bdeff9..25d6431f 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/Infrastructure/CombinedTelemetryHarness.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/Infrastructure/CombinedTelemetryHarness.cs
@@ -21,7 +21,7 @@ using ZB.MOM.WW.ScadaBridge.SiteRuntime.Tracking;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Integration.Infrastructure;
///
-/// Shared end-to-end harness for the M3 cached-call combined telemetry tests
+/// Shared end-to-end harness for the cached-call combined telemetry tests
/// (G2/G3/G4). Composes the full pipeline:
///
/// - Site-local SQLite (in-memory) +
@@ -48,15 +48,25 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Integration.Infrastructure;
///
public sealed class CombinedTelemetryHarness : IAsyncDisposable
{
+ /// The site SQLite audit writer under test.
public SqliteAuditWriter SqliteWriter { get; }
+ /// The in-memory ring buffer fallback for audit events.
public RingBufferFallback Ring { get; }
+ /// The fallback audit writer that wraps the SQLite writer and the ring buffer.
public FallbackAuditWriter FallbackWriter { get; }
+ /// The site-local operation tracking store.
public OperationTrackingStore TrackingStore { get; }
+ /// The inner cached-call telemetry forwarder.
public CachedCallTelemetryForwarder InnerForwarder { get; }
+ /// The combined telemetry dispatcher that sends audit and operational state together.
public CombinedTelemetryDispatcher Dispatcher { get; }
+ /// The cached-call lifecycle bridge wired to the forwarder.
public CachedCallLifecycleBridge Bridge { get; }
+ /// The stub site-stream audit client that delivers packets directly to the ingest actor.
public DirectActorSiteStreamAuditClient StubClient { get; }
+ /// The central audit ingest actor.
public IActorRef IngestActor { get; }
+ /// The service provider backing the central repositories.
public IServiceProvider ServiceProvider { get; }
private readonly MsSqlMigrationFixture _fixture;
@@ -64,6 +74,10 @@ public sealed class CombinedTelemetryHarness : IAsyncDisposable
private readonly ServiceProvider _trackingLocalDbProvider;
private bool _disposed;
+ /// Builds the harness, wiring the site writer chain, dispatcher and central ingest actor against the shared fixture.
+ /// The MS SQL migration fixture providing the central database.
+ /// The Akka test kit that hosts the ingest actor.
+ /// Optional factory that replaces the default site-call audit repository.
public CombinedTelemetryHarness(
MsSqlMigrationFixture fixture,
Akka.TestKit.Xunit2.TestKit testKit,
@@ -133,15 +147,23 @@ public sealed class CombinedTelemetryHarness : IAsyncDisposable
/// dispatcher (the bridge's hooks fire only for S&F retry-loop
/// attempts; submit-row emission happens at the script call site).
///
+ /// The submit packet to forward.
+ /// A token to cancel the forward.
+ /// A task that represents the asynchronous operation.
public Task EmitSubmitAsync(CachedCallTelemetry submit, CancellationToken ct = default) =>
Dispatcher.ForwardAsync(submit, ct);
///
/// Convenience: route a per-attempt or terminal outcome through the bridge.
///
+ /// The attempt or terminal-outcome context to route.
+ /// A token to cancel the emit.
+ /// A task that represents the asynchronous operation.
public Task EmitAttemptAsync(CachedCallAttemptContext context, CancellationToken ct = default) =>
Bridge.OnAttemptCompletedAsync(context, ct);
+ /// Creates a fresh central database context for reading back persisted rows.
+ /// A new .
public ScadaBridgeDbContext CreateReadContext()
{
var options = new DbContextOptionsBuilder()
@@ -173,6 +195,8 @@ public sealed class CombinedTelemetryHarness : IAsyncDisposable
return services.BuildServiceProvider();
}
+ /// Releases the resources held by this test class asynchronously.
+ /// A task that represents the asynchronous operation.
public async ValueTask DisposeAsync()
{
if (_disposed) return;
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/Infrastructure/DirectActorSiteStreamAuditClient.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/Infrastructure/DirectActorSiteStreamAuditClient.cs
index 85bab4e6..f757af89 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/Infrastructure/DirectActorSiteStreamAuditClient.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/Infrastructure/DirectActorSiteStreamAuditClient.cs
@@ -12,8 +12,8 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Integration.Infrastructure;
/// Shared component-level test double that
/// short-circuits the gRPC wire and forwards each batch directly to a central
/// via Akka .
-/// Lives under Integration/Infrastructure/ so both the M2 sync-call and
-/// M3 cached-call end-to-end suites can reuse it.
+/// Lives under Integration/Infrastructure/ so both the sync-call and
+/// cached-call end-to-end suites can reuse it.
///
///
///
@@ -27,8 +27,8 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Integration.Infrastructure;
///
///
/// arms a deterministic number of failures
-/// before the stub recovers; it applies to BOTH RPCs because the M2 sync-call
-/// retry behaviour and the M3 cached-telemetry retry behaviour share a single
+/// before the stub recovers; it applies to BOTH RPCs because the sync-call
+/// retry behaviour and the cached-telemetry retry behaviour share a single
/// SiteAuditTelemetryActor drain. Tests that need to differentiate per-RPC
/// failures should reach for a per-test wrapper rather than extending this
/// shared infrastructure.
@@ -41,6 +41,8 @@ public sealed class DirectActorSiteStreamAuditClient : ISiteStreamAuditClient
private int _callCount;
private int _cachedTelemetryCallCount;
+ /// Initializes the client with the central ingest actor that receives every forwarded batch.
+ /// The central AuditLogIngestActor reference batches are asked of.
public DirectActorSiteStreamAuditClient(IActorRef ingestActor)
{
_ingestActor = ingestActor ?? throw new ArgumentNullException(nameof(ingestActor));
@@ -69,6 +71,7 @@ public sealed class DirectActorSiteStreamAuditClient : ISiteStreamAuditClient
///
public int CachedTelemetryCallCount => Volatile.Read(ref _cachedTelemetryCallCount);
+ ///
public async Task IngestAuditEventsAsync(AuditEventBatch batch, CancellationToken ct)
{
Interlocked.Increment(ref _callCount);
@@ -106,18 +109,11 @@ public sealed class DirectActorSiteStreamAuditClient : ISiteStreamAuditClient
return ack;
}
- ///
- /// M3 dual-write path: decode each into
- /// the paired (, ) entry and
- /// Ask the central ingest actor with an .
- /// The accepted EventIds returned by the actor's dual-write transaction map
- /// back into the proto ack.
- ///
- ///
- /// Uses the shared for the audit half
- /// and for the SiteCall half — the same
- /// canonical mappers the production SiteStreamGrpcServer uses.
- ///
+ // Dual-write path: decodes each CachedTelemetryPacket into the paired (AuditEvent, SiteCall)
+ // entry via the shared AuditEventDtoMapper.FromDto and SiteCallDtoMapper.FromDto (the same
+ // canonical mappers the production SiteStreamGrpcServer uses), then Asks the central ingest
+ // actor with an IngestCachedTelemetryCommand; the accepted EventIds map back into the proto ack.
+ ///
public async Task IngestCachedTelemetryAsync(CachedTelemetryBatch batch, CancellationToken ct)
{
Interlocked.Increment(ref _cachedTelemetryCallCount);
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/NotifyDispatcherAuditTrailTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/NotifyDispatcherAuditTrailTests.cs
index 365be0ce..886e0895 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/NotifyDispatcherAuditTrailTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/NotifyDispatcherAuditTrailTests.cs
@@ -20,7 +20,7 @@ using ZB.MOM.WW.ScadaBridge.NotificationOutbox.Messages;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Integration;
///
-/// Audit Log #23 — M4 Bundle E (Task E2): end-to-end audit trail produced by
+/// End-to-end audit trail produced by
/// the central dispatcher loop. Wires
/// the production onto the real
/// against the per-class
@@ -51,6 +51,8 @@ public class NotifyDispatcherAuditTrailTests : TestKit, IClassFixtureInitializes the test class with the shared MSSQL migration fixture.
+ /// The shared MSSQL migration fixture.
public NotifyDispatcherAuditTrailTests(MsSqlMigrationFixture fixture)
{
_fixture = fixture;
@@ -105,13 +107,17 @@ public class NotifyDispatcherAuditTrailTests : TestKit, IClassFixture _outcomes;
public int CallCount;
+ /// Initializes the adapter with the delivery outcomes it returns in order.
+ /// The outcomes returned by successive delivery calls.
public QueuedOutcomeAdapter(params DeliveryOutcome[] outcomes)
{
_outcomes = new Queue(outcomes);
}
+ ///
public NotificationType Type => NotificationType.Email;
+ ///
public Task DeliverAsync(
Notification notification, CancellationToken cancellationToken = default)
{
@@ -133,7 +139,7 @@ public class NotifyDispatcherAuditTrailTests : TestKit, IClassFixtureNextAttemptAt is immediately due —
/// useful so the SECOND DispatchTick re-claims the row without waiting.
- /// NO-002: the dispatcher now clamps a non-positive RetryDelay to the
+ /// The dispatcher now clamps a non-positive RetryDelay to the
/// 1-minute fallback to avoid burn-looping on transient failures, so this
/// must be a strictly positive value (1 ms is fine for tests).
///
@@ -242,6 +248,8 @@ public class NotifyDispatcherAuditTrailTests : TestKit, IClassFixtureVerifies that a transient failure followed by a success emits two attempt rows and a terminal delivered row.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task NotifyDispatcher_FailThenSuccess_Emits_TwoAttempts_OneDelivered_Terminal()
{
@@ -319,6 +327,8 @@ public class NotifyDispatcherAuditTrailTests : TestKit, IClassFixtureVerifies that a throwing audit writer does not prevent the notification delivery from succeeding.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task NotifyDispatcher_AuditWriter_Throws_DeliveryStillSucceeds()
{
@@ -394,8 +404,10 @@ public class NotifyDispatcherAuditTrailTests : TestKit, IClassFixtureGets the number of write attempts made against this writer.
public int AttemptCount => Volatile.Read(ref _attemptCount);
+ ///
public Task WriteAsync(AuditEvent evt, CancellationToken ct = default)
{
Interlocked.Increment(ref _attemptCount);
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/OutageReconciliationTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/OutageReconciliationTests.cs
index c02d5840..020c8df5 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/OutageReconciliationTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/OutageReconciliationTests.cs
@@ -21,7 +21,7 @@ using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests.Migrations;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Integration;
///
-/// Bundle F (#23 M6-T10) end-to-end test for the central-outage + reconciliation
+/// End-to-end test for the central-outage + reconciliation
/// recovery loop. Wires the real site SQLite hot-path
/// () and the central
/// with an backed by the real
@@ -39,7 +39,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Integration;
/// the real repository.
///
///
-/// The from M3 is push-only — it has no
+/// The is push-only — it has no
/// reconciliation puller — so we build the smaller stub inline rather than
/// retrofitting the shared harness with a code path it doesn't otherwise
/// need.
@@ -49,6 +49,8 @@ public class OutageReconciliationTests : TestKit, IClassFixtureInitializes the test class with the shared MS SQL migration fixture.
+ /// The shared MS SQL migration fixture.
public OutageReconciliationTests(MsSqlMigrationFixture fixture)
{
_fixture = fixture;
@@ -77,19 +79,23 @@ public class OutageReconciliationTests : TestKit, IClassFixtureGets the number of times PullAsync has been invoked.
public int CallCount { get; private set; }
+ /// Initializes the client over the given site audit queue.
+ /// The site audit queue to pull from.
public QueueBackedPullClient(ISiteAuditQueue siteQueue)
{
_siteQueue = siteQueue ?? throw new ArgumentNullException(nameof(siteQueue));
}
+ ///
public async Task PullAsync(
string siteId, DateTime sinceUtc, string? afterId, int batchSize, CancellationToken ct)
{
CallCount++;
- // Mirrors SiteStreamGrpcServer.PullAuditEvents exactly (WP2.3): the
+ // Mirrors SiteStreamGrpcServer.PullAuditEvents exactly: the
// INCOMING cursor is central's receipt, so everything at or before it
// is retired FIRST; the rows this call serves are NOT retired, because
// nothing yet proves central consumed them. A fault between here and
@@ -119,7 +125,10 @@ public class OutageReconciliationTests : TestKit, IClassFixture _sites;
+ /// Initializes the enumerator with a fixed set of sites.
+ /// The fixed sites to enumerate.
public StaticEnumerator(params SiteEntry[] sites) => _sites = sites;
+ ///
public Task> EnumerateAsync(CancellationToken ct = default) =>
Task.FromResult(_sites);
}
@@ -208,6 +217,8 @@ public class OutageReconciliationTests : TestKit, IClassFixtureVerifies that 200 events buffered during a central outage are caught up by reconciliation with no duplicates.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task CentralOutage_200Events_Buffer_Then_Reconciliation_Catches_Up_NoDuplicates()
{
@@ -297,8 +308,8 @@ public class OutageReconciliationTests : TestKit, IClassFixtureVerifies that repeated reconciliation pull cycles are idempotent.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task ReconciliationPull_Idempotent_Across_Two_Cycles()
{
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/ParentExecutionIdCorrelationTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/ParentExecutionIdCorrelationTests.cs
index a1b826cc..2b06d233 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/ParentExecutionIdCorrelationTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/ParentExecutionIdCorrelationTests.cs
@@ -40,7 +40,7 @@ using ZB.MOM.WW.ScadaBridge.TestSupport;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Integration;
///
-/// Audit Log #23 — ParentExecutionId cross-execution correlation headline
+/// ParentExecutionId cross-execution correlation headline
/// end-to-end suite. Verifies the inbound-API → routed-site-script bridge: an
/// inbound HTTP request runs an inbound method script that calls
/// Route.Call into a site instance; the routed site script does a sync
@@ -101,6 +101,8 @@ public class ParentExecutionIdCorrelationTests : TestKit, IClassFixtureInitializes the test class with the shared MS SQL migration fixture.
+ /// The shared MS SQL migration fixture.
public ParentExecutionIdCorrelationTests(MsSqlMigrationFixture fixture)
{
_fixture = fixture;
@@ -126,6 +128,8 @@ public class ParentExecutionIdCorrelationTests : TestKit, IClassFixtureVerifies that every audit row of a routed site run carries the inbound request's execution id as its parent execution id.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task InboundRoutedRun_AllRoutedRows_CarryInboundExecutionId_AsParentExecutionId()
{
@@ -507,7 +511,7 @@ public class ParentExecutionIdCorrelationTests : TestKit, IClassFixture
{
var pending = await sqliteWriter.ReadPendingAsync(256);
- // AuditLog-001: ReadPendingAsync now excludes the cached-lifecycle
+ // ReadPendingAsync now excludes the cached-lifecycle
// kinds (they ride the combined-telemetry drain), so we union
// them in via the dedicated read surface to keep the durability
// assertion covering EVERY expected Kind.
@@ -534,8 +538,10 @@ public class ParentExecutionIdCorrelationTests : TestKit, IClassFixture
private sealed class AlwaysDeliversAdapter : INotificationDeliveryAdapter
{
+ ///
public NotificationType Type => NotificationType.Email;
+ ///
public Task DeliverAsync(
ZB.MOM.WW.ScadaBridge.Commons.Entities.Notifications.Notification notification,
CancellationToken cancellationToken = default)
@@ -568,6 +574,12 @@ public class ParentExecutionIdCorrelationTests : TestKit, IClassFixture
public string? NotificationId { get; private set; }
+ /// Initializes the router with the collaborators the routed script run needs.
+ /// The site identifier stamped on routed runs.
+ /// The external system client used by the routed script.
+ /// The audit writer the routed script emits rows through.
+ /// The cached-call telemetry forwarder.
+ /// The store-and-forward service that buffers the notification.
public BridgingInstanceRouter(
string siteId,
IExternalSystemClient externalClient,
@@ -582,6 +594,8 @@ public class ParentExecutionIdCorrelationTests : TestKit, IClassFixture
public async Task RouteToCallAsync(
string siteId, RouteToCallRequest request, CancellationToken cancellationToken)
{
@@ -626,16 +640,19 @@ public class ParentExecutionIdCorrelationTests : TestKit, IClassFixture
public Task RouteToGetAttributesAsync(
string siteId, RouteToGetAttributesRequest request, CancellationToken cancellationToken)
=> throw new NotSupportedException();
+ ///
public Task RouteToSetAttributesAsync(
string siteId, RouteToSetAttributesRequest request, CancellationToken cancellationToken)
=> throw new NotSupportedException();
// WaitForAttribute is not part of this fixture's routed-Call audit scenario;
// mirror the other non-Call methods (unexercised here).
+ ///
public Task RouteToWaitForAttributeAsync(
string siteId, RouteToWaitForAttributeRequest request, CancellationToken cancellationToken)
=> throw new NotSupportedException();
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/PartitionMaintenanceTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/PartitionMaintenanceTests.cs
index 279e4502..4bb31edd 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/PartitionMaintenanceTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/PartitionMaintenanceTests.cs
@@ -11,7 +11,7 @@ using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests.Migrations;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Integration;
///
-/// Bundle F (#23 M6-T12) end-to-end tests for the
+/// End-to-end tests for the
/// hosted service running
/// the real EF/MSSQL against the
/// per-class . The migration seeds
@@ -30,6 +30,8 @@ public class PartitionMaintenanceTests : IClassFixture
{
private readonly MsSqlMigrationFixture _fixture;
+ /// Initializes the test class with the shared MS SQL migration fixture.
+ /// The shared MS SQL migration fixture.
public PartitionMaintenanceTests(MsSqlMigrationFixture fixture)
{
_fixture = fixture;
@@ -116,6 +118,8 @@ public class PartitionMaintenanceTests : IClassFixture
// 1. EndToEnd_DefaultLookahead_NoSplit_WhenFutureCovered
// ---------------------------------------------------------------------
+ /// Verifies that the default lookahead adds no new partition boundaries when future partitions are already covered.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task EndToEnd_DefaultLookahead_NoSplit_WhenFutureCovered()
{
@@ -162,6 +166,8 @@ public class PartitionMaintenanceTests : IClassFixture
// 2. EndToEnd_LookaheadLargerThanCovered_Splits_NewBoundaries
// ---------------------------------------------------------------------
+ /// Verifies that a lookahead larger than the covered range splits in new partition boundaries.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task EndToEnd_LookaheadLargerThanCovered_Splits_NewBoundaries()
{
@@ -218,6 +224,8 @@ public class PartitionMaintenanceTests : IClassFixture
// 3. EndToEnd_PartitionMaintenance_Idempotent_OverTwoRuns
// ---------------------------------------------------------------------
+ /// Verifies that running partition maintenance twice adds boundaries only once.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task EndToEnd_PartitionMaintenance_Idempotent_OverTwoRuns()
{
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/PartitionPurgeTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/PartitionPurgeTests.cs
index 6b228b2a..6c30c1e4 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/PartitionPurgeTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/PartitionPurgeTests.cs
@@ -19,7 +19,7 @@ using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests.Migrations;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Integration;
///
-/// Bundle F (#23 M6-T11) end-to-end test for the daily partition-switch
+/// End-to-end test for the daily partition-switch
/// purge: seeds three monthly partitions (Jan / Feb / Mar 2026) with direct
/// INSERTs that bypass the standard repository ingest path (so the seed
/// timestamps are explicit), drives against
@@ -46,6 +46,8 @@ public class PartitionPurgeTests : TestKit, IClassFixture
{
private readonly MsSqlMigrationFixture _fixture;
+ /// Initializes a new instance of the class.
+ /// The fixture used by the tests.
public PartitionPurgeTests(MsSqlMigrationFixture fixture)
{
_fixture = fixture;
@@ -71,11 +73,11 @@ public class PartitionPurgeTests : TestKit, IClassFixture
string kind = "ApiCall")
{
await using var cmd = conn.CreateCommand();
- // C5 (Task 2.5): dbo.AuditLog is now the 10 canonical columns + DetailsJson;
+ // dbo.AuditLog is now the 10 canonical columns + DetailsJson;
// the ScadaBridge domain fields (channel/kind/status/sourceSiteId) ride in
// DetailsJson and the SourceSiteId/Kind/Status computed columns auto-derive.
// Action = "{channel}.{kind}", Category = channel name, Outcome = Success.
- // The channel/kind are parameterized so the M5.5 per-channel purge test can
+ // The channel/kind are parameterized so the per-channel purge test can
// seed multiple channels into the same partition.
cmd.CommandText = @"
INSERT INTO dbo.AuditLog
@@ -111,7 +113,7 @@ VALUES
///
/// Asserts the post-purge index state: the partition-ALIGNED clustered
/// PK_AuditLog is intact and the non-aligned UX_AuditLog_EventId
- /// is absent (WP2.2 — AlignAuditLogEventIdUniqueness).
+ /// is absent (AlignAuditLogEventIdUniqueness).
///
///
/// The purge used to bracket its SWITCH with a DROP/CREATE of
@@ -164,7 +166,7 @@ SELECT
///
/// Deferred-work register row 82. Every actor these tests create ticks every
/// 100 ms () and switches
- /// out the Jan-2026 partition each time it finds one eligible. The tests relied on
+ /// out the January 2026 partition each time it finds one eligible. The tests relied on
/// 's Dispose → Shutdown to stop them between
/// test methods, but that shutdown is bounded by a timeout and does NOT fail when
/// it expires — so on a loaded machine a previous test's actor was still ticking
@@ -172,9 +174,9 @@ SELECT
///
///
/// That is what made PerChannelOverride_DeletesOnlyOverriddenChannelsOldRows
- /// flake under a parallel full-solution run: it seeds a Jan-15-2026 row and expects
+ /// flake under a parallel full-solution run: it seeds a 2026-01-15 row and expects
/// its own per-channel DELETE to remove exactly one row, but a leaked actor could
- /// switch the whole Jan-2026 partition out first, leaving nothing to delete
+ /// switch the whole January 2026 partition out first, leaving nothing to delete
/// (observed: Assert.Equal() Failure … Expected: 1, Actual: 0). Stopping the
/// actor inside the test that created it removes the dependency on shutdown timing
/// altogether.
@@ -228,9 +230,9 @@ SELECT
var retentionDays = (int)(DateTime.UtcNow - thresholdAnchor).TotalDays + 1;
return (
- Old: new DateTime(2026, 1, 15, 0, 0, 0, DateTimeKind.Utc), // in Jan-2026 partition → PURGED
- Mid: new DateTime(2026, 4, 15, 0, 0, 0, DateTimeKind.Utc), // in Apr-2026 partition → KEPT
- Recent: new DateTime(2026, 6, 15, 0, 0, 0, DateTimeKind.Utc), // in Jun-2026 partition → KEPT
+ Old: new DateTime(2026, 1, 15, 0, 0, 0, DateTimeKind.Utc), // in January 2026 partition → PURGED
+ Mid: new DateTime(2026, 4, 15, 0, 0, 0, DateTimeKind.Utc), // in April 2026 partition → KEPT
+ Recent: new DateTime(2026, 6, 15, 0, 0, 0, DateTimeKind.Utc), // in June 2026 partition → KEPT
RetentionDays: retentionDays
);
}
@@ -239,6 +241,8 @@ SELECT
// 1. EndToEnd_OldestPartition_PurgedViaActor_NewerKept
// ---------------------------------------------------------------------
+ /// Verifies the case: end To End, oldest Partition, purged Via Actor, newer Kept.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task EndToEnd_OldestPartition_PurgedViaActor_NewerKept()
{
@@ -283,7 +287,7 @@ SELECT
var actor = CreateActor(sp, purgeOptions, auditOptions);
- // The Jan-2026 partition boundary is the only eligible one in this fixture DB.
+ // The January 2026 partition boundary is the only eligible one in this fixture DB.
var janBoundary = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var matched = probe.FishForMessage(
isMessage: m => m.MonthBoundary == janBoundary,
@@ -313,14 +317,16 @@ SELECT
// 2. EndToEnd_AlignedUniqueness_AfterPurge
// ---------------------------------------------------------------------
+ /// Verifies the case: end To End, aligned Uniqueness, after Purge.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task EndToEnd_AlignedUniqueness_AfterPurge()
{
Skip.IfNot(_fixture.Available, _fixture.SkipReason);
- // Same shape as test 1 — purge the Jan-2026 partition and then assert the
+ // Same shape as test 1 — purge the January 2026 partition and then assert the
// index state. RetentionDays is computed dynamically so the threshold always
- // lands near 2026-01-20 (see SeedOccurredAt()). Since WP2.2 the switch touches
+ // lands near 2026-01-20 (see SeedOccurredAt()). The switch touches
// no index at all: uniqueness rides the aligned clustered PK, so there is
// nothing to drop before the SWITCH and nothing to rebuild after it.
var siteId = "purge-uxidx-" + Guid.NewGuid().ToString("N").Substring(0, 8);
@@ -370,14 +376,16 @@ SELECT
// 3. EndToEnd_InsertIfNotExistsAsync_StillIdempotent_AfterPurge
// ---------------------------------------------------------------------
+ /// Verifies the case: end To End, insert If Not Exists Async, still Idempotent, after Purge.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task EndToEnd_InsertIfNotExistsAsync_StillIdempotent_AfterPurge()
{
Skip.IfNot(_fixture.Available, _fixture.SkipReason);
- // Seed + purge the Jan-2026 row, THEN exercise InsertIfNotExistsAsync twice for
+ // Seed + purge the January 2026 row, THEN exercise InsertIfNotExistsAsync twice for
// a fresh recent EventId. The second call must be a no-op (duplicate-key collision
- // swallowed by the repository, per M2 Bundle A's race-fix) — which means the
+ // swallowed by the repository, per the repository's race fix) — which means the
// aligned clustered PK is still enforcing uniqueness as intended.
// RetentionDays is computed dynamically so the threshold always lands near
// 2026-01-20 (see SeedOccurredAt()).
@@ -455,11 +463,11 @@ SELECT
}
// ---------------------------------------------------------------------
- // 4. PerChannelOverride_DeletesOnlyOverriddenChannelsOldRows (M5.5 T3)
+ // 4. PerChannelOverride_DeletesOnlyOverriddenChannelsOldRows
// ---------------------------------------------------------------------
///
- /// M5.5 (T3): exercises
+ /// Exercises
/// directly against the real repository + fixture DB. Seeds, in the SAME partition,
/// old + recent rows for an OVERRIDDEN channel (ApiOutbound) and old + recent
/// rows for an UN-overridden channel (DbOutbound), then runs the per-channel
@@ -474,6 +482,7 @@ SELECT
/// the append-only writer-role DENYs do not bind (the role granularity is exercised
/// in the repository/migration tests).
///
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task PerChannelOverride_DeletesOnlyOverriddenChannelsOldRows()
{
@@ -538,17 +547,18 @@ SELECT
}
// ---------------------------------------------------------------------
- // 5. SwitchOut_WithExplicitCommandTimeout_Succeeds (Task 4)
+ // 5. SwitchOut_WithExplicitCommandTimeout_Succeeds
// ---------------------------------------------------------------------
///
- /// Task 4 (arch-review 04, S2): proves the explicit- maintenance-timeout
+ /// Proves the explicit- maintenance-timeout
/// overload of runs the real
/// staging/switch batch to completion against SQL Server — the old row is purged, the kept row
/// survives, and the returned sampled row-count reflects the switched partition. Exercising the
/// timeout path end-to-end guards against a regression that only sets the timeout on the sample
/// command and forgets the DDL batch (or vice versa).
///
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task SwitchOut_WithExplicitCommandTimeout_Succeeds()
{
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/SyncCallEmissionEndToEndTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/SyncCallEmissionEndToEndTests.cs
index 409610fa..5b4af438 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/SyncCallEmissionEndToEndTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Integration/SyncCallEmissionEndToEndTests.cs
@@ -20,7 +20,7 @@ using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests.Migrations;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Integration;
///
-/// Bundle H — end-to-end test wiring the full Audit Log #23 M2 sync-call pipeline:
+/// End-to-end test wiring the full sync-call audit pipeline:
/// over a backed by
/// an in-memory SQLite database; the drains
/// Pending rows and pushes them through a stub
@@ -30,7 +30,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Integration;
///
///
/// This is a component-level integration test, not a full Akka-cluster
-/// test (per the M2 brainstorm decision). The stub gRPC client short-circuits
+/// test. The stub gRPC client short-circuits
/// the wire so we exercise the real telemetry actor, the real ingest actor, the
/// real SQLite writer, and the real MSSQL repository — without standing up a
/// Kestrel host or two-cluster topology.
@@ -51,6 +51,8 @@ public class SyncCallEmissionEndToEndTests : TestKit, IClassFixtureInitializes the test with the shared MSSQL migration fixture.
+ /// The MSSQL fixture that supplies the central database.
public SyncCallEmissionEndToEndTests(MsSqlMigrationFixture fixture)
{
_fixture = fixture;
@@ -129,6 +131,8 @@ public class SyncCallEmissionEndToEndTests : TestKit, IClassFixture.Instance)));
+ /// Verifies that one written audit event reaches the central AuditLog within a reasonable time.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task EndToEnd_OneWrittenEvent_Reaches_Central_AuditLog_Within_Reasonable_Time()
{
@@ -174,6 +178,8 @@ public class SyncCallEmissionEndToEndTests : TestKit, IClassFixtureVerifies that a gRPC stub error leaves the row pending and the next tick delivers it successfully.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task EndToEnd_GrpcStubError_RowStays_Pending_NextTick_Succeeds()
{
@@ -246,6 +252,8 @@ public class SyncCallEmissionEndToEndTests : TestKit, IClassFixtureVerifies that submitting the same event twice results in only one central row.
+ /// A task that represents the asynchronous operation.
[SkippableFact]
public async Task EndToEnd_DuplicateSubmit_OnlyOneCentralRow()
{
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Kpi/AuditLogKpiSampleSourceTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Kpi/AuditLogKpiSampleSourceTests.cs
index d6c732a5..9b22d2df 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Kpi/AuditLogKpiSampleSourceTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Kpi/AuditLogKpiSampleSourceTests.cs
@@ -8,7 +8,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Kpi;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Kpi;
///
-/// M6 "KPI History & Trends" (K8) coverage for —
+/// "KPI History & Trends" coverage for —
/// the the
/// central recorder enumerates to snapshot Audit Log KPIs. Asserts the source maps the
/// repository's onto exactly three Global-scope samples
@@ -19,6 +19,7 @@ public class AuditLogKpiSampleSourceTests
{
private static readonly DateTime CapturedAt = new(2026, 6, 17, 12, 30, 0, DateTimeKind.Utc);
+ /// Verifies that the sample source reports the AuditLog source name.
[Fact]
public void Source_is_AuditLog()
{
@@ -28,6 +29,8 @@ public class AuditLogKpiSampleSourceTests
Assert.Equal(KpiSources.AuditLog, sut.Source);
}
+ /// Verifies that collecting emits three global samples mapped from the health snapshot.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CollectAsync_emits_three_global_samples_mapped_from_snapshot()
{
@@ -64,6 +67,8 @@ public class AuditLogKpiSampleSourceTests
AssertMetric(samples, "backlogTotal", 305);
}
+ /// Verifies that the sample window is anchored on the snapshot's capture time.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CollectAsync_anchors_the_window_on_capturedAtUtc()
{
@@ -83,6 +88,8 @@ public class AuditLogKpiSampleSourceTests
Arg.Any());
}
+ /// Verifies that collecting returns no samples when the snapshot is null.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CollectAsync_returns_empty_when_snapshot_is_null()
{
@@ -98,6 +105,8 @@ public class AuditLogKpiSampleSourceTests
Assert.Empty(samples);
}
+ /// Verifies that the backlog provider's value is used when one is registered.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CollectAsync_uses_backlog_provider_when_present()
{
@@ -127,11 +136,13 @@ public class AuditLogKpiSampleSourceTests
AssertMetric(samples, "backlogTotal", 42);
}
+ /// Verifies that the snapshot's own backlog total is used when no backlog provider is registered.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task CollectAsync_falls_back_to_snapshot_backlog_when_provider_absent()
{
// No provider (sites / unit tests): the sample source degrades to the
- // snapshot's own BacklogTotal — unchanged pre-Task-10 behavior.
+ // snapshot's own BacklogTotal — unchanged behavior from before the provider existed.
var snapshot = new AuditLogKpiSnapshot(
TotalEventsLastHour: 10,
ErrorEventsLastHour: 1,
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Redaction/AuditRedactorFailClosedTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Redaction/AuditRedactorFailClosedTests.cs
index 477de7a0..8b924c6c 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Redaction/AuditRedactorFailClosedTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Redaction/AuditRedactorFailClosedTests.cs
@@ -10,8 +10,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Redaction;
///
-/// Guards the fail-closed contract for unavailable redactor patterns
-/// (Gitea #35).
+/// Guards the fail-closed contract for unavailable redactor patterns.
///
///
/// The defect. rejects a pattern that is
@@ -118,6 +117,7 @@ public class AuditRedactorFailClosedTests
Assert.Equal(0, counter.Count);
}
+ /// Verifies that an unavailable global body redactor suppresses the payload rather than emitting it raw.
[Fact]
public void UnavailableGlobalBodyRedactor_SuppressesPayload_RatherThanEmittingItRaw()
{
@@ -152,6 +152,7 @@ public class AuditRedactorFailClosedTests
Assert.Equal(1, counter.Count);
}
+ /// Verifies that an unavailable per-target body redactor suppresses the payload.
[Fact]
public void UnavailablePerTargetBodyRedactor_SuppressesPayload()
{
@@ -267,16 +268,27 @@ public class AuditRedactorFailClosedTests
private sealed class CountingCounter : IAuditRedactionFailureCounter
{
private int _count;
+ /// Gets the number of redaction failures counted so far.
public int Count => Volatile.Read(ref _count);
+ ///
public void Increment() => Interlocked.Increment(ref _count);
}
private sealed class StaticMonitor : IOptionsMonitor
{
private readonly AuditLogOptions _value;
+ /// Initializes the monitor with a fixed options value.
+ /// The value.
public StaticMonitor(AuditLogOptions value) => _value = value;
+ /// Gets the current options value.
public AuditLogOptions CurrentValue => _value;
+ /// Returns the current options value regardless of the name.
+ /// The options name; ignored.
+ /// The current options value.
public AuditLogOptions Get(string? name) => _value;
+ /// Registers a listener to be notified when the options change.
+ /// The change callback.
+ /// A disposable that removes the listener, or null when none is tracked.
public IDisposable? OnChange(Action listener) => null;
}
@@ -286,10 +298,14 @@ public class AuditRedactorFailClosedTests
private readonly List> _listeners = new();
private int _reads;
+ /// Initializes the monitor with an initial options value.
+ /// The value.
public MutableMonitor(AuditLogOptions value) => _value = value;
+ /// Gets the number of times CurrentValue has been read.
public int CurrentValueReads => Volatile.Read(ref _reads);
+ /// Gets the current options value.
public AuditLogOptions CurrentValue
{
get
@@ -299,8 +315,14 @@ public class AuditRedactorFailClosedTests
}
}
+ /// Gets the current options value.
+ /// The options name (ignored).
+ /// The current options value.
public AuditLogOptions Get(string? name) => CurrentValue;
+ /// Registers a listener to be notified when the options change.
+ /// The change callback.
+ /// A disposable that removes the listener, or null when none is tracked.
public IDisposable? OnChange(Action listener)
{
lock (_listeners)
@@ -310,6 +332,8 @@ public class AuditRedactorFailClosedTests
return new Noop();
}
+ /// Replaces the current options value and notifies every registered listener.
+ /// The value.
public void Set(AuditLogOptions value)
{
_value = value;
@@ -326,6 +350,7 @@ public class AuditRedactorFailClosedTests
private sealed class Noop : IDisposable
{
+ /// Releases the resources held by the test fixture.
public void Dispose() { }
}
}
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Redaction/SafeDefaultAuditRedactorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Redaction/SafeDefaultAuditRedactorTests.cs
index 1f639c2e..e46476be 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Redaction/SafeDefaultAuditRedactorTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Redaction/SafeDefaultAuditRedactorTests.cs
@@ -5,7 +5,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Redaction;
///
-/// ScadaBridge audit re-architecture stage C2 (Task 2.5) tests for
+/// Tests for
/// — the minimal always-safe
/// fallback.
/// Header-only scrub of the always-sensitive default headers inside
@@ -35,6 +35,7 @@ public class SafeDefaultAuditRedactorTests
private static AuditDetails Details(AuditEvent evt) =>
AuditDetailsCodec.Deserialize(evt.DetailsJson);
+ /// Verifies that the default sensitive headers are redacted in the request summary.
[Fact]
public void Redacts_DefaultSensitiveHeaders_InRequestSummary()
{
@@ -48,6 +49,7 @@ public class SafeDefaultAuditRedactorTests
Assert.Contains("Content-Type: application/json", d.RequestSummary!);
}
+ /// Verifies that the default sensitive headers are redacted in the response summary.
[Fact]
public void Redacts_DefaultSensitiveHeaders_InResponseSummary()
{
@@ -61,6 +63,7 @@ public class SafeDefaultAuditRedactorTests
Assert.Contains("X-Other: ok", d.ResponseSummary!);
}
+ /// Verifies that header names are matched case-insensitively when redacting.
[Fact]
public void CaseInsensitive_HeaderName_Redacted()
{
@@ -72,6 +75,7 @@ public class SafeDefaultAuditRedactorTests
Assert.DoesNotContain("x-y-z", Details(result).RequestSummary!);
}
+ /// Verifies that a non-sensitive header is preserved unchanged.
[Fact]
public void NonSensitiveHeader_Preserved()
{
@@ -84,6 +88,7 @@ public class SafeDefaultAuditRedactorTests
Assert.Contains("Accept: application/json", d.RequestSummary!);
}
+ /// Verifies that an event with null details takes the fast path and returns the same instance.
[Fact]
public void NullDetails_FastPath_ReturnsSameInstance()
{
@@ -102,6 +107,7 @@ public class SafeDefaultAuditRedactorTests
Assert.Same(evt, result);
}
+ /// Verifies that malformed details JSON never causes the redactor to throw.
[Fact]
public void MalformedDetailsJson_NeverThrows()
{
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Redaction/ScadaBridgeAuditRedactorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Redaction/ScadaBridgeAuditRedactorTests.cs
index f6ccec76..075b0df9 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Redaction/ScadaBridgeAuditRedactorTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Redaction/ScadaBridgeAuditRedactorTests.cs
@@ -11,7 +11,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Redaction;
///
-/// ScadaBridge audit re-architecture stage C2 (Task 2.5) tests for
+/// ScadaBridge audit re-architecture stage C2 tests for
/// — the canonical
/// implementation. Covers the header-redaction /
/// body-regex / SQL-param / safety-net / truncation pipeline operating on
@@ -20,6 +20,8 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Redaction;
///
public class ScadaBridgeAuditRedactorTests
{
+ /// Verifies that every field of an error row is redacted and the error message is capped, whether the redactor is configured globally or per target.
+ /// True to configure the redactor through a per-target override; false to configure it globally.
[Theory]
[InlineData(false)]
[InlineData(true)]
@@ -35,6 +37,7 @@ public class ScadaBridgeAuditRedactorTests
Assert.True(Encoding.UTF8.GetByteCount(Details(result).ErrorMessage!) <= 128);
}
+ /// Verifies that the SQL parameter policy does not persist values echoed back in an exception message.
[Fact]
public void EI3_SqlParameterPolicy_DoesNotPersistExceptionEchoedValues()
{
@@ -101,6 +104,7 @@ public class ScadaBridgeAuditRedactorTests
// ---- Header redaction (ports HeaderRedactionTests) ---------------------
+ /// Verifies that an Authorization bearer header is redacted.
[Fact]
public void HeaderRedaction_AuthorizationBearer_Redacted()
{
@@ -116,6 +120,7 @@ public class ScadaBridgeAuditRedactorTests
Assert.Contains("application/json", d.RequestSummary);
}
+ /// Verifies that header redaction is case-insensitive, so a lowercase authorization header is redacted.
[Fact]
public void HeaderRedaction_CaseInsensitive_LowercaseAuthorization_Redacted()
{
@@ -127,6 +132,7 @@ public class ScadaBridgeAuditRedactorTests
Assert.Contains("\"authorization\":\"\"", Details(result).RequestSummary!);
}
+ /// Verifies that a custom redact list redacts a custom header name.
[Fact]
public void HeaderRedaction_CustomRedactList_RedactsCustomHeaderName()
{
@@ -141,6 +147,7 @@ public class ScadaBridgeAuditRedactorTests
Assert.Contains("Bearer keep-me", d.RequestSummary!);
}
+ /// Verifies that a request summary that is not JSON passes through header redaction unchanged.
[Fact]
public void HeaderRedaction_NonJson_RequestSummary_Unchanged()
{
@@ -153,6 +160,7 @@ public class ScadaBridgeAuditRedactorTests
// ---- Body regex redaction (ports BodyRegexRedactionTests) --------------
+ /// Verifies that the global regex redacts a Hunter-style password value.
[Fact]
public void GlobalRegex_HunterPassword_Redacted()
{
@@ -170,6 +178,7 @@ public class ScadaBridgeAuditRedactorTests
Assert.Contains("alice", d.RequestSummary!);
}
+ /// Verifies that the body regex is applied to the error detail and the extra field.
[Fact]
public void BodyRegex_AppliesToErrorDetailAndExtra()
{
@@ -190,6 +199,7 @@ public class ScadaBridgeAuditRedactorTests
Assert.Contains("", d.Extra!);
}
+ /// Verifies that a per-target regex is applied only to the matching target.
[Fact]
public void PerTargetRegex_OnlyAppliedToMatchingTarget()
{
@@ -213,6 +223,7 @@ public class ScadaBridgeAuditRedactorTests
Assert.Equal(input, Details(unmatched).RequestSummary);
}
+ /// Verifies that a field is left unchanged when no regex is configured.
[Fact]
public void NoRegexConfigured_FieldUnchanged()
{
@@ -223,6 +234,7 @@ public class ScadaBridgeAuditRedactorTests
Assert.Equal("{\"password\":\"hunter2\"}", Details(result).RequestSummary);
}
+ /// Verifies that a regex that times out turns the field into the redacted marker and increments the failure counter.
[Fact]
public void RegexThrowsTimeout_FieldBecomesRedactedMarker_CounterIncrements()
{
@@ -257,6 +269,7 @@ public class ScadaBridgeAuditRedactorTests
return sb.ToString();
}
+ /// Verifies that SQL parameters are kept verbatim when no opt-in is configured.
[Fact]
public void NoOptIn_ParamsVerbatim_Unchanged()
{
@@ -272,6 +285,7 @@ public class ScadaBridgeAuditRedactorTests
Assert.Equal(input, Details(result).RequestSummary);
}
+ /// Verifies that an opt-in regex on parameter names redacts the matching values and keeps the others.
[Fact]
public void OptInRegex_AtToken_RedactsThoseValues_KeepsOthers()
{
@@ -298,6 +312,7 @@ public class ScadaBridgeAuditRedactorTests
Assert.DoesNotContain("k-987", d.RequestSummary!);
}
+ /// Verifies that a channel other than database-outbound is not affected by the SQL parameter policy.
[Fact]
public void NonDbOutboundChannel_NotAffected()
{
@@ -320,6 +335,7 @@ public class ScadaBridgeAuditRedactorTests
// ---- Truncation + cap selection (ports TruncationTests) ----------------
+ /// Verifies that a 10 KB request summary on a success row is truncated to 8 KB and flagged as payload-truncated.
[Fact]
public void SuccessRow_10KB_RequestSummary_TruncatedTo8KB_PayloadTruncatedTrue()
{
@@ -334,6 +350,7 @@ public class ScadaBridgeAuditRedactorTests
Assert.True(d.PayloadTruncated);
}
+ /// Verifies that a 10 KB request summary on a failed row is not truncated because it is under the error cap.
[Fact]
public void ErrorRow_Failed_10KB_RequestSummary_NotTruncated_UnderErrorCap()
{
@@ -348,6 +365,7 @@ public class ScadaBridgeAuditRedactorTests
Assert.False(d.PayloadTruncated);
}
+ /// Verifies that a 70 KB request summary on a failed row is truncated to 64 KB.
[Fact]
public void ErrorRow_Failed_70KB_RequestSummary_TruncatedTo64KB()
{
@@ -362,6 +380,7 @@ public class ScadaBridgeAuditRedactorTests
Assert.True(d.PayloadTruncated);
}
+ /// Verifies that an attempted status is treated as an error and uses the error cap even though the outcome is success.
[Fact]
public void StatusAttempted_TreatedAsError_UsesErrorCap_EvenThoughOutcomeSuccess()
{
@@ -379,6 +398,7 @@ public class ScadaBridgeAuditRedactorTests
Assert.False(d.PayloadTruncated);
}
+ /// Verifies that a skipped status is treated as an error and uses the error cap even though the outcome is success.
[Fact]
public void StatusSkipped_TreatedAsError_UsesErrorCap_EvenThoughOutcomeSuccess()
{
@@ -391,6 +411,7 @@ public class ScadaBridgeAuditRedactorTests
Assert.False(Details(result).PayloadTruncated);
}
+ /// Verifies that a submitted status is treated as a success and uses the default cap.
[Fact]
public void StatusSubmitted_TreatedAsSuccess_UsesDefaultCap()
{
@@ -406,6 +427,7 @@ public class ScadaBridgeAuditRedactorTests
Assert.True(d.PayloadTruncated);
}
+ /// Verifies that a large inbound API body uses the inbound cap rather than the default cap.
[Fact]
public void ApiInbound_LargeBody_UsesInboundCap_NotDefault()
{
@@ -420,6 +442,7 @@ public class ScadaBridgeAuditRedactorTests
Assert.Equal(100_000, Encoding.UTF8.GetByteCount(d.RequestSummary!));
}
+ /// Verifies that a failed inbound API body above the inbound maximum is truncated to that maximum.
[Fact]
public void ApiInbound_Failed_BodyAboveInboundMaxBytes_TruncatedToInboundMaxBytes()
{
@@ -435,6 +458,7 @@ public class ScadaBridgeAuditRedactorTests
Assert.True(Encoding.UTF8.GetByteCount(d.ResponseSummary!) <= 16_384);
}
+ /// Verifies that multibyte UTF-8 text is truncated at a character boundary rather than in the middle of a byte sequence.
[Fact]
public void Multibyte_UTF8_TruncatedAtCharacterBoundary_NotMidByte()
{
@@ -453,6 +477,7 @@ public class ScadaBridgeAuditRedactorTests
Assert.True(d.PayloadTruncated);
}
+ /// Verifies that a details payload that was already marked truncated stays truncated.
[Fact]
public void ExistingDetailsPayloadTruncated_RemainsTrue()
{
@@ -467,6 +492,7 @@ public class ScadaBridgeAuditRedactorTests
// ---- Target length cap -------------------------------------------------
+ /// Verifies that a target over the cap is truncated at a byte boundary.
[Fact]
public void Target_OverCap_Truncated_ToByteBoundary()
{
@@ -483,6 +509,7 @@ public class ScadaBridgeAuditRedactorTests
// ---- Fast-path ---------------------------------------------------------
+ /// Verifies that the fast path returns the same instance when the details are null and the target is short.
[Fact]
public void FastPath_NullDetailsAndShortTarget_ReturnsSameInstance()
{
@@ -502,6 +529,7 @@ public class ScadaBridgeAuditRedactorTests
Assert.Same(evt, result);
}
+ /// Verifies that the fast path returns the same instance when the details are empty and the target is short.
[Fact]
public void FastPath_EmptyDetailsAndShortTarget_ReturnsSameInstance()
{
@@ -523,6 +551,7 @@ public class ScadaBridgeAuditRedactorTests
// ---- Never-throws safety net -------------------------------------------
+ /// Verifies that malformed details JSON never throws and yields a safe copy.
[Fact]
public void MalformedDetailsJson_NeverThrows_ReturnsSafeCopy()
{
@@ -545,6 +574,7 @@ public class ScadaBridgeAuditRedactorTests
Assert.Null(ex);
}
+ /// Verifies that a fault thrown by the options monitor never leaks sensitive values, because the outer catch over-redacts every sensitive field.
[Fact]
public void OuterCatch_OptionsThrows_NeverLeaks_AllSensitiveFieldsOverRedacted()
{
@@ -601,7 +631,9 @@ public class ScadaBridgeAuditRedactorTests
private sealed class CountingRedactionFailureCounter : IAuditRedactionFailureCounter
{
private int _count;
+ /// Gets the number of recorded failures.
public int Count => _count;
+ ///
public void Increment() => System.Threading.Interlocked.Increment(ref _count);
}
@@ -609,9 +641,18 @@ public class ScadaBridgeAuditRedactorTests
private sealed class StaticMonitor : IOptionsMonitor
{
private readonly AuditLogOptions _value;
+ /// Initializes a new instance of the class.
+ /// The options snapshot returned on every read.
public StaticMonitor(AuditLogOptions value) => _value = value;
+ /// Gets the options value this monitor serves.
public AuditLogOptions CurrentValue => _value;
+ /// Returns the options value regardless of name.
+ /// The options name (ignored).
+ /// The options value.
public AuditLogOptions Get(string? name) => _value;
+ /// Ignores the listener; the options never change.
+ /// The change listener (ignored).
+ /// Always null.
public IDisposable? OnChange(Action listener) => null;
}
@@ -622,9 +663,16 @@ public class ScadaBridgeAuditRedactorTests
///
private sealed class ThrowingMonitor : IOptionsMonitor
{
+ /// Gets the options value this monitor serves.
public AuditLogOptions CurrentValue =>
throw new InvalidOperationException("Simulated options fault for outer-catch test");
+ /// Returns the options value regardless of name.
+ /// The options name (ignored).
+ /// The options value.
public AuditLogOptions Get(string? name) => new AuditLogOptions();
+ /// Ignores the listener; the options never change.
+ /// The change listener (ignored).
+ /// Always null.
public IDisposable? OnChange(Action listener) => null;
}
}
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/FallbackAuditWriterTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/FallbackAuditWriterTests.cs
index 2047de9c..42ac0be0 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/FallbackAuditWriterTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/FallbackAuditWriterTests.cs
@@ -10,7 +10,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Site;
///
-/// Bundle B (M2-T4) tests for — composes the
+/// Tests for — composes the
/// primary , the drop-oldest
/// , and an
/// health counter.
@@ -28,9 +28,12 @@ public class FallbackAuditWriterTests
/// Flip-switch primary writer mock.
private sealed class FlipSwitchPrimary : IAuditWriter
{
+ /// Gets or sets a value indicating whether the next write fails.
public bool FailNext { get; set; }
+ /// Gets the events the primary has accepted.
public List Written { get; } = new();
+ ///
public Task WriteAsync(AuditEvent evt, CancellationToken ct = default)
{
if (FailNext)
@@ -42,6 +45,8 @@ public class FallbackAuditWriterTests
}
}
+ /// Verifies that when the primary writer throws, the event lands in the ring buffer and the call still returns successfully.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task WriteAsync_PrimaryThrows_EventLandsInRing_CallReturnsSuccess()
{
@@ -59,6 +64,8 @@ public class FallbackAuditWriterTests
counter.Received(1).Increment();
}
+ /// Verifies that after the primary recovers, the ring buffer drains in FIFO order on the next write.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task WriteAsync_PrimaryRecovers_RingDrains_InFIFOOrder_OnNextWrite()
{
@@ -93,6 +100,8 @@ public class FallbackAuditWriterTests
Assert.Equal("c", primary.Written[3].Target);
}
+ /// Verifies that the ring buffer stays empty when the primary writer always succeeds.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task WriteAsync_PrimaryAlwaysSucceeds_Ring_StaysEmpty()
{
@@ -112,6 +121,8 @@ public class FallbackAuditWriterTests
counter.DidNotReceive().Increment();
}
+ /// Verifies that the failure counter is incremented once per primary failure.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task WriteAsync_FailureCounter_Incremented_Per_PrimaryFailure()
{
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/HealthMetricsAuditRedactionFailureCounterTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/HealthMetricsAuditRedactionFailureCounterTests.cs
index 4413d8a0..5f094ac6 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/HealthMetricsAuditRedactionFailureCounterTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/HealthMetricsAuditRedactionFailureCounterTests.cs
@@ -5,17 +5,18 @@ using ZB.MOM.WW.ScadaBridge.HealthMonitoring;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Site;
///
-/// Bundle C (M5-T7) — the
+/// The
/// adapter is the production binding for
/// on
/// site nodes; it forwards every
/// redactor over-redaction event into the shared
/// so the site health report surfaces the
-/// count as AuditRedactionFailure. Mirrors the M2 Bundle G
+/// count as AuditRedactionFailure. Mirrors the
/// HealthMetricsAuditWriteFailureCounter shape one-for-one.
///
public class HealthMetricsAuditRedactionFailureCounterTests
{
+ /// Verifies the case: increment, routes, to, collector, increment Audit Redaction Failure.
[Fact]
public void Increment_Routes_To_Collector_IncrementAuditRedactionFailure()
{
@@ -27,6 +28,7 @@ public class HealthMetricsAuditRedactionFailureCounterTests
collector.Received(1).IncrementAuditRedactionFailure();
}
+ /// Verifies the case: increment, multiple, calls, route, to, collector, each, time.
[Fact]
public void Increment_Multiple_Calls_Route_To_Collector_Each_Time()
{
@@ -40,6 +42,7 @@ public class HealthMetricsAuditRedactionFailureCounterTests
collector.Received(3).IncrementAuditRedactionFailure();
}
+ /// Verifies the case: construction, with, null, collector, throws, argument Null Exception.
[Fact]
public void Construction_With_Null_Collector_Throws_ArgumentNullException()
{
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/HealthMetricsAuditWriteFailureCounterTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/HealthMetricsAuditWriteFailureCounterTests.cs
index 57738105..838f441f 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/HealthMetricsAuditWriteFailureCounterTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/HealthMetricsAuditWriteFailureCounterTests.cs
@@ -5,7 +5,7 @@ using ZB.MOM.WW.ScadaBridge.HealthMonitoring;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Site;
///
-/// Bundle G (M2-T11) — the
+/// The
/// adapter is the production binding for
/// on site nodes; it forwards every FallbackAuditWriter primary failure into
/// the shared so the site health report
@@ -13,6 +13,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Site;
///
public class HealthMetricsAuditWriteFailureCounterTests
{
+ /// Verifies that a single increment is routed to the collector as a site audit write failure.
[Fact]
public void Increment_Routes_To_Collector_IncrementSiteAuditWriteFailures()
{
@@ -24,6 +25,7 @@ public class HealthMetricsAuditWriteFailureCounterTests
collector.Received(1).IncrementSiteAuditWriteFailures();
}
+ /// Verifies that each of several increments is routed to the collector.
[Fact]
public void Increment_Multiple_Calls_Route_To_Collector_Each_Time()
{
@@ -37,6 +39,7 @@ public class HealthMetricsAuditWriteFailureCounterTests
collector.Received(3).IncrementSiteAuditWriteFailures();
}
+ /// Verifies that constructing the counter with a null collector throws an .
[Fact]
public void Construction_With_Null_Collector_Throws_ArgumentNullException()
{
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/RingBufferFallbackTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/RingBufferFallbackTests.cs
index 995f6f17..28af1fee 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/RingBufferFallbackTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/RingBufferFallbackTests.cs
@@ -6,7 +6,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Site;
///
-/// Bundle B (M2-T3) tests for — the
+/// Tests for — the
/// drop-oldest fallback used by when the
/// primary SQLite writer is throwing.
///
@@ -23,6 +23,8 @@ public class RingBufferFallbackTests
target: target);
}
+ /// Verifies that enqueuing 1025 events into a ring with a capacity of 1024 drops the oldest event and raises the overflow notification exactly once.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Enqueue_1025_Into_1024Cap_Ring_DropsOldest_AndRaisesOverflowOnce()
{
@@ -51,6 +53,8 @@ public class RingBufferFallbackTests
Assert.Equal("1024", drained[^1].Target);
}
+ /// Verifies that draining yields the queued events in FIFO order and then completes once the ring is empty.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task DrainAsync_Yields_FIFO_Then_Completes_When_Empty()
{
@@ -76,6 +80,7 @@ public class RingBufferFallbackTests
}
}
+ /// Verifies that enqueuing a single event into a ring with spare capacity succeeds and returns true.
[Fact]
public void TryEnqueue_AllSucceeds_ReturnsTrue()
{
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/SiteAuditBacklogReporterCadenceTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/SiteAuditBacklogReporterCadenceTests.cs
index daa0826d..1cff7d15 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/SiteAuditBacklogReporterCadenceTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/SiteAuditBacklogReporterCadenceTests.cs
@@ -11,7 +11,7 @@ using ZB.MOM.WW.ScadaBridge.HealthMonitoring;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Site;
///
-/// Deferred-work #21: the backlog reporter's poll cadence is configurable via
+/// The backlog reporter's poll cadence is configurable via
/// instead of the
/// old hard-coded 30 s constant.
///
@@ -26,6 +26,8 @@ public class SiteAuditBacklogReporterCadenceTests
explicitInterval,
options);
+ /// Verifies that stopping the reporter while a probe is in flight lets the loop complete cleanly.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task StopAsync_WhileProbeInFlight_LoopCompletesCleanly()
{
@@ -62,10 +64,12 @@ public class SiteAuditBacklogReporterCadenceTests
throw new UnreachableException("the delay above always throws on cancellation");
}
+ /// Verifies that stopping the reporter after it has been disposed does not throw.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task StopAsync_AfterDispose_DoesNotThrow()
{
- // Regression (Gitea #15 follow-up): Dispose tears down the CTS StopAsync
+ // Regression (follow-up): Dispose tears down the CTS StopAsync
// cancels, and the host does not guarantee StopAsync is driven before the DI
// container is disposed. Cancel() on a disposed CTS throws, and letting that
// escape an IHostedService aborts the host's whole shutdown sequence.
@@ -79,6 +83,7 @@ public class SiteAuditBacklogReporterCadenceTests
reporter.Dispose();
}
+ /// Verifies that the poll cadence comes from the options when one is configured.
[Fact]
public void Cadence_ComesFromOptions_WhenConfigured()
{
@@ -89,6 +94,7 @@ public class SiteAuditBacklogReporterCadenceTests
Assert.Equal(TimeSpan.FromSeconds(12), reporter.RefreshInterval);
}
+ /// Verifies that a non-positive configured interval falls back to the default cadence.
[Fact]
public void Cadence_FallsBackToDefault_WhenOptionsNonPositive()
{
@@ -99,6 +105,7 @@ public class SiteAuditBacklogReporterCadenceTests
Assert.Equal(SiteAuditBacklogReporter.DefaultRefreshInterval, reporter.RefreshInterval);
}
+ /// Verifies that the default cadence is used when no options are supplied.
[Fact]
public void Cadence_FallsBackToDefault_WhenNoOptions()
{
@@ -107,6 +114,7 @@ public class SiteAuditBacklogReporterCadenceTests
Assert.Equal(SiteAuditBacklogReporter.DefaultRefreshInterval, reporter.RefreshInterval);
}
+ /// Verifies that an explicitly supplied interval takes precedence over the options value.
[Fact]
public void ExplicitInterval_WinsOverOptions()
{
@@ -119,6 +127,7 @@ public class SiteAuditBacklogReporterCadenceTests
// ----- Stale-Pending signal (review F5) ----- //
+ /// Verifies that a stale pending backlog is logged as a warning and that repeat warnings are then rate-limited.
[Fact]
public void StalePendingBacklog_IsWarned_ThenRateLimited()
{
@@ -143,6 +152,7 @@ public class SiteAuditBacklogReporterCadenceTests
Assert.Contains("pending", warning.Message, StringComparison.OrdinalIgnoreCase);
}
+ /// Verifies that a fresh or empty pending backlog does not log a warning.
[Fact]
public void FreshOrEmptyPendingBacklog_IsNotWarned()
{
@@ -163,12 +173,27 @@ public class SiteAuditBacklogReporterCadenceTests
/// Captures log entries so the stale-pending signal can be asserted.
private sealed class CapturingLogger : ILogger
{
+ /// Gets the log entries captured so far.
public List<(LogLevel Level, Exception? Exception, string Message)> Entries { get; } = new();
+ /// Returns no scope; the fake logger does not track scopes.
+ /// The type of the scope state.
+ /// The scope state, ignored.
+ /// Always null or a no-op scope.
public IDisposable? BeginScope(TState state) where TState : notnull => null;
+ /// Reports every log level as enabled.
+ /// The log level being queried.
+ /// Always true.
public bool IsEnabled(LogLevel logLevel) => true;
+ /// Records the log entry for later assertions.
+ /// The type of the log state.
+ /// The severity of the entry.
+ /// The event identifier.
+ /// The log state.
+ /// The associated exception, if any.
+ /// Formats the state and exception into the message.
public void Log(
LogLevel logLevel,
EventId eventId,
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/SiteAuditRetentionOptionsTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/SiteAuditRetentionOptionsTests.cs
index da9d66da..f626c02f 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/SiteAuditRetentionOptionsTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/SiteAuditRetentionOptionsTests.cs
@@ -3,13 +3,16 @@ using ZB.MOM.WW.ScadaBridge.AuditLog.Site;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Site;
///
-/// Tests for clamping (PLAN-04 Task 2).
+/// Tests for clamping.
/// The resolved knobs guard the daily site retention job against misconfiguration:
/// retention days are clamped to [1, 90]; the purge interval is clamped away from
/// TimeSpan.Zero (an Akka/Timer spin footgun).
///
public class SiteAuditRetentionOptionsTests
{
+ /// Verifies that resolved retention days, clamps.
+ /// Test case input.
+ /// Test case input.
[Theory]
[InlineData(0, 1)] // clamp floor
[InlineData(7, 7)] // default passthrough
@@ -17,6 +20,7 @@ public class SiteAuditRetentionOptionsTests
public void ResolvedRetentionDays_Clamps(int configured, int expected)
=> Assert.Equal(expected, new SiteAuditRetentionOptions { RetentionDays = configured }.ResolvedRetentionDays);
+ /// Verifies that defaults, are, seven days, daily purge, five minute initial delay.
[Fact]
public void Defaults_Are_SevenDays_DailyPurge_FiveMinuteInitialDelay()
{
@@ -26,6 +30,7 @@ public class SiteAuditRetentionOptionsTests
Assert.Equal(TimeSpan.FromMinutes(5), o.InitialDelay);
}
+ /// Verifies that resolved purge interval, clamps zero to one minute.
[Fact]
public void ResolvedPurgeInterval_ClampsZeroToOneMinute()
=> Assert.Equal(TimeSpan.FromMinutes(1),
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/SiteAuditRetentionOptionsValidatorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/SiteAuditRetentionOptionsValidatorTests.cs
index 42c8d777..857bd533 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/SiteAuditRetentionOptionsValidatorTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/SiteAuditRetentionOptionsValidatorTests.cs
@@ -12,6 +12,7 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Site;
///
public class SiteAuditRetentionOptionsValidatorTests
{
+ /// Verifies that the default options pass validation.
[Fact]
public void DefaultOptions_AreValid()
{
@@ -19,6 +20,7 @@ public class SiteAuditRetentionOptionsValidatorTests
Assert.True(validator.Validate(null, new SiteAuditRetentionOptions()).Succeeded);
}
+ /// Verifies that a retention period of zero days fails validation.
[Fact]
public void ZeroRetentionDays_Fails()
{
@@ -29,6 +31,7 @@ public class SiteAuditRetentionOptionsValidatorTests
f => f.Contains(nameof(SiteAuditRetentionOptions.RetentionDays), StringComparison.Ordinal));
}
+ /// Verifies that a purge interval of zero fails validation.
[Fact]
public void ZeroPurgeInterval_Fails()
{
@@ -40,6 +43,7 @@ public class SiteAuditRetentionOptionsValidatorTests
f => f.Contains(nameof(SiteAuditRetentionOptions.PurgeInterval), StringComparison.Ordinal));
}
+ /// Verifies that a negative initial delay fails validation.
[Fact]
public void NegativeInitialDelay_Fails()
{
diff --git a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/SiteAuditRetentionServiceTests.cs b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/SiteAuditRetentionServiceTests.cs
index 817fda22..a66a4ca5 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/SiteAuditRetentionServiceTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.AuditLog.Tests/Site/SiteAuditRetentionServiceTests.cs
@@ -10,7 +10,7 @@ using AuditEvent = ZB.MOM.WW.Audit.AuditEvent;
namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Site;
///
-/// Tests for (PLAN-04 Task 3). The hosted
+/// Tests for The hosted
/// service runs the site SQLite retention purge on a timer: first tick after
/// , then every
/// , passing a cutoff of
@@ -19,10 +19,12 @@ namespace ZB.MOM.WW.ScadaBridge.AuditLog.Tests.Site;
///
public class SiteAuditRetentionServiceTests
{
+ /// Verifies that stopping the service after it has been disposed does not throw.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task StopAsync_AfterDispose_DoesNotThrow()
{
- // Regression (Gitea #15 follow-up): Dispose tears down the CTS StopAsync
+ // Regression: Dispose tears down the CTS StopAsync
// cancels, and the host does not guarantee StopAsync is driven before the DI
// container is disposed. Cancel() on a disposed CTS throws, and letting that
// escape an IHostedService aborts the host's whole shutdown sequence.
@@ -43,6 +45,8 @@ public class SiteAuditRetentionServiceTests
svc.Dispose();
}
+ /// Verifies that a tick purges events older than the retention cutoff.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Tick_Purges_With_RetentionCutoff()
{
@@ -66,6 +70,8 @@ public class SiteAuditRetentionServiceTests
DateTime.UtcNow.AddDays(-7).AddMinutes(1));
}
+ /// Verifies that a failing tick is swallowed and later ticks still run.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task Tick_SwallowsExceptions_AndKeepsTicking()
{
@@ -87,6 +93,8 @@ public class SiteAuditRetentionServiceTests
Assert.True(queue.PurgeCalls.Count >= 2);
}
+ /// Verifies that stopping the service during an in-flight purge completes cleanly without surfacing a cancellation.
+ /// A task that represents the asynchronous operation.
[Fact]
public async Task StopAsync_MidPurge_CompletesCleanly_WithoutSurfacingCancellation()
{
@@ -127,6 +135,13 @@ public class SiteAuditRetentionServiceTests
/// Stub queue that records purge cutoffs and can throw on the first tick.
private sealed class RecordingSiteAuditQueue : ISiteAuditQueue
{
+ /// Not supported by this stub.
+ /// The execution ids to read.
+ /// The lower time bound.
+ /// The maximum number of rows.
+ /// A token to cancel the operation.
+ /// The keyset cursor.
+ /// Never returns; always throws .
public Task> ReadByExecutionIdsAsync(
IReadOnlyCollection executionIds, DateTime sinceUtc, int limit, CancellationToken ct = default, string? afterEventId = null)
=> throw new NotSupportedException("not exercised by the retention service");
@@ -134,13 +149,17 @@ public class SiteAuditRetentionServiceTests
private readonly ConcurrentQueue _purgeCalls = new();
private int _calls;
+ /// Gets the cutoffs passed to each purge call, in call order.
public IReadOnlyCollection PurgeCalls => _purgeCalls;
+ /// Gets a value indicating whether the first purge call throws a simulated transient failure.
public bool ThrowOnFirstCall { get; init; }
/// When set, PurgeExpiredAsync records the call then blocks until the token
/// cancels — simulating a shutdown that lands while a purge is in flight.
public bool BlockUntilCancelled { get; init; }
+ // Test stub: Records the cutoff and optionally throws or blocks, as configured.
+ ///
public async Task PurgeExpiredAsync(DateTime olderThanUtc, CancellationToken ct = default)
{
var n = Interlocked.Increment(ref _calls);
@@ -158,19 +177,41 @@ public class SiteAuditRetentionServiceTests
return 0;
}
+ /// Not supported by this stub.
+ /// The maximum number of rows.
+ /// A token to cancel the operation.
+ /// Never returns; always throws .
public Task> ReadPendingAsync(int limit, CancellationToken ct = default)
=> throw new NotSupportedException();
+ /// Not supported by this stub.
+ /// The maximum number of rows.
+ /// A token to cancel the operation.
+ /// Never returns; always throws .
public Task> ReadPendingCachedTelemetryAsync(int limit, CancellationToken ct = default)
=> throw new NotSupportedException();
+ // Test stub: Not supported by this stub.
+ ///
public Task MarkForwardedAsync(IReadOnlyList eventIds, CancellationToken ct = default)
=> throw new NotSupportedException();
+ /// Not supported by this stub.
+ /// The lower time bound.
+ /// The maximum batch size.
+ /// The keyset cursor.
+ /// A token to cancel the operation.
+ /// Never returns; always throws .
public Task> ReadPendingSinceAsync(
DateTime sinceUtc, int batchSize, string? afterId = null, CancellationToken ct = default)
=> throw new NotSupportedException();
+ // Test stub: Not supported by this stub.
+ ///
public Task MarkReconciledAsync(IReadOnlyList eventIds, CancellationToken ct = default)
=> throw new NotSupportedException();
+ // Test stub: Not supported by this stub.
+ ///
public Task MarkReconciledUpToAsync(DateTime sinceUtc, string? afterId, CancellationToken ct = default)
=> throw new NotSupportedException();
+ // Test stub: Not supported by this stub.
+ ///