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
@@ -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);