docs(tests): fix CommentChecker findings across tests/ — 13,392 → 0 (repo-wide scan clean)

Comment/XML-doc changes only:
- add summaries to ~10k test methods, fixtures, fakes and helpers
  (+ <param> on theories, <returns> on async Task methods,
  <inheritdoc/> on interface implementations the checker can resolve)
- strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N,
  #NN, Component-0NN) from comments, keeping the explanatory text;
  reword false positives in comments only
- real docs on fakes implementing BCL/library interfaces
  (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime)
- reword the last 5 src/ false positives (CWE-1236, failures-1,
  year-0001, M365)

Only non-comment edits: four one-line nested classes split across lines
so members can carry docs. Build 0 errors / 0 warnings.
This commit is contained in:
Joseph Doherty
2026-10-07 15:05:49 -04:00
parent 0ffd4bdbea
commit 513fa1aa16
998 changed files with 17896 additions and 2395 deletions
@@ -13,7 +13,7 @@ using ZB.MOM.WW.ScadaBridge.NotificationOutbox.Tests.TestSupport;
namespace ZB.MOM.WW.ScadaBridge.NotificationOutbox.Tests;
/// <summary>
/// Task 14: Tests for the <see cref="NotificationOutboxActor"/> dispatcher loop — the
/// Tests for the <see cref="NotificationOutboxActor"/> dispatcher loop — the
/// periodic sweep that claims due notifications via
/// <see cref="INotificationOutboxRepository.GetDueAsync"/>, delivers each through the
/// matching <see cref="INotificationDeliveryAdapter"/>, and applies the resulting status
@@ -52,6 +52,10 @@ public class NotificationOutboxActorDispatchTests : TestKit
private readonly Func<DeliveryOutcome> _outcome;
private readonly TimeSpan _delay;
/// <summary>Initializes the adapter with the outcome factory, an optional delivery delay and the notification type it serves.</summary>
/// <param name="outcome">Produces the outcome returned for each delivery.</param>
/// <param name="delay">Optional delay applied to each delivery.</param>
/// <param name="type">The notification type this adapter serves.</param>
public StubAdapter(Func<DeliveryOutcome> outcome, TimeSpan? delay = null,
NotificationType type = NotificationType.Email)
{
@@ -62,8 +66,10 @@ public class NotificationOutboxActorDispatchTests : TestKit
public int CallCount;
/// <inheritdoc />
public NotificationType Type { get; }
/// <inheritdoc />
public async Task<DeliveryOutcome> DeliverAsync(
Notification notification, CancellationToken cancellationToken = default)
{
@@ -110,6 +116,7 @@ public class NotificationOutboxActorDispatchTests : TestKit
.Returns(new[] { config });
}
/// <summary>Verifies that a dispatch tick claims due notifications and invokes the adapter.</summary>
[Fact]
public void DispatchTick_ClaimsDueNotifications_AndInvokesAdapter()
{
@@ -130,6 +137,7 @@ public class NotificationOutboxActorDispatchTests : TestKit
});
}
/// <summary>Verifies that an SMS notification is routed to the SMS adapter rather than the email adapter.</summary>
[Fact]
public void Dispatch_SmsTypedNotification_RoutesToSmsAdapter_NotEmailAdapter()
{
@@ -159,6 +167,7 @@ public class NotificationOutboxActorDispatchTests : TestKit
});
}
/// <summary>Verifies that a successful delivery marks the notification delivered with the resolved targets.</summary>
[Fact]
public void Success_MarksNotificationDelivered_WithResolvedTargets()
{
@@ -184,6 +193,7 @@ public class NotificationOutboxActorDispatchTests : TestKit
});
}
/// <summary>Verifies that a transient failure below the maximum retries marks the notification as retrying and schedules the next attempt.</summary>
[Fact]
public void TransientFailure_BelowMaxRetries_MarksRetrying_AndSchedulesNextAttempt()
{
@@ -209,11 +219,12 @@ public class NotificationOutboxActorDispatchTests : TestKit
});
}
/// <summary>Verifies that a transient failure that reaches the maximum retries parks the notification.</summary>
[Fact]
public void TransientFailure_ReachingMaxRetries_MarksParked()
{
SetupSmtpRetryPolicy(maxRetries: 3, retryDelay: TimeSpan.FromMinutes(1));
// RetryCount starts at max-1; the failed attempt increments it to max.
// RetryCount starts one below the maximum; the failed attempt increments it to the maximum.
var notification = MakeNotification(retryCount: 2);
_outboxRepository.GetDueAsync(Arg.Any<DateTimeOffset>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
.Returns(new[] { notification });
@@ -232,6 +243,7 @@ public class NotificationOutboxActorDispatchTests : TestKit
});
}
/// <summary>Verifies that a permanent failure parks the notification and records the last error.</summary>
[Fact]
public void PermanentFailure_MarksParked_WithLastError()
{
@@ -255,6 +267,7 @@ public class NotificationOutboxActorDispatchTests : TestKit
});
}
/// <summary>Verifies that a notification with no adapter for its type is parked with an explanatory error.</summary>
[Fact]
public void NoAdapterForType_MarksParked_WithExplanatoryError()
{
@@ -279,6 +292,7 @@ public class NotificationOutboxActorDispatchTests : TestKit
});
}
/// <summary>Verifies that a faulted dispatch pass clears the in-flight guard so the next tick still runs.</summary>
[Fact]
public void FaultedDispatchPass_ClearsInFlightGuard_SoNextTickStillRuns()
{
@@ -329,10 +343,11 @@ public class NotificationOutboxActorDispatchTests : TestKit
duration: TimeSpan.FromSeconds(10));
}
/// <summary>Verifies that a transient failure with a maximum retry count of zero retries using the fallback instead of parking immediately.</summary>
[Fact]
public void TransientFailure_WithZeroMaxRetries_RetriesUsingFallback_DoesNotParkImmediately()
{
// NO-002: SmtpConfiguration.MaxRetries=0 used to satisfy 1 >= 0 on the very first
// SmtpConfiguration.MaxRetries=0 used to satisfy 1 >= 0 on the very first
// transient failure and park the row without a single retry. ResolveRetryPolicyAsync
// now clamps non-positive MaxRetries to the FallbackMaxRetries (10) so transient
// failures actually retry before parking.
@@ -357,10 +372,11 @@ public class NotificationOutboxActorDispatchTests : TestKit
});
}
/// <summary>Verifies that a transient failure with a negative maximum retry count retries using the fallback instead of parking immediately.</summary>
[Fact]
public void TransientFailure_WithNegativeMaxRetries_RetriesUsingFallback_DoesNotParkImmediately()
{
// NO-002: a negative MaxRetries reaches ResolveRetryPolicyAsync just as -1 — same
// A negative MaxRetries reaches ResolveRetryPolicyAsync just as -1 — same
// park-immediately bug. Clamp to FallbackMaxRetries.
SetupSmtpRetryPolicy(maxRetries: -1, retryDelay: TimeSpan.FromMinutes(1));
var notification = MakeNotification(retryCount: 0);
@@ -383,10 +399,11 @@ public class NotificationOutboxActorDispatchTests : TestKit
});
}
/// <summary>Verifies that a non-positive retry delay is replaced by the fallback delay rather than zero.</summary>
[Fact]
public void TransientFailure_WithNonPositiveRetryDelay_UsesFallbackDelay_NotZero()
{
// NO-002: a non-positive RetryDelay would burn-loop the dispatcher because
// A non-positive RetryDelay would burn-loop the dispatcher because
// NextAttemptAt would equal now. Clamp to FallbackRetryDelay (1 min) so the
// schedule actually advances.
SetupSmtpRetryPolicy(maxRetries: 5, retryDelay: TimeSpan.Zero);
@@ -410,10 +427,11 @@ public class NotificationOutboxActorDispatchTests : TestKit
});
}
/// <summary>Verifies that stopping the actor cancels an in-flight delivery and leaves the row non-terminal.</summary>
[Fact]
public void PostStop_CancelsInFlightDelivery_LeavesRowNonTerminal()
{
// NO-003: the dispatcher used to drop the CancellationToken on its way into
// The dispatcher used to drop the CancellationToken on its way into
// the channel adapter, so a coordinated shutdown had to wait the full SMTP
// connect/auth/send timeout per in-flight notification before the sweep
// finished. The actor now passes a lifecycle-scoped token; cancelling it on
@@ -456,7 +474,7 @@ public class NotificationOutboxActorDispatchTests : TestKit
// ── Register row 45: the delivery adapter is resolved PER DELIVERY ────────
//
// Supersedes NotificationOutbox-006 ("resolve the adapter dictionary once for the
// Supersedes an earlier optimisation ("resolve the adapter dictionary once for the
// actor's lifetime"). That optimisation cached scoped adapter instances — which hold
// a scoped INotificationRepository over an EF DbContext — in an actor-lifetime DI
// scope, so MaxParallelDeliveries concurrent deliveries all ran on ONE DbContext and
@@ -478,11 +496,14 @@ public class NotificationOutboxActorDispatchTests : TestKit
private int _arrived;
private int _overlaps;
/// <summary>Initializes the ledger with the number of deliveries expected to reach the rendezvous.</summary>
/// <param name="expectedArrivals">The number of arrivals that completes the rendezvous.</param>
public DeliveryConcurrencyLedger(int expectedArrivals) => _expectedArrivals = expectedArrivals;
/// <summary>Number of second-operation-on-one-context collisions observed.</summary>
public int Overlaps => Volatile.Read(ref _overlaps);
/// <summary>Records one overlapping use of the same simulated context.</summary>
public void RecordOverlap() => Interlocked.Increment(ref _overlaps);
/// <summary>
@@ -491,6 +512,8 @@ public class NotificationOutboxActorDispatchTests : TestKit
/// timeout matters for the shared-context (pre-fix) run, where the colliding
/// delivery throws instead of arriving and the rendezvous can never complete.
/// </summary>
/// <param name="cancellationToken">Token that cancels the wait.</param>
/// <returns>A task that completes once every delivery has arrived or the safety timeout elapses.</returns>
public async Task ArriveAndWaitAsync(CancellationToken cancellationToken)
{
if (Interlocked.Increment(ref _arrived) >= _expectedArrivals)
@@ -517,8 +540,13 @@ public class NotificationOutboxActorDispatchTests : TestKit
private readonly DeliveryConcurrencyLedger _ledger;
private int _inFlight;
/// <summary>Initializes the probe with the ledger it reports to.</summary>
/// <param name="ledger">The ledger that records overlaps and coordinates the rendezvous.</param>
public SimulatedDbContextProbe(DeliveryConcurrencyLedger ledger) => _ledger = ledger;
/// <summary>Enters the simulated context, failing if another operation is already in flight, and waits at the rendezvous.</summary>
/// <param name="cancellationToken">Token that cancels the wait.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public async Task EnterAsync(CancellationToken cancellationToken)
{
if (Interlocked.Increment(ref _inFlight) > 1)
@@ -550,10 +578,14 @@ public class NotificationOutboxActorDispatchTests : TestKit
{
private readonly INotificationRepository _repository;
/// <summary>Initializes the adapter with the scoped repository it works through.</summary>
/// <param name="repository">The scoped notification repository.</param>
public RepositoryBackedAdapter(INotificationRepository repository) => _repository = repository;
/// <inheritdoc />
public NotificationType Type => NotificationType.Email;
/// <inheritdoc />
public async Task<DeliveryOutcome> DeliverAsync(
Notification notification, CancellationToken cancellationToken = default)
{
@@ -579,6 +611,7 @@ public class NotificationOutboxActorDispatchTests : TestKit
}
}
/// <summary>Verifies that parallel deliveries each get their own adapter and DbContext.</summary>
[Fact]
public void ParallelDeliveries_EachGetTheirOwnAdapterAndDbContext()
{
@@ -649,6 +682,7 @@ public class NotificationOutboxActorDispatchTests : TestKit
Assert.Equal(0, ledger.Overlaps);
}
/// <summary>Verifies that the adapter is resolved per delivery rather than once per actor lifetime.</summary>
[Fact]
public void Dispatch_ResolvesAdapterPerDelivery_NotOncePerActorLifetime()
{
@@ -703,11 +737,15 @@ public class NotificationOutboxActorDispatchTests : TestKit
{
private readonly System.Collections.Concurrent.ConcurrentBag<INotificationDeliveryAdapter> _seen;
/// <summary>Initializes the adapter with the bag that records each adapter instance used.</summary>
/// <param name="seen">The bag that collects the adapter instances that performed deliveries.</param>
public RecordingStubAdapter(
System.Collections.Concurrent.ConcurrentBag<INotificationDeliveryAdapter> seen) => _seen = seen;
/// <inheritdoc />
public NotificationType Type => NotificationType.Email;
/// <inheritdoc />
public Task<DeliveryOutcome> DeliverAsync(
Notification notification, CancellationToken cancellationToken = default)
{
@@ -716,6 +754,7 @@ public class NotificationOutboxActorDispatchTests : TestKit
}
}
/// <summary>Verifies that overlapping ticks while a dispatch is in flight do not claim notifications concurrently.</summary>
[Fact]
public void OverlappingTicks_WhileDispatchInFlight_DoNotClaimConcurrently()
{
@@ -739,14 +778,14 @@ public class NotificationOutboxActorDispatchTests : TestKit
duration: TimeSpan.FromSeconds(2));
}
// ── Task 25: bounded parallel delivery ───────────────────────────────────
// ── Bounded parallel delivery ───────────────────────────────────
/// <summary>
/// Adapter that records the concurrent-invocation high-water mark. Each
/// <see cref="DeliverAsync"/> increments a live counter, notes the peak, then blocks
/// on a shared gate until <paramref name="releaseWhen"/> deliveries are concurrently
/// in flight — proving real overlap. A safety timeout releases the gate so a
/// parallelism-1 run (which can never reach <c>releaseWhen</c> concurrently) still
/// run with a parallelism of one (which can never reach <c>releaseWhen</c> concurrently) still
/// drains instead of deadlocking.
/// </summary>
private sealed class ConcurrencyHighWaterAdapter : INotificationDeliveryAdapter
@@ -758,12 +797,17 @@ public class NotificationOutboxActorDispatchTests : TestKit
private int _highWater;
private int _entered;
/// <summary>Initializes the adapter with the concurrency level that releases the gate.</summary>
/// <param name="releaseWhen">The number of concurrent deliveries that releases the gate.</param>
public ConcurrencyHighWaterAdapter(int releaseWhen) => _releaseWhen = releaseWhen;
/// <inheritdoc />
public NotificationType Type => NotificationType.Email;
/// <summary>Gets the highest number of deliveries observed in flight at once.</summary>
public int HighWater => Volatile.Read(ref _highWater);
/// <inheritdoc />
public async Task<DeliveryOutcome> DeliverAsync(
Notification notification, CancellationToken cancellationToken = default)
{
@@ -800,6 +844,7 @@ public class NotificationOutboxActorDispatchTests : TestKit
: Array.Empty<Notification>());
}
/// <summary>Verifies that with a maximum parallelism of four, deliveries run concurrently and all reach a terminal state.</summary>
[Fact]
public void Dispatch_WithMaxParallelFour_DeliversConcurrently_AndAllReachTerminal()
{
@@ -830,6 +875,7 @@ public class NotificationOutboxActorDispatchTests : TestKit
duration: TimeSpan.FromSeconds(10));
}
/// <summary>Verifies that with a maximum parallelism of one, deliveries run sequentially and all reach a terminal state.</summary>
[Fact]
public void Dispatch_WithMaxParallelOne_DeliversSequentially_AndAllReachTerminal()
{
@@ -856,7 +902,7 @@ public class NotificationOutboxActorDispatchTests : TestKit
Assert.Equal(1, adapter.HighWater);
}
// ── Task 18: per-type retry policy (SMS honors SmsConfiguration) ──────────
// ── Per-type retry policy (SMS honors SmsConfiguration) ──────────
private void SetupSmsRetryPolicy(int maxRetries, TimeSpan retryDelay)
{
@@ -869,10 +915,11 @@ public class NotificationOutboxActorDispatchTests : TestKit
.Returns(config);
}
/// <summary>Verifies that a transient failure on an SMS notification uses the SMS retry policy.</summary>
[Fact]
public void SmsNotification_TransientFailure_UsesSmsRetryPolicy()
{
// Task 18: an SMS-typed notification resolves its retry policy from the SMS
// An SMS-typed notification resolves its retry policy from the SMS
// configuration, NOT the shared SMTP policy. The SMTP policy (MaxRetries=10)
// would keep this row Retrying; the SMS policy (MaxRetries=1) parks it after
// this attempt (RetryCount 1 -> 2 >= 1). A pre-fix dispatcher (SMTP-for-all)
@@ -899,10 +946,11 @@ public class NotificationOutboxActorDispatchTests : TestKit
});
}
/// <summary>Verifies that an email notification still uses the SMTP retry policy.</summary>
[Fact]
public void EmailNotification_StillUsesSmtpPolicy()
{
// Task 18: an Email row continues to resolve against the SMTP policy even when a
// An Email row continues to resolve against the SMTP policy even when a
// (tighter) SMS policy is present. SMTP MaxRetries=10 keeps it Retrying
// (RetryCount 1 -> 2 < 10) and schedules the next attempt with the SMTP delay.
SetupSmtpRetryPolicy(maxRetries: 10, retryDelay: TimeSpan.FromMinutes(3));