- Added support for Typesense as a search engine alongside Meilisearch and PostgreSQL. - Updated configuration structure to include Typesense parameters in `Config` and `.env.example`. - Enhanced search handler and service to accommodate external search clients and filters. - Implemented new tests for external search clients and search service functionalities. - Updated project checklist to reflect completion of multi-index search features and contextual snippets.
185 lines
5.4 KiB
Go
185 lines
5.4 KiB
Go
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: "<mark>Hello</mark> World"},
|
|
{name: "multiple terms", text: "Meet Alice and Bob", q: "alice bob", want: "Meet <mark>Alice</mark> and <mark>Bob</mark>"},
|
|
{name: "word boundary only", text: "hellish hello", q: "hello", want: "hellish <mark>hello</mark>"},
|
|
}
|
|
|
|
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 <mark>Acme</mark> 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, "<mark>needle</mark>") {
|
|
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, "<mark>findme</mark>") {
|
|
t.Fatalf("expected highlighted findme with default maxLen, got %q", got)
|
|
}
|
|
})
|
|
}
|