using ZB.MOM.WW.MxGateway.Contracts.Proto; namespace ZB.MOM.WW.MxGateway.Client.Tests; /// /// Unit tests for — the exact-substring scrub applied to /// diagnostic text and rebuilt exceptions before they leave the client on a failure path. /// public sealed class MxGatewaySecretRedactionTests { [Fact] public void Redact_ReplacesEveryOccurrenceOfSecret() { string result = MxGatewaySecretRedaction.Redact( "pw=hunter2 retry pw=hunter2 again hunter2", "hunter2"); Assert.DoesNotContain("hunter2", result, StringComparison.Ordinal); Assert.Equal("pw= retry pw= again ", result); } [Fact] public void Redact_ScrubsBothSecretsWhenOneIsSubstringOfTheOther() { // "secret" is a substring of "secretPassword"; both must be fully scrubbed regardless of // supplied order — no residual leak of either verbatim value. string result = MxGatewaySecretRedaction.Redact( "a=secretPassword b=secret", "secret", "secretPassword"); Assert.DoesNotContain("secretPassword", result, StringComparison.Ordinal); Assert.DoesNotContain("secret", result, StringComparison.Ordinal); } [Fact] public void Redact_WithNullSecretsArray_ReturnsMessageUnchanged() { const string message = "nothing to scrub here"; string result = MxGatewaySecretRedaction.Redact(message, null!); Assert.Equal(message, result); } [Fact] public void Redact_WithEmptySecretsArray_ReturnsMessageUnchanged() { const string message = "nothing to scrub here"; string result = MxGatewaySecretRedaction.Redact(message); Assert.Equal(message, result); } [Fact] public void Redact_IgnoresWhitespaceOnlySecret() { // A whitespace-only secret must not over-redact the internal spaces of the message. const string message = "user operator logged in"; string result = MxGatewaySecretRedaction.Redact(message, " "); Assert.Equal(message, result); } [Fact] public void Redacted_PreservesConcreteSubtypeAndDoesNotChainSecretBearingOriginal() { const string secret = "hunter2"; Exception transportCause = new InvalidOperationException("transport reset"); MxGatewaySessionException original = new( $"session rejected credential '{secret}'", "session-1", "correlation-1", new ProtocolStatus { Code = ProtocolStatusCode.SessionNotReady, Message = $"echoed '{secret}'" }, hResult: -1, statuses: [new MxStatusProxy { DiagnosticText = $"denied '{secret}'" }], innerException: transportCause); MxGatewayException redacted = MxGatewaySecretRedaction.Redacted(original, secret); // Concrete runtime type is preserved. Assert.IsType(redacted); // The secret is gone from the message and every structured accessor. Assert.DoesNotContain(secret, redacted.Message, StringComparison.Ordinal); Assert.DoesNotContain(secret, redacted.ToString(), StringComparison.Ordinal); Assert.DoesNotContain(secret, redacted.ProtocolStatus!.Message, StringComparison.Ordinal); Assert.All(redacted.Statuses, status => Assert.DoesNotContain(secret, status.DiagnosticText, StringComparison.Ordinal)); Assert.Contains("", redacted.Message, StringComparison.Ordinal); // The secret-bearing original is NOT chained; the original's transport cause is carried. Assert.NotSame(original, redacted.InnerException); Assert.Same(transportCause, redacted.InnerException); } }