package search import ( "strings" "testing" "time" "github.com/ultisuite/ulti-backend/internal/api/query" ) func TestNewExternalSearchClient(t *testing.T) { tests := []struct { name string engine string opts ServiceOptions wantOK bool }{ { name: "meilisearch with url", engine: "meilisearch", opts: ServiceOptions{MeilisearchURL: "http://localhost:7700"}, wantOK: true, }, { name: "meilisearch empty url", engine: "meilisearch", opts: ServiceOptions{MeilisearchURL: " "}, wantOK: false, }, { name: "typesense with url", engine: "typesense", opts: ServiceOptions{TypesenseURL: "http://localhost:8108"}, wantOK: true, }, { name: "typesense empty url", engine: "typesense", opts: ServiceOptions{TypesenseURL: ""}, wantOK: false, }, { name: "postgres engine", engine: "postgres", opts: ServiceOptions{MeilisearchURL: "http://localhost:7700"}, wantOK: false, }, { name: "unknown engine", engine: "elasticsearch", opts: ServiceOptions{TypesenseURL: "http://localhost:8108"}, wantOK: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := newExternalSearchClient(tt.engine, tt.opts) if tt.wantOK && got == nil { t.Fatal("expected non-nil client") } if !tt.wantOK && got != nil { t.Fatal("expected nil client") } }) } } func TestMapExternalHits(t *testing.T) { hits := []map[string]any{ { "id": "mail-42", "type": "mail", "title": "Invoice from Acme", "snippet": "Your Acme invoice is ready", }, } results := mapExternalHits(hits, "acme") if len(results) != 1 { t.Fatalf("len(results) = %d, want 1", len(results)) } r := results[0] if r.Type != "mail" { t.Fatalf("Type = %q, want mail", r.Type) } if r.ID != "mail-42" { t.Fatalf("ID = %q, want mail-42", r.ID) } if r.Title == "" { t.Fatal("Title is empty") } if r.Snippet == "" { t.Fatal("Snippet is empty") } if r.Score <= 0 { t.Fatalf("Score = %v, want positive fallback score", r.Score) } if !strings.Contains(r.Title, "") { t.Fatalf("Title missing highlight: %q", r.Title) } if !strings.Contains(r.Snippet, "") { t.Fatalf("Snippet missing highlight: %q", r.Snippet) } } func TestMapExternalHitsMinimalDefaults(t *testing.T) { hits := []map[string]any{ {"title": "Budget report"}, } results := mapExternalHits(hits, "budget") if len(results) != 1 { t.Fatalf("len(results) = %d, want 1", len(results)) } r := results[0] if r.Type != "mail" { t.Fatalf("Type = %q, want default mail", r.Type) } if r.ID != "mail:Budget report" { t.Fatalf("ID = %q, want mail:Budget report", r.ID) } if r.Snippet == "" { t.Fatal("Snippet is empty") } } func TestFinalizeExternalResultsDateFiltering(t *testing.T) { mid := time.Date(2024, 6, 15, 12, 0, 0, 0, time.UTC) from := time.Date(2024, 6, 1, 0, 0, 0, 0, time.UTC) to := time.Date(2024, 6, 30, 23, 59, 59, 0, time.UTC) results := []Result{ {ID: "before", Title: "Old mail", Snippet: "Old mail", Date: time.Date(2024, 5, 15, 12, 0, 0, 0, time.UTC), Score: 5}, {ID: "in-range", Title: "Current mail", Snippet: "Current mail", Date: mid, Score: 5}, {ID: "after", Title: "Future mail", Snippet: "Future mail", Date: time.Date(2024, 7, 15, 12, 0, 0, 0, time.UTC), Score: 5}, } params := query.ListParams{From: &from, To: &to} finalizeExternalResults(results, "mail", params) if results[0].Score >= 0 { t.Fatalf("before-range Score = %v, want negative", results[0].Score) } if results[1].Score <= 0 { t.Fatalf("in-range Score = %v, want positive with recency boost", results[1].Score) } if results[2].Score >= 0 { t.Fatalf("after-range Score = %v, want negative", results[2].Score) } } func TestFinalizeExternalResultsFallbackScore(t *testing.T) { results := []Result{ {ID: "1", Title: "Invoice details", Snippet: "Invoice details", Score: 0}, } finalizeExternalResults(results, "invoice", query.ListParams{}) if results[0].Score <= 0 { t.Fatalf("Score = %v, want positive fallback score", results[0].Score) } } func TestAsStringNil(t *testing.T) { if got := asString(nil); got != "" { t.Fatalf("asString(nil) = %q, want empty string", got) } }