package store import ( "context" "testing" "time" ) // Tests der Kennzahlen für die Systemseite (sysinfo.go). Laufen mit // MUNIN_TEST_PG auch gegen PostgreSQL (siehe testStore). // TestInventory zählt Projekte, Einträge, Papierkorb, Fassungen und Keys — // Keys nach denselben Regeln wie die Seite "Zugänge". func TestInventory(t *testing.T) { s := testStore(t) ctx := context.Background() now := time.Now() empty, err := s.Inventory(ctx, now) if err != nil { t.Fatal(err) } if empty != (Inventory{}) { t.Errorf("leerer Workspace: %+v, erwartet nur Nullen", empty) } p := testProject(t, s) for _, slug := range []string{"a", "b", "c"} { if _, err := s.PutEntry(ctx, EntryWrite{ProjectID: p.ID, Slug: slug, Title: slug, Content: "x", Source: SourceHuman}); err != nil { t.Fatal(err) } } // Eine Änderung legt eine ältere Fassung ab. if _, err := s.PutEntry(ctx, EntryWrite{ProjectID: p.ID, Slug: "a", Title: "a", Content: "neu", Source: SourceHuman}); err != nil { t.Fatal(err) } if err := s.MoveEntryToTrash(ctx, p.ID, "c", "test"); err != nil { t.Fatal(err) } // Keys: einer frisch, einer läuft bald ab, einer zurückgezogen, einer // abgelaufen. Gültig sind die ersten beiden. if _, err := s.CreateAPIKey(ctx, p.ID, "frisch", "h1", "write"); err != nil { t.Fatal(err) } if _, err := s.CreateAPIKeyWithExpiry(ctx, p.ID, "bald", "h2", "read", now.Add(3*24*time.Hour)); err != nil { t.Fatal(err) } gone, err := s.CreateAPIKey(ctx, p.ID, "weg", "h3", "read") if err != nil { t.Fatal(err) } if err := s.RevokeAPIKey(ctx, gone.ID); err != nil { t.Fatal(err) } if _, err := s.CreateAPIKeyWithExpiry(ctx, p.ID, "abgelaufen", "h4", "read", now.Add(time.Hour)); err != nil { t.Fatal(err) } // Zwei Stunden später: "abgelaufen" ist es dann. b, err := s.Inventory(ctx, now.Add(2*time.Hour)) if err != nil { t.Fatal(err) } want := Inventory{Projects: 1, Entries: 2, Versions: 1, Trash: 1, Keys: 2, KeysExpiring: 1} if b != want { t.Errorf("Bestand = %+v\nerwartet %+v", b, want) } // 100 Tage später ist "frisch" nie benutzt worden und damit ungenutzt; // "bald" ist bis dahin abgelaufen und zählt gar nicht mehr. b, err = s.Inventory(ctx, now.Add(100*24*time.Hour)) if err != nil { t.Fatal(err) } if b.Keys != 1 || b.KeysUnused != 1 || b.KeysExpiring != 0 { t.Errorf("nach 100 Tagen: Keys %d, ungenutzt %d, laufen ab %d — erwartet 1, 1, 0", b.Keys, b.KeysUnused, b.KeysExpiring) } } // TestDiskUsage: Ein Workspace mit Inhalt belegt Platz. Wie viel genau, hängt // von der Datenbank ab — geprüft wird nur, dass die Zahl plausibel ist und // mit dem Inhalt wächst. func TestDiskUsage(t *testing.T) { s := testStore(t) ctx := context.Background() before, err := s.DiskUsage(ctx) if err != nil { t.Fatal(err) } if before.Data <= 0 { t.Fatalf("leerer Workspace belegt %d Bytes — die Tabellen allein brauchen Platz", before.Data) } p := testProject(t, s) content := make([]byte, 64<<10) for i := range content { content[i] = 'a' + byte(i%26) } for _, slug := range []string{"gross-1", "gross-2", "gross-3", "gross-4"} { if _, err := s.PutEntry(ctx, EntryWrite{ProjectID: p.ID, Slug: slug, Title: slug, Content: string(content), Source: SourceHuman}); err != nil { t.Fatal(err) } } after, err := s.DiskUsage(ctx) if err != nil { t.Fatal(err) } // Bei SQLite landet das Geschriebene zunächst in der WAL-Datei, bei // PostgreSQL in den Tabellen. Zusammen muss es mehr geworden sein. if after.Total() <= before.Total() { t.Errorf("Belegung vorher %d, nachher %d — sollte gewachsen sein", before.Total(), after.Total()) } } // TestActivity zählt Schreibvorgänge je Projekt, Tag und Weg sowie Suchen, // und lässt alles vor der Grenze weg. func TestActivity(t *testing.T) { s := testStore(t) ctx := context.Background() p := testProject(t, s) // Mit festen Zeitstempeln direkt eintragen — LogAudit und LogSearch // nehmen immer "jetzt". audit := func(ts, action, source string) { t.Helper() _, err := s.write.ExecContext(ctx, `INSERT INTO audit_events (id, ts, project_id, actor, action, source) VALUES (?, ?, ?, 'test', ?, ?)`, NewID(), ts, p.ID, action, source) if err != nil { t.Fatal(err) } } search := func(ts string) { t.Helper() _, err := s.write.ExecContext(ctx, `INSERT INTO search_log (ts, project_id, query, results) VALUES (?, ?, 'frage', 1)`, ts, p.ID) if err != nil { t.Fatal(err) } } audit("2026-10-01T08:00:00Z", ActionEntryWrite, SourceMCP) audit("2026-10-01T09:00:00Z", ActionEntryWrite, SourceMCP) audit("2026-10-01T10:00:00Z", ActionEntryDelete, SourceWeb) audit("2026-10-01T11:00:00Z", ActionLoginSuccess, SourceWeb) // kein Schreibvorgang audit("2026-10-02T08:00:00Z", ActionEntryWrite, SourceWeb) audit("2026-09-20T08:00:00Z", ActionEntryWrite, SourceMCP) // vor der Grenze search("2026-10-02T12:00:00Z") search("2026-10-02T13:00:00Z") search("2026-09-20T13:00:00Z") // vor der Grenze list, err := s.Activity(ctx, time.Date(2026, 9, 25, 0, 0, 0, 0, time.UTC)) if err != nil { t.Fatal(err) } per := map[string]DailyActivity{} for _, a := range list { if a.ProjectID != p.ID { t.Errorf("fremdes Projekt %q", a.ProjectID) } per[a.Tag] = a } if len(per) != 2 { t.Fatalf("%d Tage, erwartet 2: %+v", len(per), list) } if a := per["2026-10-01"]; a.AgentWrites != 2 || a.HumanWrites != 1 || a.Searches != 0 { t.Errorf("1.10.: %+v, erwartet 2 Agent, 1 Mensch, 0 Suchen", a) } if a := per["2026-10-02"]; a.AgentWrites != 0 || a.HumanWrites != 1 || a.Searches != 2 { t.Errorf("2.10.: %+v, erwartet 0 Agent, 1 Mensch, 2 Suchen", a) } } // TestSchemaVersion: Nach dem Öffnen steht der Workspace auf der neuesten // Migration, die das Programm mitbringt. func TestSchemaVersion(t *testing.T) { s := testStore(t) actual, expected, err := s.SchemaVersion(context.Background()) if err != nil { t.Fatal(err) } if expected == "" || actual != expected { t.Errorf("Schemastand ist %q, soll %q", actual, expected) } }