package mxgateway import ( "errors" "strings" "testing" pb "gitea.dohertylan.com/dohertj2/mxaccessgw/clients/go/internal/generated" ) // TestScrubReplyStringsRedactsEveryOccurrence covers the multi-occurrence case: // one secret appearing across ProtocolStatus.Message, DiagnosticMessage, and every // Statuses[].DiagnosticText must be fully redacted with no residue. func TestScrubReplyStringsRedactsEveryOccurrence(t *testing.T) { const secret = "hunter2" reply := &pb.MxCommandReply{ ProtocolStatus: &pb.ProtocolStatus{Message: "rejected hunter2 then hunter2 again"}, DiagnosticMessage: "echoed hunter2 back", Statuses: []*pb.MxStatusProxy{ {DiagnosticText: "first hunter2"}, {DiagnosticText: "second hunter2 and hunter2"}, }, } scrubbed := scrubReplyStrings(reply, []string{secret}) if strings.Contains(scrubbed.GetProtocolStatus().GetMessage(), secret) { t.Fatalf("ProtocolStatus.Message still contains the secret: %q", scrubbed.GetProtocolStatus().GetMessage()) } if strings.Contains(scrubbed.GetDiagnosticMessage(), secret) { t.Fatalf("DiagnosticMessage still contains the secret: %q", scrubbed.GetDiagnosticMessage()) } for i, status := range scrubbed.GetStatuses() { if strings.Contains(status.GetDiagnosticText(), secret) { t.Fatalf("Statuses[%d].DiagnosticText still contains the secret: %q", i, status.GetDiagnosticText()) } } if !strings.Contains(scrubbed.GetProtocolStatus().GetMessage(), redactedSecretMarker) { t.Fatalf("ProtocolStatus.Message missing redaction marker: %q", scrubbed.GetProtocolStatus().GetMessage()) } // The original reply must be untouched (scrubReplyStrings clones). if !strings.Contains(reply.GetDiagnosticMessage(), secret) { t.Fatal("scrubReplyStrings mutated the original reply instead of cloning it") } } // TestScrubReplyStringsRedactsOverlappingSecrets covers two secrets where one is a // substring of the other: both must be fully redacted, with no partial leak of the // longer secret's non-shared remainder. func TestScrubReplyStringsRedactsOverlappingSecrets(t *testing.T) { const shortSecret = "pass" const longSecret = "password123" reply := &pb.MxCommandReply{ DiagnosticMessage: "value was password123 and also pass", } scrubbed := scrubReplyStrings(reply, []string{longSecret, shortSecret}) got := scrubbed.GetDiagnosticMessage() if strings.Contains(got, shortSecret) { t.Fatalf("scrubbed message still contains a secret substring %q: %q", shortSecret, got) } if strings.Contains(got, longSecret) { t.Fatalf("scrubbed message still contains %q: %q", longSecret, got) } // "123" is the longer secret's remainder past the shared "pass" prefix; it must // not survive as a partial leak. if strings.Contains(got, "123") { t.Fatalf("scrubbed message leaked the longer secret's remainder: %q", got) } } // TestRedactSecretsEmptyOrNilLeavesErrorUnchanged confirms the no-secret paths keep // the original typed error verbatim (no wrapping, no scrubbed clone). func TestRedactSecretsEmptyOrNilLeavesErrorUnchanged(t *testing.T) { base := &MxAccessError{Reply: &pb.MxCommandReply{DiagnosticMessage: "boom"}} if got := redactSecrets(base); got != error(base) { t.Fatalf("redactSecrets with no secrets = %v, want the original error unchanged", got) } if got := redactSecrets(base, ""); got != error(base) { t.Fatalf("redactSecrets with only an empty secret = %v, want the original error unchanged", got) } if got := redactSecrets(nil, "secret"); got != nil { t.Fatalf("redactSecrets(nil, ...) = %v, want nil", got) } } // TestRedactSecretsRebuildsTypedCommandError confirms a *CommandError (non-MXAccess // path) is rebuilt with a scrubbed Status and Reply, and errors.As still reaches it. func TestRedactSecretsRebuildsTypedCommandError(t *testing.T) { const secret = "topSecretValue" base := &CommandError{ Op: "write secured", Status: &pb.ProtocolStatus{Message: "rejected topSecretValue"}, Reply: &pb.MxCommandReply{DiagnosticMessage: "echoed topSecretValue"}, } redacted := redactSecrets(base, secret) var cmdErr *CommandError if !errors.As(redacted, &cmdErr) { t.Fatalf("redactSecrets result %T does not unwrap to *CommandError", redacted) } if strings.Contains(cmdErr.Status.GetMessage(), secret) { t.Fatalf("CommandError.Status.Message leaked the secret: %q", cmdErr.Status.GetMessage()) } if strings.Contains(cmdErr.Reply.GetDiagnosticMessage(), secret) { t.Fatalf("CommandError.Reply.DiagnosticMessage leaked the secret: %q", cmdErr.Reply.GetDiagnosticMessage()) } if strings.Contains(redacted.Error(), secret) { t.Fatalf("rendered error leaked the secret: %q", redacted.Error()) } }