Files
scadaproj/ZB.MOM.WW.Secrets/tests/ZB.MOM.WW.Secrets.Ui.Tests/Fakes/CapturingAuditWriter.cs
T
Joseph Doherty e1487fbf10 fix(secrets): UI reports a committed change as saved when its audit throws (scadaproj#34)
SecretEditor and ConfirmDeleteModal wrote the success audit inside the same
try as the store write, so a throwing IAuditWriter after a committed
add/rotate/delete showed an error, wrote a Failure audit and skipped the
resolver-cache eviction - the old plaintext kept resolving for the TTL.

The mutation try now covers only the cipher + store call; the success audit
is best-effort afterwards and logs a warning (no value) on failure, so the
change is reported saved, the callback fires and the cache is evicted.

- tests: AuditFailureAfterCommitTests (add, rotate, delete) with a throwing
  success audit; all fail without the fix
- code-reviews: Secrets-013 Resolved; README regenerated

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
2026-11-09 15:00:06 -05:00

37 lines
1.3 KiB
C#

using System.Collections.Concurrent;
using ZB.MOM.WW.Audit;
namespace ZB.MOM.WW.Secrets.Ui.Tests.Fakes;
/// <summary>
/// An <see cref="IAuditWriter"/> test double that collects every <see cref="AuditEvent"/> written,
/// so a test can assert on the audit trail (action, outcome, target, actor) and — critically — that
/// no plaintext ever appears in any audit field.
/// </summary>
public sealed class CapturingAuditWriter : IAuditWriter
{
private readonly ConcurrentQueue<AuditEvent> _events = new();
/// <summary>The events captured so far, in write order.</summary>
public IReadOnlyList<AuditEvent> Events => _events.ToArray();
/// <summary>
/// When set, every <see cref="AuditOutcome.Success"/> write throws this instead of being captured
/// — the "audit sink briefly unavailable after the store committed" case (scadaproj#34).
/// Failure-outcome writes are still captured, so a test can prove none was written.
/// </summary>
public Exception? SuccessFault { get; set; }
/// <inheritdoc />
public Task WriteAsync(AuditEvent evt, CancellationToken ct = default)
{
if (SuccessFault is not null && evt.Outcome == AuditOutcome.Success)
{
throw SuccessFault;
}
_events.Enqueue(evt);
return Task.CompletedTask;
}
}