513fa1aa16
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.
101 lines
4.5 KiB
C#
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));
|
|
}
|
|
}
|