package store import ( "context" "testing" "time" ) // TestSearchStatsCounts prüft die Kennzahlen: alle Suchen, ohne Treffer, mit // Synonymen — und nur innerhalb des Zeitfensters und des Projekts. func TestSearchStatsCounts(t *testing.T) { s := testStore(t) ctx := context.Background() p := testProject(t, s) s.LogSearch(ctx, SearchLogEntry{ProjectID: p.ID, Query: "deployment", Results: 3}) s.LogSearch(ctx, SearchLogEntry{ProjectID: p.ID, Query: "release", Synonyms: []string{"rollout"}, Results: 0}) s.LogSearch(ctx, SearchLogEntry{ProjectID: p.ID, Query: "vpn", Results: 0}) // Außerhalb des Fensters: darf nicht mitzählen. old := time.Now().UTC().Add(-UsageWindow - time.Hour).Format(time.RFC3339Nano) if _, err := s.write.ExecContext(ctx, `INSERT INTO search_log (ts, project_id, query, results) VALUES (?, ?, 'alt', 0)`, old, p.ID); err != nil { t.Fatal(err) } st, err := s.SearchStatsFor(ctx, p.ID, time.Now()) if err != nil { t.Fatal(err) } if st.Total != 3 || st.Empty != 2 || st.WithSynonyms != 1 { t.Errorf("SearchStats = %+v, erwartet Total 3, Empty 2, WithSynonyms 1", st) } if st.EmptyPercent() != 67 || st.SynonymPercent() != 33 { t.Errorf("Anteile = %d %% / %d %%, erwartet 67 / 33", st.EmptyPercent(), st.SynonymPercent()) } } // TestSearchStatsEmpty prüft ein Projekt ohne Suchen: alles 0, kein Fehler. func TestSearchStatsEmpty(t *testing.T) { s := testStore(t) p := testProject(t, s) st, err := s.SearchStatsFor(context.Background(), p.ID, time.Now()) if err != nil { t.Fatal(err) } if st != (SearchStats{}) || st.EmptyPercent() != 0 { t.Errorf("SearchStats = %+v, erwartet nur Nullen", st) } } // TestEmptySearchesAggregates prüft, dass gleiche Anfragen ohne Treffer // zusammengefasst werden, unabhängig von Groß- und Kleinschreibung, und die // häufigste vorne steht. func TestEmptySearchesAggregates(t *testing.T) { s := testStore(t) ctx := context.Background() p := testProject(t, s) s.LogSearch(ctx, SearchLogEntry{ProjectID: p.ID, Query: "vpn zugang", Results: 0}) s.LogSearch(ctx, SearchLogEntry{ProjectID: p.ID, Query: "VPN Zugang", Results: 0}) s.LogSearch(ctx, SearchLogEntry{ProjectID: p.ID, Query: " vpn zugang ", Synonyms: []string{"remote"}, Results: 0}) s.LogSearch(ctx, SearchLogEntry{ProjectID: p.ID, Query: "drucker", Results: 0}) s.LogSearch(ctx, SearchLogEntry{ProjectID: p.ID, Query: "deployment", Results: 2}) // hat Treffer list, err := s.EmptySearches(ctx, p.ID, time.Now(), 10) if err != nil { t.Fatal(err) } if len(list) != 2 { t.Fatalf("%d Anfragen, erwartet 2: %+v", len(list), list) } if list[0].Count != 3 { t.Errorf("häufigste Anfrage = %+v, erwartet 3× vpn zugang", list[0]) } if list[0].Synonyms != "remote" { t.Errorf("Synonyme = %q, erwartet die der jüngsten Suche (remote)", list[0].Synonyms) } if list[1].Query != "drucker" { t.Errorf("zweite Anfrage = %q, erwartet drucker", list[1].Query) } } // TestPruneSearchLog prüft das Aufräumen nach Ablauf der Frist. func TestPruneSearchLog(t *testing.T) { s := testStore(t) ctx := context.Background() p := testProject(t, s) old := time.Now().UTC().Add(-SearchLogRetention - time.Hour).Format(time.RFC3339Nano) if _, err := s.write.ExecContext(ctx, `INSERT INTO search_log (ts, project_id, query, results) VALUES (?, ?, 'alt', 0)`, old, p.ID); err != nil { t.Fatal(err) } s.LogSearch(ctx, SearchLogEntry{ProjectID: p.ID, Query: "neu", Results: 0}) limit := time.Now().UTC().Add(-SearchLogRetention).Format(time.RFC3339Nano) n, err := s.PruneSearchLog(ctx, limit) if err != nil { t.Fatal(err) } if n != 1 { t.Errorf("%d Zeilen entfernt, erwartet 1", n) } } // TestSearchLogVanishesWithProject prüft den Fremdschlüssel: Anfragen // sind Inhalt des Projekts und gehen mit ihm. func TestSearchLogVanishesWithProject(t *testing.T) { s := testStore(t) ctx := context.Background() p := testProject(t, s) s.LogSearch(ctx, SearchLogEntry{ProjectID: p.ID, Query: "geheim", Results: 0}) if err := s.DeleteProject(ctx, p.ID); err != nil { t.Fatal(err) } var count int if err := s.read.QueryRowContext(ctx, `SELECT COUNT(*) FROM search_log`).Scan(&count); err != nil { t.Fatal(err) } if count != 0 { t.Errorf("%d Anfragen des gelöschten Projekts sind noch da", count) } }