package audit import ( "bytes" "encoding/json" "os" "path/filepath" "strings" "sync" "testing" "time" ) func newTestLogger(t *testing.T, opts Options) (*Logger, string, *bytes.Buffer) { t.Helper() dir := t.TempDir() path := filepath.Join(dir, "audit.log") var stdout bytes.Buffer opts.Stdout = &stdout if opts.MaxSizeMB == 0 { opts.MaxSizeMB = 50 } lg, err := New(path, opts) if err != nil { t.Fatalf("New: %v", err) } t.Cleanup(func() { lg.Close() }) return lg, path, &stdout } func readLines(t *testing.T, path string) []map[string]any { t.Helper() raw, err := os.ReadFile(path) if err != nil { t.Fatal(err) } var out []map[string]any for _, line := range strings.Split(strings.TrimSpace(string(raw)), "\n") { if line == "" { continue } var m map[string]any if err := json.Unmarshal([]byte(line), &m); err != nil { t.Fatalf("Zeile ist kein gültiges JSON: %q: %v", line, err) } out = append(out, m) } return out } func TestLogWritesJSONLWithTimezone(t *testing.T) { fixed := time.Date(2026, 8, 14, 9, 32, 11, 0, time.FixedZone("CEST", 2*3600)) lg, path, stdout := newTestLogger(t, Options{Clock: func() time.Time { return fixed }}) lg.Log(Event{Event: EventLoginSuccess, User: "mmueller", SrcIP: "10.1.20.34", Session: "a3f9"}) if err := lg.Close(); err != nil { t.Fatal(err) } lines := readLines(t, path) if len(lines) != 1 { t.Fatalf("got %d Zeilen, want 1", len(lines)) } got := lines[0] if got["ts"] != "2026-08-14T09:32:11+02:00" { t.Errorf("ts = %v, want RFC3339 mit Zeitzone", got["ts"]) } if got["event"] != EventLoginSuccess || got["user"] != "mmueller" { t.Errorf("Feldwerte falsch: %v", got) } if _, present := got["reason"]; present { t.Errorf("leere Felder dürfen nicht serialisiert werden: %v", got) } if !strings.Contains(stdout.String(), EventLoginSuccess) { t.Error("Event muss zusätzlich nach stdout gehen") } } func TestLogIsConcurrencySafe(t *testing.T) { lg, path, _ := newTestLogger(t, Options{}) var wg sync.WaitGroup for i := 0; i < 50; i++ { wg.Add(1) go func() { defer wg.Done() lg.Log(Event{Event: EventLoginFailed, User: UnknownUser, SrcIP: "10.0.0.1"}) }() } wg.Wait() if err := lg.Close(); err != nil { t.Fatal(err) } if n := len(readLines(t, path)); n != 50 { t.Fatalf("got %d Zeilen, want 50 (kein Interleaving erlaubt)", n) } } func TestNewCreatesFileWithTightPermissions(t *testing.T) { _, path, _ := newTestLogger(t, Options{}) info, err := os.Stat(path) if err != nil { t.Fatal(err) } if perm := info.Mode().Perm(); perm&0o077 != 0 { t.Errorf("Audit-Log hat Rechte %#o — Gruppe/Welt dürfen nicht zugreifen", perm) } } func TestShortSessionNeverLeaksToken(t *testing.T) { token := "0123456789abcdef0123456789abcdef" short := ShortSession(token) if len(short) != 4 { t.Errorf("ShortSession = %q, want 4 Zeichen", short) } if strings.Contains(token, short) { t.Errorf("gekürzte ID %q ist ein Präfix des Tokens — muss gehasht sein", short) } if ShortSession(token) != short { t.Error("ShortSession muss deterministisch sein") } if ShortSession("") != "" { t.Error("leerer Token muss leere ID liefern") } }