Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests/Repositories/BundleUnlockAttemptRepositoryTests.cs
T
Joseph Doherty 513fa1aa16 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.
2026-10-07 15:05:49 -04:00

101 lines
4.5 KiB
C#

using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Repositories;
namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests.Repositories;
/// <summary>
/// Register row 52 coverage for <see cref="BundleUnlockAttemptRepository"/> over the
/// in-memory SQLite harness — the fleet-wide bundle-unlock throttle window's persistence
/// layer. Exercises the windowed count (including the bucket-isolation and window-boundary
/// cases the throttle depends on) and the bounded prune.
/// </summary>
public class BundleUnlockAttemptRepositoryTests
{
// Fixed UTC base so the window assertions are deterministic.
private static readonly DateTime Base = new(2026, 6, 1, 12, 0, 0, DateTimeKind.Utc);
private static ScadaBridgeDbContext NewContext() => SqliteTestHelper.CreateInMemoryContext();
/// <summary>Verifies that counting attempts includes only the given bucket within the window.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task CountAttemptsSinceAsync_CountsOnlyTheGivenBucketInsideTheWindow()
{
await using var ctx = NewContext();
var repo = new BundleUnlockAttemptRepository(ctx);
// Two inside the trailing hour, one aged out, one from a different client.
await repo.RecordAttemptAsync("10.0.0.1", Base.AddMinutes(-10));
await repo.RecordAttemptAsync("10.0.0.1", Base.AddMinutes(-50));
await repo.RecordAttemptAsync("10.0.0.1", Base.AddMinutes(-90));
await repo.RecordAttemptAsync("10.0.0.2", Base.AddMinutes(-5));
var count = await repo.CountAttemptsSinceAsync("10.0.0.1", Base.AddHours(-1));
Assert.Equal(2, count);
}
/// <summary>Verifies that counting attempts is inclusive at the window floor.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task CountAttemptsSinceAsync_IsInclusiveAtTheWindowFloor()
{
// The floor is inclusive, matching the in-memory limiter's prune predicate
// (entries at exactly the cutoff still count). Pinned because an off-by-one here
// would silently hand every client one extra attempt per window.
await using var ctx = NewContext();
var repo = new BundleUnlockAttemptRepository(ctx);
await repo.RecordAttemptAsync("10.0.0.1", Base.AddHours(-1));
Assert.Equal(1, await repo.CountAttemptsSinceAsync("10.0.0.1", Base.AddHours(-1)));
}
/// <summary>Verifies that counting attempts for an unknown bucket returns zero.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task CountAttemptsSinceAsync_UnknownBucket_ReturnsZero()
{
await using var ctx = NewContext();
var repo = new BundleUnlockAttemptRepository(ctx);
Assert.Equal(0, await repo.CountAttemptsSinceAsync("<unattributed>", Base.AddHours(-1)));
}
/// <summary>Verifies that pruning deletes only aged rows and respects the batch bound.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task PruneAttemptsOlderThanAsync_DeletesOnlyAgedRows_AndIsBounded()
{
await using var ctx = NewContext();
var repo = new BundleUnlockAttemptRepository(ctx);
for (var i = 0; i < 5; i++)
{
await repo.RecordAttemptAsync("10.0.0.1", Base.AddHours(-5).AddMinutes(i));
}
await repo.RecordAttemptAsync("10.0.0.1", Base.AddMinutes(-1));
// Bounded: only 3 of the 5 eligible rows go in this pass.
var deleted = await repo.PruneAttemptsOlderThanAsync(Base.AddHours(-2), maxRows: 3);
Assert.Equal(3, deleted);
// The recent row is untouched, and a second pass clears the remainder.
Assert.Equal(1, await repo.CountAttemptsSinceAsync("10.0.0.1", Base.AddHours(-1)));
Assert.Equal(2, await repo.PruneAttemptsOlderThanAsync(Base.AddHours(-2), maxRows: 3));
Assert.Equal(0, await repo.PruneAttemptsOlderThanAsync(Base.AddHours(-2), maxRows: 3));
}
/// <summary>Verifies that pruning rejects a non-positive batch size.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task PruneAttemptsOlderThanAsync_RejectsANonPositiveBatchSize()
{
await using var ctx = NewContext();
var repo = new BundleUnlockAttemptRepository(ctx);
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(
() => repo.PruneAttemptsOlderThanAsync(Base, maxRows: 0));
}
}