package search import ( "strings" "testing" ) func TestNormalizeEngine(t *testing.T) { tests := []struct { name string raw string want string }{ {name: "postgres explicit", raw: "postgres", want: "postgres"}, {name: "meilisearch", raw: "meilisearch", want: "meilisearch"}, {name: "meilisearch mixed case", raw: "MeiliSearch", want: "meilisearch"}, {name: "meilisearch trimmed", raw: " meilisearch ", want: "meilisearch"}, {name: "typesense", raw: "typesense", want: "typesense"}, {name: "typesense mixed case", raw: "TypeSense", want: "typesense"}, {name: "empty defaults postgres", raw: "", want: "postgres"}, {name: "unknown defaults postgres", raw: "elasticsearch", want: "postgres"}, {name: "whitespace defaults postgres", raw: " ", want: "postgres"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := normalizeEngine(tt.raw); got != tt.want { t.Fatalf("normalizeEngine(%q) = %q, want %q", tt.raw, got, tt.want) } }) } } func TestParseTypes(t *testing.T) { tests := []struct { name string raw string want []string }{ { name: "empty defaults all", raw: "", want: []string{"mail", "contacts", "files", "events"}, }, { name: "whitespace defaults all", raw: " ", want: []string{"mail", "contacts", "files", "events"}, }, { name: "explicit subset", raw: "mail,contacts", want: []string{"mail", "contacts"}, }, { name: "deduplicates", raw: "mail,mail,contacts,contacts", want: []string{"mail", "contacts"}, }, { name: "trims and skips empty parts", raw: " mail , , contacts ", want: []string{"mail", "contacts"}, }, { name: "preserves order", raw: "events,mail,contacts", want: []string{"events", "mail", "contacts"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := parseTypes(tt.raw) if len(got) != len(tt.want) { t.Fatalf("parseTypes(%q) = %v, want %v", tt.raw, got, tt.want) } for i := range tt.want { if got[i] != tt.want[i] { t.Fatalf("parseTypes(%q)[%d] = %q, want %q (full: %v)", tt.raw, i, got[i], tt.want[i], got) } } }) } } func TestSplitSearchTerms(t *testing.T) { tests := []struct { name string input string want []string }{ {name: "empty", input: "", want: nil}, {name: "whitespace", input: " ", want: nil}, {name: "simple words", input: "hello world", want: []string{"hello", "world"}}, {name: "strips punctuation", input: "hello, world!", want: []string{"hello", "world"}}, {name: "deduplicates", input: "foo foo bar", want: []string{"foo", "bar"}}, {name: "mixed punctuation", input: "can't re-read", want: []string{"can", "t", "re", "read"}}, {name: "case normalized", input: "Hello HELLO", want: []string{"hello"}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := splitSearchTerms(tt.input) if len(got) == 0 && len(tt.want) == 0 { return } if len(got) != len(tt.want) { t.Fatalf("splitSearchTerms(%q) = %v, want %v", tt.input, got, tt.want) } for i := range tt.want { if got[i] != tt.want[i] { t.Fatalf("splitSearchTerms(%q)[%d] = %q, want %q (full: %v)", tt.input, i, got[i], tt.want[i], got) } } }) } } func TestHighlightTerms(t *testing.T) { tests := []struct { name string text string q string want string }{ {name: "empty text", text: "", q: "hello", want: ""}, {name: "empty query unchanged", text: "Hello World", q: "", want: "Hello World"}, {name: "case insensitive preserves match casing", text: "Hello World", q: "hello", want: "Hello World"}, {name: "multiple terms", text: "Meet Alice and Bob", q: "alice bob", want: "Meet Alice and Bob"}, {name: "word boundary only", text: "hellish hello", q: "hello", want: "hellish hello"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := highlightTerms(tt.text, tt.q); got != tt.want { t.Fatalf("highlightTerms(%q, %q) = %q, want %q", tt.text, tt.q, got, tt.want) } }) } } func TestContextualSnippet(t *testing.T) { t.Run("highlights match in short text", func(t *testing.T) { got := contextualSnippet("Invoice from Acme Corp", "acme", 180) want := "Invoice from Acme Corp" if got != want { t.Fatalf("contextualSnippet short = %q, want %q", got, want) } }) t.Run("trims long content around match", func(t *testing.T) { prefix := strings.Repeat("word ", 30) text := prefix + "needle here " + strings.Repeat("tail ", 30) got := contextualSnippet(text, "needle", 40) if !strings.Contains(got, "needle") { t.Fatalf("expected highlighted needle, got %q", got) } if !strings.HasPrefix(got, "...") { t.Fatalf("expected leading ellipsis for trimmed snippet, got %q", got) } if !strings.HasSuffix(got, "...") { t.Fatalf("expected trailing ellipsis for trimmed snippet, got %q", got) } if len(got) > 80 { t.Fatalf("snippet too long (%d chars): %q", len(got), got) } }) t.Run("empty text", func(t *testing.T) { if got := contextualSnippet(" ", "foo", 100); got != "" { t.Fatalf("contextualSnippet empty = %q, want empty", got) } }) t.Run("default maxLen when zero", func(t *testing.T) { long := strings.Repeat("x", 300) + " findme" got := contextualSnippet(long, "findme", 0) if !strings.Contains(got, "findme") { t.Fatalf("expected highlighted findme with default maxLen, got %q", got) } }) }