package protolog import ( "bytes" "context" "log/slog" "strings" "testing" ) // TestRedactCommandLine_HidesCredentials ist Teil der geforderten // Pflichtprüfung 1 (ING-08): Redaktion sensibler Felder. func TestRedactCommandLine_HidesCredentials(t *testing.T) { cases := []struct { verb string args []string wantSafe bool // true: darf das geheimnis NICHT enthalten secret string }{ {"PASS", []string{"geheim123"}, true, "geheim123"}, {"LOGIN", []string{"alice", "geheim123"}, true, "geheim123"}, {"AUTH", []string{"PLAIN", "AGFsaWNlAGdlaGVpbTEyMw=="}, true, "AGFsaWNlAGdlaGVpbTEyMw=="}, {"STAT", nil, false, ""}, {"USER", []string{"alice"}, false, "alice"}, } for _, tc := range cases { out := RedactCommandLine(tc.verb, tc.args) if tc.wantSafe && strings.Contains(out, tc.secret) { t.Fatalf("%s: geheimnis im klartext gefunden: %q", tc.verb, out) } if !strings.HasPrefix(out, strings.ToUpper(tc.verb)) { t.Fatalf("%s: kommandoname fehlt in redigierter zeile: %q", tc.verb, out) } } } // TestSessionLogger_EventNeverContainsRawMessage stellt sicher, dass // über die reguläre Event-API keine Nachrichteninhalte geloggt werden // können, ohne dass der Aufrufer sie explizit (und damit sichtbar im // Code) als Attribut übergibt — Event selbst fügt nie Rohinhalte hinzu. func TestSessionLogger_EventNeverContainsRawMessage(t *testing.T) { var buf bytes.Buffer logger := slog.New(slog.NewJSONHandler(&buf, nil)) sl := NewSessionLogger(logger, "pop3") sl.Event(context.Background(), "command", slog.String("command", RedactCommandLine("PASS", []string{"geheim123"}))) if strings.Contains(buf.String(), "geheim123") { t.Fatalf("passwort im log gefunden: %s", buf.String()) } if !strings.Contains(buf.String(), sl.CorrelationID()) { t.Fatalf("correlation_id fehlt im log: %s", buf.String()) } } // TestReconstruct_ReturnsOnlyMatchingSessionInOrder ist die geforderte // Pflichtprüfung 2 (ING-08): eine komplette Session ist über die // Korrelations-ID lückenlos rekonstruierbar, aus einem Log mit // mehreren gemischten Sessions. func TestReconstruct_ReturnsOnlyMatchingSessionInOrder(t *testing.T) { var buf bytes.Buffer logger := slog.New(slog.NewJSONHandler(&buf, nil)) target := NewSessionLogger(logger, "imap") other := NewSessionLogger(logger, "imap") target.Event(context.Background(), "session_start", slog.String("remote_addr", "127.0.0.1:1")) other.Event(context.Background(), "session_start", slog.String("remote_addr", "127.0.0.1:2")) target.Event(context.Background(), "command", slog.String("command", "LOGIN [REDACTED]")) other.Event(context.Background(), "command", slog.String("command", "SELECT INBOX")) target.Event(context.Background(), "command", slog.String("command", "SELECT INBOX")) target.Event(context.Background(), "session_end") other.Event(context.Background(), "session_end") entries, err := Reconstruct(&buf, target.CorrelationID()) if err != nil { t.Fatalf("Reconstruct: %v", err) } if len(entries) != 4 { t.Fatalf("erwartete 4 einträge für die zielsession, habe %d", len(entries)) } wantMsgs := []string{"session_start", "command", "command", "session_end"} for i, e := range entries { if e.Msg != wantMsgs[i] { t.Fatalf("eintrag %d: erwartete msg %q, habe %q", i, wantMsgs[i], e.Msg) } if e.CorrelationID != target.CorrelationID() { t.Fatalf("eintrag %d gehört zur falschen session", i) } } }