From 3ee0099fae1fad4a10bae53b23450849f40b8968 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 19 Jun 2026 00:38:41 -0400 Subject: [PATCH] refactor(adminui): omit error key on success cert-audit rows + assert OccurredAtUtc (review) --- .../Audit/CertAuditEvents.cs | 13 ++++++------- .../Audit/CertAuditEventsTests.cs | 4 +++- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Audit/CertAuditEvents.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Audit/CertAuditEvents.cs index 910bfb2b..3244a9b5 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Audit/CertAuditEvents.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Audit/CertAuditEvents.cs @@ -28,18 +28,17 @@ public static class CertAuditEvents /// ); otherwise (Outcome /// ). /// On failure, the error text carried in the details payload; ignored on - /// success (the details payload serializes for the error field). + /// success (the error field is omitted from the details payload entirely). /// A fully populated with a fresh /// and set to now (UTC). public static AuditEvent Build( string action, string store, string thumbprint, string actor, bool success, string? error) { - var detailsJson = JsonSerializer.Serialize(new - { - store, - thumbprint, - error = success ? null : error, - }); + // On success the error field is omitted entirely (not serialized as null) so the common + // success-path audit row carries no dead "error" key. + var detailsJson = JsonSerializer.Serialize(success + ? (object)new { store, thumbprint } + : new { store, thumbprint, error }); return new AuditEvent { diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Audit/CertAuditEventsTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Audit/CertAuditEventsTests.cs index 68cf6f7b..ee10c8f3 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Audit/CertAuditEventsTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Audit/CertAuditEventsTests.cs @@ -26,6 +26,7 @@ public sealed class CertAuditEventsTests evt.Actor.ShouldBe("alice"); evt.Outcome.ShouldBe(AuditOutcome.Success); evt.EventId.ShouldNotBe(Guid.Empty); + evt.OccurredAtUtc.ShouldBeGreaterThan(DateTimeOffset.UtcNow.AddSeconds(-5)); evt.DetailsJson.ShouldNotBeNull(); evt.DetailsJson!.ShouldContain(Thumb); } @@ -88,13 +89,14 @@ public sealed class CertAuditEventsTests evt.DetailsJson!.ShouldContain("store write failed"); } - /// On success the error text is omitted from the details payload (it serializes null). + /// On success the error text AND the error key are omitted from the details payload. [Fact] public void Build_success_omits_error_text() { var evt = CertAuditEvents.Build("Trust", "rejected", Thumb, "alice", success: true, error: "ignored"); evt.DetailsJson!.ShouldNotContain("ignored"); + evt.DetailsJson!.ShouldNotContain("error"); } /// The public Category constant matches the value stamped onto built events.