package audit import ( "context" "fmt" "os" "testing" "time" "github.com/jackc/pgx/v5/pgxpool" ) func setupAppendOnlyTest(t *testing.T) (*Log, *pgxpool.Pool, func()) { t.Helper() adminDSN := os.Getenv("TEST_ADMIN_DSN") if adminDSN == "" { t.Skip("TEST_ADMIN_DSN nicht gesetzt, Integrationstest uebersprungen") } ctx := context.Background() pool, err := pgxpool.New(ctx, adminDSN) if err != nil { t.Fatalf("pool: %v", err) } if _, err := pool.Exec(ctx, ` CREATE TABLE IF NOT EXISTS audit_events ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(), tenant_slug TEXT NOT NULL CHECK (tenant_slug <> ''), actor TEXT NOT NULL CHECK (actor <> ''), action TEXT NOT NULL CHECK (action <> ''), target TEXT NOT NULL, metadata JSONB NOT NULL DEFAULT '{}'::jsonb ); CREATE OR REPLACE FUNCTION audit_events_prevent_mutation() RETURNS TRIGGER AS $$ BEGIN RAISE EXCEPTION 'audit_events ist append-only: % ist nicht erlaubt', TG_OP; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS audit_events_no_update ON audit_events; CREATE TRIGGER audit_events_no_update BEFORE UPDATE ON audit_events FOR EACH ROW EXECUTE FUNCTION audit_events_prevent_mutation(); DROP TRIGGER IF EXISTS audit_events_no_delete ON audit_events; CREATE TRIGGER audit_events_no_delete BEFORE DELETE ON audit_events FOR EACH ROW EXECUTE FUNCTION audit_events_prevent_mutation(); `); err != nil { t.Fatalf("schema: %v", err) } cleanup := func() { pool.Close() } return NewLog(pool), pool, cleanup } // Akzeptanzkriterium 1 + Pruefung 1: direkter UPDATE/DELETE-Versuch wird von // der Datenbank abgewiesen. func TestAppendOnly_RejectsUpdateAndDelete(t *testing.T) { log, pool, cleanup := setupAppendOnlyTest(t) defer cleanup() ctx := context.Background() // Append-only bedeutet: dieser Testeintrag kann NIE wieder geloescht // werden, auch nicht vom Test selbst. Eindeutiger Tenant-Slug pro Lauf, // damit wiederholte Testlaeufe sich nicht gegenseitig die Zaehlung // verfaelschen. tenantSlug := fmt.Sprintf("test_appendonly_%d", time.Now().UnixNano()) if err := log.Record(ctx, Event{ TenantSlug: tenantSlug, Actor: "alice", Action: "test.event", Target: "x", }); err != nil { t.Fatalf("record: %v", err) } _, err := pool.Exec(ctx, `UPDATE audit_events SET actor = 'mallory' WHERE tenant_slug = $1`, tenantSlug) if err == nil { t.Fatal("erwartet fehler bei UPDATE auf audit_events, habe nil") } _, err = pool.Exec(ctx, `DELETE FROM audit_events WHERE tenant_slug = $1`, tenantSlug) if err == nil { t.Fatal("erwartet fehler bei DELETE auf audit_events, habe nil") } count, err := log.CountByTenant(ctx, tenantSlug) if err != nil { t.Fatalf("count: %v", err) } if count != 1 { t.Fatalf("eintrag haette trotz fehlgeschlagener update/delete-versuche erhalten bleiben muessen, count=%d", count) } }