ultisuite-backend/internal/search/external_clients_test.go
R3D347HR4Y 0435e27ce6 Enhance search functionality with multi-engine support and configuration updates
- 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.
2026-05-22 19:14:27 +02:00

171 lines
4.2 KiB
Go

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, "<mark>") {
t.Fatalf("Title missing highlight: %q", r.Title)
}
if !strings.Contains(r.Snippet, "<mark>") {
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)
}
}