Files
OwnCord/Server/db/audit_test.go
T
J3vbandClaude Fable 5 b60bc8d04b feat(audit): route every LogAudit call through a best-effort WriteAudit helper
Audit writes stay best-effort — a LogAudit failure must never fail or abort
the request — but a failed write must no longer be silently discarded. Add
db.WriteAudit(auditor, actor, action, targetType, targetID, detail), which
logs a failed write with actor/action/target context (never the detail
string, which may be sensitive) and never propagates the error.

The Auditor interface is satisfied structurally by both *db.DB and the
service-layer Store, so api/admin/ws/service all reach the helper without an
import cycle. Converts all ~26 call sites from `_ = LogAudit(...)` (and the
two backup handlers' inline `if err` blocks) to db.WriteAudit. Pinned by
db/audit_test.go: failure logged and not propagated, success logs nothing,
detail never leaks.

Resolves the repo-wide LogAudit policy question flagged by the D8 note.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 10:48:05 +02:00

80 lines
2.0 KiB
Go

package db_test
import (
"errors"
"log/slog"
"strings"
"testing"
"github.com/owncord/server/db"
)
// fakeAuditor lets a test drive WriteAudit down either the success or the
// failure path without a real database.
type fakeAuditor struct {
err error
called bool
}
func (f *fakeAuditor) LogAudit(_ int64, _, _ string, _ int64, _ string) error {
f.called = true
return f.err
}
// captureLogs redirects the default slog logger to a buffer for the duration
// of fn and returns everything it wrote.
func captureLogs(t *testing.T, fn func()) string {
t.Helper()
var buf strings.Builder
prev := slog.Default()
slog.SetDefault(slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug})))
defer slog.SetDefault(prev)
fn()
return buf.String()
}
func TestWriteAudit_LogsFailureButDoesNotPropagate(t *testing.T) {
a := &fakeAuditor{err: errors.New("disk on fire")}
// WriteAudit returns nothing, so "never propagated" is structural — the
// call simply must not panic and must record the failure.
out := captureLogs(t, func() {
db.WriteAudit(a, 7, "user_ban", "user", 42, "spam")
})
if !a.called {
t.Fatal("WriteAudit did not attempt the underlying LogAudit")
}
for _, want := range []string{
"audit log write failed",
"action=user_ban",
"actor_id=7",
"target_type=user",
"target_id=42",
"disk on fire",
} {
if !strings.Contains(out, want) {
t.Errorf("failure log missing %q; got: %s", want, out)
}
}
// The detail string must not leak into logs.
if strings.Contains(out, "spam") {
t.Errorf("detail string leaked into audit failure log: %s", out)
}
}
func TestWriteAudit_SuccessLogsNothing(t *testing.T) {
a := &fakeAuditor{err: nil}
out := captureLogs(t, func() {
db.WriteAudit(a, 1, "user_login", "user", 1, "")
})
if !a.called {
t.Fatal("WriteAudit did not attempt the underlying LogAudit")
}
if strings.Contains(out, "audit log write failed") {
t.Errorf("successful audit write should not log a failure; got: %s", out)
}
}