refactor(adminui): omit error key on success cert-audit rows + assert OccurredAtUtc (review)

This commit is contained in:
Joseph Doherty
2026-06-19 00:38:41 -04:00
parent 23d45efcfd
commit 3ee0099fae
2 changed files with 9 additions and 8 deletions
@@ -28,18 +28,17 @@ public static class CertAuditEvents
/// <see cref="AuditOutcome.Success" />); otherwise <see langword="false" /> (Outcome /// <see cref="AuditOutcome.Success" />); otherwise <see langword="false" /> (Outcome
/// <see cref="AuditOutcome.Failure" />).</param> /// <see cref="AuditOutcome.Failure" />).</param>
/// <param name="error">On failure, the error text carried in the details payload; ignored on /// <param name="error">On failure, the error text carried in the details payload; ignored on
/// success (the details payload serializes <see langword="null" /> for the error field).</param> /// success (the <c>error</c> field is omitted from the details payload entirely).</param>
/// <returns>A fully populated <see cref="AuditEvent" /> with a fresh <see cref="AuditEvent.EventId" /> /// <returns>A fully populated <see cref="AuditEvent" /> with a fresh <see cref="AuditEvent.EventId" />
/// and <see cref="AuditEvent.OccurredAtUtc" /> set to now (UTC).</returns> /// and <see cref="AuditEvent.OccurredAtUtc" /> set to now (UTC).</returns>
public static AuditEvent Build( public static AuditEvent Build(
string action, string store, string thumbprint, string actor, bool success, string? error) string action, string store, string thumbprint, string actor, bool success, string? error)
{ {
var detailsJson = JsonSerializer.Serialize(new // On success the error field is omitted entirely (not serialized as null) so the common
{ // success-path audit row carries no dead "error" key.
store, var detailsJson = JsonSerializer.Serialize(success
thumbprint, ? (object)new { store, thumbprint }
error = success ? null : error, : new { store, thumbprint, error });
});
return new AuditEvent return new AuditEvent
{ {
@@ -26,6 +26,7 @@ public sealed class CertAuditEventsTests
evt.Actor.ShouldBe("alice"); evt.Actor.ShouldBe("alice");
evt.Outcome.ShouldBe(AuditOutcome.Success); evt.Outcome.ShouldBe(AuditOutcome.Success);
evt.EventId.ShouldNotBe(Guid.Empty); evt.EventId.ShouldNotBe(Guid.Empty);
evt.OccurredAtUtc.ShouldBeGreaterThan(DateTimeOffset.UtcNow.AddSeconds(-5));
evt.DetailsJson.ShouldNotBeNull(); evt.DetailsJson.ShouldNotBeNull();
evt.DetailsJson!.ShouldContain(Thumb); evt.DetailsJson!.ShouldContain(Thumb);
} }
@@ -88,13 +89,14 @@ public sealed class CertAuditEventsTests
evt.DetailsJson!.ShouldContain("store write failed"); evt.DetailsJson!.ShouldContain("store write failed");
} }
/// <summary>On success the error text is omitted from the details payload (it serializes null).</summary> /// <summary>On success the error text AND the error key are omitted from the details payload.</summary>
[Fact] [Fact]
public void Build_success_omits_error_text() public void Build_success_omits_error_text()
{ {
var evt = CertAuditEvents.Build("Trust", "rejected", Thumb, "alice", success: true, error: "ignored"); var evt = CertAuditEvents.Build("Trust", "rejected", Thumb, "alice", success: true, error: "ignored");
evt.DetailsJson!.ShouldNotContain("ignored"); evt.DetailsJson!.ShouldNotContain("ignored");
evt.DetailsJson!.ShouldNotContain("error");
} }
/// <summary>The public Category constant matches the value stamped onto built events.</summary> /// <summary>The public Category constant matches the value stamped onto built events.</summary>