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:
+140
-6
@@ -12,11 +12,11 @@ using ZB.MOM.WW.Secrets.Abstractions;
|
||||
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Secrets;
|
||||
|
||||
/// <summary>
|
||||
/// Task 9, site-secrets artifact deploy: <see cref="DeployedSecretsMirror"/> — per-node apply
|
||||
/// Site-secrets artifact deploy: <see cref="DeployedSecretsMirror"/> — per-node apply
|
||||
/// + decrypt-verify of <c>deployed_secrets</c> into this node's own secret store.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Fix round 1 (review <c>task-9-review.md</c>): <see cref="FakeSecretStore"/> now honours the
|
||||
/// <see cref="FakeSecretStore"/> honours the
|
||||
/// real <c>SqliteSecretStore</c> semantics — <c>ApplyReplicatedAsync</c> is strict
|
||||
/// last-writer-wins via <see cref="SecretLastWriterWins.IsNewer"/>, and <c>DeleteAsync</c>
|
||||
/// tombstones rather than hard-deleting — so tests exercise the same conflict shapes the real
|
||||
@@ -26,11 +26,13 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
{
|
||||
private readonly TestLocalDb _localDb;
|
||||
|
||||
/// <summary>Initializes the test class with a temporary LocalDb database.</summary>
|
||||
public DeployedSecretsMirrorTests()
|
||||
{
|
||||
_localDb = TestLocalDb.CreateTemp("deployed-secrets-mirror");
|
||||
}
|
||||
|
||||
/// <summary>Releases the resources used by this instance.</summary>
|
||||
public void Dispose()
|
||||
{
|
||||
var path = _localDb.Path;
|
||||
@@ -40,6 +42,8 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
|
||||
// ── Apply / remove / out-of-band ──
|
||||
|
||||
/// <summary>Verifies that reconcile applies and verifies deployed secrets and deletes only the names it mirrored.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Reconcile_Applies_Verifies_AndOnlyDeletesWhatItMirrored()
|
||||
{
|
||||
@@ -69,6 +73,8 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
Assert.True(store.IsTombstoned("a"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a newer out-of-band secret is neither overwritten nor ledgered and survives a later removal.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Reconcile_OutOfBandNewerSecret_NotOverwritten_NotLedgered_AndSurvivesLaterRemoval()
|
||||
{
|
||||
@@ -101,6 +107,8 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
Assert.Equal(operatorCiphertext, store.Rows["a"].Ciphertext);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a removed then re-added secret recovers from its tombstone, reapplies and verifies.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Reconcile_RemoveThenReAdd_TombstoneIsRecovered_ReappliesAndVerifies()
|
||||
{
|
||||
@@ -142,6 +150,8 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
Assert.Equal(1, mirrored.Single(m => m.Name == "a").Revision);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a clock-skewed recovered row still converges on the next central revision.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Reconcile_ClockSkewedRecoveredRow_StillConvergesOnNextCentralRevision()
|
||||
{
|
||||
@@ -187,6 +197,8 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
Assert.Equal(2, mirrored.Single(m => m.Name == "a").Revision);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reconcile reapplies a secret only when its revision changes.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Reconcile_ReAppliesOnlyWhenRevisionChanges()
|
||||
{
|
||||
@@ -208,6 +220,8 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
|
||||
// ── Same-revision drift (final review I1) ──
|
||||
|
||||
/// <summary>Verifies that an owned name deleted locally is healed and verified rather than misreported as a KEK mismatch.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Reconcile_OwnedNameDeletedLocally_IsHealedAndVerified_NotMisreportedAsKekMismatch()
|
||||
{
|
||||
@@ -238,6 +252,8 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
Assert.Equal(1, (await storage.GetMirroredSecretsAsync()).Single(m => m.Name == "a").Revision);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an owned name edited locally is reverted to the deployed ciphertext.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Reconcile_OwnedNameEditedLocally_IsRevertedToTheDeployedCiphertext()
|
||||
{
|
||||
@@ -263,6 +279,8 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
Assert.Contains("a", invalidator.Invalidated);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that owned-name drift is logged once per drift rather than on every pass.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Reconcile_OwnedNameDrift_LogsOncePerDriftNotEveryPass()
|
||||
{
|
||||
@@ -281,8 +299,10 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
Assert.DoesNotContain(logger.Entries, e => e.Level == LogLevel.Error);
|
||||
}
|
||||
|
||||
// ── Unreadable row_json (final review M4) ──
|
||||
// ── Unreadable row_json ──
|
||||
|
||||
/// <summary>Verifies that an unparseable row JSON fails only that name while other names still apply.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Reconcile_UnparseableRowJson_IsAFailedEntryForThatName_OthersStillApply()
|
||||
{
|
||||
@@ -301,6 +321,8 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
Assert.False(store.Contains("bad"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an unparseable removed row still removes a mirrored name.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Reconcile_UnparseableRemovedRow_StillRemovesAMirroredName()
|
||||
{
|
||||
@@ -326,6 +348,8 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
Assert.Equal(1, await command.ExecuteNonQueryAsync());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reconcile applies through <c>ApplyReplicatedAsync</c> with a row mapped from the secret artifact.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Reconcile_AppliesThroughApplyReplicatedAsync_MappedFromSecretArtifact()
|
||||
{
|
||||
@@ -340,6 +364,9 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
Assert.Equal(SecretContentType.Text, applied.ContentType);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reconcile round-trips every secret content type.</summary>
|
||||
/// <param name="contentType">The secret content type under test.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Theory]
|
||||
[InlineData(SecretContentType.Text)]
|
||||
[InlineData(SecretContentType.ConnectionString)]
|
||||
@@ -358,6 +385,8 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
|
||||
// ── Per-row isolation (ruling 2) ──
|
||||
|
||||
/// <summary>Verifies that when one row throws the other rows still apply.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Reconcile_OneRowThrows_OthersStillApply()
|
||||
{
|
||||
@@ -376,6 +405,8 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
Assert.False(store.Contains("bad"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a throwing row's failure is de-spammed like any other failure.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Reconcile_OneRowThrows_FailureIsDeSpammedLikeAnyOtherFailure()
|
||||
{
|
||||
@@ -395,6 +426,8 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
|
||||
// ── Cache invalidation (ruling 3) ──
|
||||
|
||||
/// <summary>Verifies that a successful apply invalidates the cache before verification.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Reconcile_SuccessfulApply_InvalidatesCache_BeforeVerify()
|
||||
{
|
||||
@@ -406,6 +439,8 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
Assert.Contains("a", invalidator.Invalidated);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a removal invalidates the cache.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Reconcile_Removal_InvalidatesCache()
|
||||
{
|
||||
@@ -420,6 +455,8 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
Assert.Contains("a", invalidator.Invalidated);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reconcile still applies when no cache invalidator is registered.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Reconcile_WithNoInvalidatorRegistered_StillApplies()
|
||||
{
|
||||
@@ -442,6 +479,8 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
|
||||
// ── Decrypt verification ──
|
||||
|
||||
/// <summary>Verifies that a resolver decrypt exception is reported together with the KEK id.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Reconcile_DecryptFailure_ExceptionFromResolver_IsReportedWithKekId()
|
||||
{
|
||||
@@ -454,11 +493,13 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
var message = Assert.Single(r.Failed);
|
||||
Assert.Contains("kek-central", message);
|
||||
Assert.Contains("'a'", message);
|
||||
// FailedNames (Task 10): the name behind each Failed entry, in lockstep order, so a
|
||||
// FailedNames: the name behind each Failed entry, in lockstep order, so a
|
||||
// caller (DeploymentManagerActor) can tell whether a failure belongs to names it shipped.
|
||||
Assert.Equal(["a"], r.FailedNames);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a null resolve is also reported as a decrypt failure.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Reconcile_DecryptFailure_NullResolve_IsAlsoReported()
|
||||
{
|
||||
@@ -471,6 +512,8 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
Assert.Single(r.Failed);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that reconcile verifies on every pass even when the secret is already applied.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Reconcile_VerifiesEveryPass_EvenWhenAlreadyApplied()
|
||||
{
|
||||
@@ -491,6 +534,8 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
|
||||
// ── Error-log de-spam (ruling 1 of the original task, unaffected by fix round 1) ──
|
||||
|
||||
/// <summary>Verifies that a decrypt failure logs an error the first time, then debug, then an error again after a success.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Reconcile_DecryptFailure_LogsErrorOnlyFirstTime_ThenDebug_ThenErrorAgainAfterASuccess()
|
||||
{
|
||||
@@ -527,6 +572,8 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
Assert.Equal(LogLevel.Error, failureEntriesAfterRecovery[3].Level);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a decrypt failure on a new revision logs an error again even without an intervening success.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Reconcile_DecryptFailure_NewRevision_LogsErrorAgain_EvenWithoutASuccessInBetween()
|
||||
{
|
||||
@@ -551,6 +598,8 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
|
||||
// ── Cancellation + concurrency (ruling 6) ──
|
||||
|
||||
/// <summary>Verifies that a pre-cancelled token makes reconcile throw an <see cref="OperationCanceledException"/>.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReconcileAsync_PreCancelledToken_ThrowsOperationCanceled()
|
||||
{
|
||||
@@ -562,6 +611,8 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
() => mirror.ReconcileAsync(cts.Token));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that concurrent reconcile callers both complete without applying twice.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ReconcileAsync_ConcurrentCallers_BothCompleteWithoutDoubleApply()
|
||||
{
|
||||
@@ -583,6 +634,7 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
|
||||
// ── Dispose (ruling 6) ──
|
||||
|
||||
/// <summary>Verifies that disposing the mirror does not throw and is idempotent.</summary>
|
||||
[Fact]
|
||||
public void Dispose_DoesNotThrow_AndIsIdempotent()
|
||||
{
|
||||
@@ -602,6 +654,8 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
|
||||
// ── Hosted loop ──
|
||||
|
||||
/// <summary>Verifies that the background loop runs once at start and again every interval.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_RunsOnceAtStart_AndAgainEveryInterval()
|
||||
{
|
||||
@@ -627,6 +681,8 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a per-row failure is isolated and the loop recovers once the failure clears.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task ExecuteAsync_PerRowFailure_IsIsolated_AndLoopRecoversOnceCleared()
|
||||
{
|
||||
@@ -656,8 +712,10 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
// ── Per-node status row for central (review finding M1) ──
|
||||
// ── Per-node status row for central ──
|
||||
|
||||
/// <summary>Verifies that the first pass after startup writes this node's status row even when healthy.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Status_FirstPassAfterStartup_WritesThisNodesRow_EvenWhenHealthy()
|
||||
{
|
||||
@@ -672,6 +730,8 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
Assert.Equal(time.Now, status.CheckedAtUtc);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an unchanged failure set is not rewritten on later passes.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Status_UnchangedFailureSet_IsNotRewrittenOnLaterPasses()
|
||||
{
|
||||
@@ -695,6 +755,8 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
Assert.Contains("kek-central", failure.Message);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an unchanged failure set is refreshed once the refresh interval has passed.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Status_UnchangedFailureSet_IsRefreshedOnceTheRefreshIntervalHasPassed()
|
||||
{
|
||||
@@ -715,6 +777,8 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
Assert.Equal(time.Now, Assert.Single(await storage.GetSecretMirrorStatusesAsync()).CheckedAtUtc);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a status row deleted by a peer prune is rewritten on the next pass.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Status_RowDeletedByAPeerPrune_IsRewrittenOnTheNextPass()
|
||||
{
|
||||
@@ -735,6 +799,8 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
Assert.Equal(time.Now, status.CheckedAtUtc);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a failure set change rewrites the status row and that it clears once healed.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Status_FailureSetChange_RewritesTheRow_AndClearsWhenHealed()
|
||||
{
|
||||
@@ -762,6 +828,8 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
Assert.Equal(time.Now, healed.CheckedAtUtc);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the status row carries only names and messages, never ciphertext.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Status_RowCarriesNamesAndMessagesOnly_NeverCiphertext()
|
||||
{
|
||||
@@ -780,6 +848,8 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
Assert.DoesNotContain(Convert.ToBase64String(new byte[] { 1, 2, 3, 4, 5 }), json);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a status write failure does not break the pass and is retried on the next pass.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Status_WriteFailure_DoesNotBreakThePass_AndIsRetriedNextPass()
|
||||
{
|
||||
@@ -807,6 +877,8 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
Assert.Equal("node-a", Assert.Single(await storage.GetSecretMirrorStatusesAsync()).NodeName);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that without a node identity the status row falls back to the machine name.</summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
[Fact]
|
||||
public async Task Status_WithoutNodeIdentity_FallsBackToMachineName()
|
||||
{
|
||||
@@ -819,6 +891,7 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
|
||||
private sealed class FixedNodeIdentity(string? name) : INodeIdentityProvider
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string? NodeName { get; } = name;
|
||||
}
|
||||
|
||||
@@ -890,11 +963,23 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
/// <summary>Thread-safe capturing logger so tests can assert on emitted log levels/messages, mirroring SiteReconciliationActorTests.</summary>
|
||||
private sealed class CapturingLogger<T> : ILogger<T>
|
||||
{
|
||||
/// <summary>Gets the captured log entries in emission order.</summary>
|
||||
public ConcurrentQueue<(LogLevel Level, string Message)> Entries { get; } = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
IDisposable? ILogger.BeginScope<TState>(TState state) => null;
|
||||
/// <summary>Reports whether logging is enabled for the given level.</summary>
|
||||
/// <param name="logLevel">The log level.</param>
|
||||
/// <returns>True if the level is enabled; otherwise false.</returns>
|
||||
public bool IsEnabled(LogLevel logLevel) => true;
|
||||
|
||||
/// <summary>Records the log entry for later assertions.</summary>
|
||||
/// <typeparam name="TState">The type of the state object.</typeparam>
|
||||
/// <param name="logLevel">The log level.</param>
|
||||
/// <param name="eventId">The event id.</param>
|
||||
/// <param name="state">The scope or log state.</param>
|
||||
/// <param name="exception">The exception associated with the entry, if any.</param>
|
||||
/// <param name="formatter">Formats the state and exception into the message text.</param>
|
||||
public void Log<TState>(
|
||||
LogLevel logLevel, EventId eventId, TState state, Exception? exception,
|
||||
Func<TState, Exception?, string> formatter)
|
||||
@@ -904,21 +989,28 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
/// <summary>Minimal controllable <see cref="TimeProvider"/> — mirrors the pattern used by <c>ApiMethodCacheTests</c>.</summary>
|
||||
private sealed class FakeTimeProvider : TimeProvider
|
||||
{
|
||||
/// <summary>Gets or sets the current time reported by this provider.</summary>
|
||||
public DateTimeOffset Now { get; set; }
|
||||
/// <summary>Initializes the provider at the given start time.</summary>
|
||||
/// <param name="start">The initial time.</param>
|
||||
public FakeTimeProvider(DateTimeOffset start) => Now = start;
|
||||
/// <inheritdoc />
|
||||
public override DateTimeOffset GetUtcNow() => Now;
|
||||
}
|
||||
|
||||
/// <summary>Records every <see cref="ISecretCacheInvalidator.Invalidate"/> call by secret name.</summary>
|
||||
private sealed class FakeSecretCacheInvalidator : ISecretCacheInvalidator
|
||||
{
|
||||
/// <summary>Gets the secret names invalidated so far, in call order.</summary>
|
||||
public List<string> Invalidated { get; } = new();
|
||||
/// <summary>Records the invalidated secret name.</summary>
|
||||
/// <param name="name">The secret name.</param>
|
||||
public void Invalidate(SecretName name) => Invalidated.Add(name.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// In-memory <see cref="ISecretStore"/> honouring the REAL <c>SqliteSecretStore</c>
|
||||
/// semantics (fix round 1, review Important #1): <see cref="ApplyReplicatedAsync"/> is
|
||||
/// semantics: <see cref="ApplyReplicatedAsync"/> is
|
||||
/// strict last-writer-wins via <see cref="SecretLastWriterWins.IsNewer"/> and applies
|
||||
/// verbatim (no revision bump); <see cref="DeleteAsync"/> tombstones in place (bumps
|
||||
/// revision, stamps <see cref="Now"/>) rather than hard-deleting.
|
||||
@@ -941,6 +1033,10 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
public IReadOnlyDictionary<string, StoredSecret> Rows => _rows;
|
||||
|
||||
/// <summary>Seeds a LIVE row directly (bypassing ApplyReplicatedAsync/LWW) — an out-of-band secret.</summary>
|
||||
/// <param name="name">The secret name.</param>
|
||||
/// <param name="updatedUtc">The last-updated timestamp stamped on the row.</param>
|
||||
/// <param name="revision">The row revision.</param>
|
||||
/// <param name="ciphertext">The ciphertext bytes, or null for a default placeholder.</param>
|
||||
public void Seed(string name, DateTimeOffset updatedUtc, long revision = 1, byte[]? ciphertext = null) =>
|
||||
_rows[name] = new StoredSecret
|
||||
{
|
||||
@@ -959,14 +1055,26 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
};
|
||||
|
||||
/// <summary>True if the row is present and NOT tombstoned.</summary>
|
||||
/// <param name="name">The secret name.</param>
|
||||
/// <returns>True if a live row exists for the name.</returns>
|
||||
public bool Contains(string name) => _rows.TryGetValue(name, out var row) && !row.IsDeleted;
|
||||
|
||||
/// <summary>True if the row is present as a tombstone.</summary>
|
||||
/// <param name="name">The secret name.</param>
|
||||
/// <returns>True if the row exists and is tombstoned.</returns>
|
||||
public bool IsTombstoned(string name) => _rows.TryGetValue(name, out var row) && row.IsDeleted;
|
||||
|
||||
/// <summary>Looks up the secret by name.</summary>
|
||||
/// <param name="name">The secret name.</param>
|
||||
/// <param name="ct">A token to cancel the operation.</param>
|
||||
/// <returns>The secret, or null when none matches.</returns>
|
||||
public Task<StoredSecret?> GetAsync(SecretName name, CancellationToken ct) =>
|
||||
Task.FromResult(_rows.TryGetValue(name.Value, out var row) ? row : null);
|
||||
|
||||
/// <summary>Stores or replaces the row in the in-memory fake.</summary>
|
||||
/// <param name="row">The secret row.</param>
|
||||
/// <param name="ct">A token to cancel the operation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public Task UpsertAsync(StoredSecret row, CancellationToken ct)
|
||||
{
|
||||
_rows[row.Name.Value] = row;
|
||||
@@ -974,6 +1082,10 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Tombstones (does not hard-delete) — matches <c>SqliteSecretStore.DeleteAsync</c>.</summary>
|
||||
/// <param name="name">The secret name.</param>
|
||||
/// <param name="actor">The actor recorded on the tombstone.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>True if a live row was tombstoned; false if it was absent or already deleted.</returns>
|
||||
public Task<bool> DeleteAsync(SecretName name, string? actor, CancellationToken ct)
|
||||
{
|
||||
if (!_rows.TryGetValue(name.Value, out var existing) || existing.IsDeleted)
|
||||
@@ -992,13 +1104,23 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
/// <summary>Returns an empty secret listing.</summary>
|
||||
/// <param name="includeDeleted">Whether deleted secrets are included, which the fake ignores.</param>
|
||||
/// <param name="ct">A token to cancel the operation.</param>
|
||||
/// <returns>An empty list.</returns>
|
||||
public Task<IReadOnlyList<SecretMetadata>> ListAsync(bool includeDeleted, CancellationToken ct) =>
|
||||
Task.FromResult<IReadOnlyList<SecretMetadata>>([]);
|
||||
|
||||
/// <summary>Returns an empty manifest.</summary>
|
||||
/// <param name="ct">A token to cancel the operation.</param>
|
||||
/// <returns>An empty manifest.</returns>
|
||||
public Task<IReadOnlyList<SecretManifestEntry>> GetManifestAsync(CancellationToken ct) =>
|
||||
Task.FromResult<IReadOnlyList<SecretManifestEntry>>([]);
|
||||
|
||||
/// <summary>Strict LWW on (UpdatedUtc, Revision) via <see cref="SecretLastWriterWins.IsNewer"/>; applied verbatim — matches <c>SqliteSecretStore</c>.</summary>
|
||||
/// <param name="row">The replicated row to apply.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public Task ApplyReplicatedAsync(StoredSecret row, CancellationToken ct)
|
||||
{
|
||||
if (ThrowOnApply)
|
||||
@@ -1016,6 +1138,11 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>Reports that no rewrap was applied.</summary>
|
||||
/// <param name="rewrappedRow">The rewrapped secret row.</param>
|
||||
/// <param name="expectedCurrentWrappedDek">The wrapped data key the row is expected to hold currently.</param>
|
||||
/// <param name="ct">A token to cancel the operation.</param>
|
||||
/// <returns>Always false.</returns>
|
||||
public Task<bool> ApplyRewrapAsync(
|
||||
StoredSecret rewrappedRow, byte[] expectedCurrentWrappedDek, CancellationToken ct) =>
|
||||
Task.FromResult(false);
|
||||
@@ -1033,17 +1160,24 @@ public class DeployedSecretsMirrorTests : IDisposable
|
||||
public FakeSecretStore? BackingStore { get; set; }
|
||||
|
||||
/// <summary>Row absent/tombstoned path — <c>DefaultSecretResolver.GetAsync</c> returns null.</summary>
|
||||
/// <param name="name">The secret name that should resolve to null.</param>
|
||||
public void ReturnNullFor(string name) => _nullReturning.Add(name);
|
||||
|
||||
/// <summary>Decryption/integrity failure path — <c>DefaultSecretResolver.GetAsync</c> rethrows <c>SecretDecryptionException</c>.</summary>
|
||||
/// <param name="name">The secret name whose resolution should throw.</param>
|
||||
public void ThrowFor(string name) => _throwing.Add(name);
|
||||
|
||||
/// <summary>Clears every configured null-returning and throwing name.</summary>
|
||||
public void ClearFailures()
|
||||
{
|
||||
_nullReturning.Clear();
|
||||
_throwing.Clear();
|
||||
}
|
||||
|
||||
/// <summary>Looks up the secret by name.</summary>
|
||||
/// <param name="name">The secret name.</param>
|
||||
/// <param name="ct">A token to cancel the operation.</param>
|
||||
/// <returns>The secret, or null when none matches.</returns>
|
||||
public Task<string?> GetAsync(SecretName name, CancellationToken ct)
|
||||
{
|
||||
Interlocked.Increment(ref CallCount);
|
||||
|
||||
Reference in New Issue
Block a user