127 lines
4.2 KiB
Go
127 lines
4.2 KiB
Go
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)
|
||
}
|
||
}
|