package service import ( "context" "encoding/json" "net/http" "net/http/httptest" "testing" ) func TestSemanticSearchService_Search(t *testing.T) { // Mock Ollama server ollamaServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/api/embeddings" { t.Errorf("expected /api/embeddings, got %s", r.URL.Path) } var req ollamaEmbedRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { t.Fatalf("failed to decode request: %v", err) } if req.Model != "nomic-embed-text" { t.Errorf("expected model nomic-embed-text, got %s", req.Model) } if req.Prompt == "" { t.Error("expected non-empty prompt") } // Return a 768-dim embedding (truncated for test) embedding := make([]float64, 768) embedding[0] = 0.1 embedding[1] = -0.2 w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(ollamaEmbedResponse{Embedding: embedding}) })) defer ollamaServer.Close() // Mock Qdrant server qdrantServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/collections/media/points/search" { t.Errorf("expected /collections/media/points/search, got %s", r.URL.Path) } var req qdrantSearchRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { t.Fatalf("failed to decode request: %v", err) } if req.Limit != 5 { t.Errorf("expected limit 5, got %d", req.Limit) } if !req.WithPayload { t.Error("expected WithPayload=true") } payload, _ := json.Marshal(qdrantPayload{ ID: 42, Title: "Blade Runner 2049", MediaType: "movie", Year: intPtr(2017), Overview: "A young blade runner's discovery of a long-buried secret leads him to track down former blade runner Rick Deckard.", }) resp := qdrantSearchResponse{ Result: []qdrantHit{ {ID: "42", Score: 0.8712, Payload: payload}, }, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) })) defer qdrantServer.Close() svc := NewSemanticSearchService(ollamaServer.URL, qdrantServer.URL) results, err := svc.Search(context.Background(), "sci-fi noir film", 5) if err != nil { t.Fatalf("Search failed: %v", err) } if len(results) != 1 { t.Fatalf("expected 1 result, got %d", len(results)) } if results[0].ID != 42 { t.Errorf("expected ID 42, got %d", results[0].ID) } if results[0].Title != "Blade Runner 2049" { t.Errorf("expected title 'Blade Runner 2049', got %s", results[0].Title) } if results[0].MediaType != "movie" { t.Errorf("expected media_type 'movie', got %s", results[0].MediaType) } if results[0].Score != 0.8712 { t.Errorf("expected score 0.8712, got %f", results[0].Score) } if results[0].Year == nil || *results[0].Year != 2017 { t.Error("expected year 2017") } } // Note: empty query validation happens at the API handler level (search.go), // not in the service. The service layer trusts its callers. func TestSemanticSearchService_embed_emptyQuery(t *testing.T) { ollamaServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Empty string is passed through to Ollama — Ollama may return an error w.WriteHeader(http.StatusBadRequest) })) defer ollamaServer.Close() svc := NewSemanticSearchService(ollamaServer.URL, "http://localhost:6333") _, err := svc.embed(context.Background(), "") if err == nil { t.Error("expected error for empty query passed to Ollama") } } func TestSemanticSearchService_qdrantError(t *testing.T) { ollamaServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { embedding := make([]float64, 768) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(ollamaEmbedResponse{Embedding: embedding}) })) defer ollamaServer.Close() qdrantServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusServiceUnavailable) })) defer qdrantServer.Close() svc := NewSemanticSearchService(ollamaServer.URL, qdrantServer.URL) _, err := svc.Search(context.Background(), "test query", 5) if err == nil { t.Fatal("expected error when Qdrant is unavailable") } } func TestSemanticSearchService_ollamaError(t *testing.T) { ollamaServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusServiceUnavailable) })) defer ollamaServer.Close() svc := NewSemanticSearchService(ollamaServer.URL, "http://localhost:6333") _, err := svc.Search(context.Background(), "test query", 5) if err == nil { t.Fatal("expected error when Ollama is unavailable") } } func intPtr(i int) *int { return &i }