namespace ZB.MOM.WW.Audit.Tests; public class TruncatingAuditRedactorTests { private static AuditEvent Evt(string? details, string? target = null) => new() { EventId = Guid.NewGuid(), OccurredAtUtc = DateTimeOffset.UtcNow, Actor = "a", Action = "x", Outcome = AuditOutcome.Success, DetailsJson = details, Target = target, }; [Fact] public void Short_values_pass_through_unchanged() { var r = new TruncatingAuditRedactor(new() { MaxDetailsJsonLength = 100 }); var evt = Evt("small"); Assert.Equal("small", r.Apply(evt).DetailsJson); } [Fact] public void Oversized_details_are_truncated_with_marker() { var opts = new TruncatingAuditRedactorOptions { MaxDetailsJsonLength = 10, TruncationMarker = "~" }; var r = new TruncatingAuditRedactor(opts); var result = r.Apply(Evt(new string('x', 50))); Assert.Equal(10, result.DetailsJson!.Length); Assert.EndsWith("~", result.DetailsJson); } [Fact] public void Oversized_target_is_truncated() { var r = new TruncatingAuditRedactor(new() { MaxTargetLength = 5, TruncationMarker = "" }); var result = r.Apply(Evt(null, target: "abcdefghij")); Assert.Equal(5, result.Target!.Length); } [Fact] public void Null_fields_are_left_null() { var r = new TruncatingAuditRedactor(); var result = r.Apply(Evt(null)); Assert.Null(result.DetailsJson); Assert.Null(result.Target); } [Fact] public void Marker_longer_than_max_clips_the_marker_itself() { // Misconfiguration: marker longer than the cap. Must not throw; clips to the first max chars. var opts = new TruncatingAuditRedactorOptions { MaxDetailsJsonLength = 3, TruncationMarker = "…[truncated]" }; var r = new TruncatingAuditRedactor(opts); var result = r.Apply(Evt(new string('x', 20))); Assert.Equal(3, result.DetailsJson!.Length); } [Fact] public void Negative_max_is_treated_as_zero_and_does_not_throw() { // A negative cap is nonsensical misconfiguration. Truncate must clamp to 0 rather than // throw, capping the value to the empty string (plus marker handling). var opts = new TruncatingAuditRedactorOptions { MaxDetailsJsonLength = -5, MaxTargetLength = -1, TruncationMarker = "" }; var r = new TruncatingAuditRedactor(opts); var result = r.Apply(Evt(new string('x', 20), target: new string('y', 20))); Assert.Equal(string.Empty, result.DetailsJson); Assert.Equal(string.Empty, result.Target); } [Fact] public void Over_redact_fallback_scrubs_both_details_and_target_without_throwing() { // Drive the REAL TruncatingAuditRedactor.Apply into its catch branch via a reachable // misconfiguration (a null TruncationMarker faults inside Truncate). The over-redact // fallback must be strictly safer: BOTH DetailsJson AND Target scrubbed to null, no throw. var opts = new TruncatingAuditRedactorOptions { MaxDetailsJsonLength = 5, TruncationMarker = null! }; var r = new TruncatingAuditRedactor(opts); var raw = Evt(new string('x', 50), target: "sensitive target"); var result = r.Apply(raw); Assert.Null(result.DetailsJson); Assert.Null(result.Target); } }