package store import ( "context" "testing" "time" ) // TestSearchStatsZaehlt prüft die Kennzahlen: alle Suchen, ohne Treffer, mit // Synonymen — und nur innerhalb des Zeitfensters und des Projekts. func TestSearchStatsZaehlt(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. alt := time.Now().UTC().Add(-UsageWindow - time.Hour).Format(time.RFC3339Nano) if _, err := s.schreiben.ExecContext(ctx, `INSERT INTO search_log (ts, project_id, query, results) VALUES (?, ?, 'alt', 0)`, alt, 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()) } } // TestSearchStatsLeer prüft ein Projekt ohne Suchen: alles 0, kein Fehler. func TestSearchStatsLeer(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) } } // TestEmptySearchesFasstZusammen prüft, dass gleiche Anfragen ohne Treffer // zusammengefasst werden, unabhängig von Groß- und Kleinschreibung, und die // häufigste vorne steht. func TestEmptySearchesFasstZusammen(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 liste, err := s.EmptySearches(ctx, p.ID, time.Now(), 10) if err != nil { t.Fatal(err) } if len(liste) != 2 { t.Fatalf("%d Anfragen, erwartet 2: %+v", len(liste), liste) } if liste[0].Count != 3 { t.Errorf("häufigste Anfrage = %+v, erwartet 3× vpn zugang", liste[0]) } if liste[0].Synonyms != "remote" { t.Errorf("Synonyme = %q, erwartet die der jüngsten Suche (remote)", liste[0].Synonyms) } if liste[1].Query != "drucker" { t.Errorf("zweite Anfrage = %q, erwartet drucker", liste[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) alt := time.Now().UTC().Add(-SearchLogRetention - time.Hour).Format(time.RFC3339Nano) if _, err := s.schreiben.ExecContext(ctx, `INSERT INTO search_log (ts, project_id, query, results) VALUES (?, ?, 'alt', 0)`, alt, p.ID); err != nil { t.Fatal(err) } s.LogSearch(ctx, SearchLogEntry{ProjectID: p.ID, Query: "neu", Results: 0}) grenze := time.Now().UTC().Add(-SearchLogRetention).Format(time.RFC3339Nano) n, err := s.PruneSearchLog(ctx, grenze) if err != nil { t.Fatal(err) } if n != 1 { t.Errorf("%d Zeilen entfernt, erwartet 1", n) } } // TestSuchprotokollVerschwindetMitProjekt prüft den Fremdschlüssel: Anfragen // sind Inhalt des Projekts und gehen mit ihm. func TestSuchprotokollVerschwindetMitProjekt(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 anzahl int if err := s.lesen.QueryRowContext(ctx, `SELECT COUNT(*) FROM search_log`).Scan(&anzahl); err != nil { t.Fatal(err) } if anzahl != 0 { t.Errorf("%d Anfragen des gelöschten Projekts sind noch da", anzahl) } }