package handlers import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "github.com/gin-gonic/gin" ) func init() { gin.SetMode(gin.TestMode) } // setupTestRouter creates a test router with the handler func setupTestRouter(h *Handler, apiKey string) *gin.Engine { router := gin.New() SetupRoutes(router, h, apiKey) return router } // setupTestSeedStore creates a test seed store func setupTestSeedStore(t *testing.T) string { t.Helper() dir := t.TempDir() // Initialize global seed store err := InitSeedStore(dir) if err != nil { t.Fatalf("Failed to initialize seed store: %v", err) } return dir } func TestHealthEndpoint(t *testing.T) { // Health endpoint requires indexClient for health check // This test verifies the route is set up correctly // A full integration test would need a mock OpenSearch client t.Skip("Skipping: requires mock indexer client for full test") } func TestAuthMiddleware_NoAuth(t *testing.T) { h := &Handler{} router := setupTestRouter(h, "test-api-key") // Request without auth header req, _ := http.NewRequest("POST", "/v1/search", bytes.NewBufferString(`{"q":"test"}`)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusUnauthorized { t.Errorf("Expected status 401, got %d", w.Code) } } func TestAuthMiddleware_InvalidFormat(t *testing.T) { h := &Handler{} router := setupTestRouter(h, "test-api-key") // Request with wrong auth format req, _ := http.NewRequest("POST", "/v1/search", bytes.NewBufferString(`{"q":"test"}`)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Basic dGVzdDp0ZXN0") // Basic auth instead of Bearer w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusUnauthorized { t.Errorf("Expected status 401, got %d", w.Code) } } func TestAuthMiddleware_InvalidKey(t *testing.T) { h := &Handler{} router := setupTestRouter(h, "test-api-key") // Request with wrong API key req, _ := http.NewRequest("POST", "/v1/search", bytes.NewBufferString(`{"q":"test"}`)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer wrong-key") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusUnauthorized { t.Errorf("Expected status 401, got %d", w.Code) } } func TestAuthMiddleware_ValidKey(t *testing.T) { h := &Handler{} router := setupTestRouter(h, "test-api-key") // Request with correct API key (search will fail due to no search service, but auth should pass) req, _ := http.NewRequest("GET", "/v1/document?doc_id=test", nil) req.Header.Set("Authorization", "Bearer test-api-key") w := httptest.NewRecorder() router.ServeHTTP(w, req) // Auth should pass, endpoint returns 501 (not implemented) if w.Code == http.StatusUnauthorized { t.Error("Expected auth to pass, got 401") } } func TestAuthMiddleware_HealthNoAuth(t *testing.T) { // Health endpoint requires indexClient for health check // Skipping because route calls h.indexClient.Health() which panics with nil t.Skip("Skipping: requires mock indexer client for full test") } func TestGetDocument_MissingDocID(t *testing.T) { h := &Handler{} router := setupTestRouter(h, "test-key") req, _ := http.NewRequest("GET", "/v1/document", nil) req.Header.Set("Authorization", "Bearer test-key") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Errorf("Expected status 400, got %d", w.Code) } } // Admin Handler Tests func TestSeedStore_InitAndLoad(t *testing.T) { dir := t.TempDir() // First initialization should create default seeds err := InitSeedStore(dir) if err != nil { t.Fatalf("InitSeedStore failed: %v", err) } // Check that seeds file was created seedsFile := filepath.Join(dir, "seeds.json") if _, err := os.Stat(seedsFile); os.IsNotExist(err) { t.Error("seeds.json was not created") } // Check that default seeds were loaded seeds := seedStore.GetAllSeeds() if len(seeds) == 0 { t.Error("Expected default seeds to be loaded") } } func TestSeedStore_CreateSeed(t *testing.T) { setupTestSeedStore(t) newSeed := SeedURL{ URL: "https://test.example.com", Name: "Test Seed", Category: "test", Description: "A test seed", TrustBoost: 0.5, Enabled: true, } created, err := seedStore.CreateSeed(newSeed) if err != nil { t.Fatalf("CreateSeed failed: %v", err) } if created.ID == "" { t.Error("Expected generated ID") } if created.URL != newSeed.URL { t.Errorf("Expected URL %q, got %q", newSeed.URL, created.URL) } if created.CreatedAt.IsZero() { t.Error("Expected CreatedAt to be set") } } func TestSeedStore_GetSeed(t *testing.T) { setupTestSeedStore(t) // Create a seed first newSeed := SeedURL{ URL: "https://get-test.example.com", Name: "Get Test", Category: "test", } created, _ := seedStore.CreateSeed(newSeed) // Get the seed retrieved, found := seedStore.GetSeed(created.ID) if !found { t.Fatal("Seed not found") } if retrieved.URL != newSeed.URL { t.Errorf("Expected URL %q, got %q", newSeed.URL, retrieved.URL) } } func TestSeedStore_GetSeed_NotFound(t *testing.T) { setupTestSeedStore(t) _, found := seedStore.GetSeed("nonexistent-id") if found { t.Error("Expected seed not to be found") } } func TestSeedStore_UpdateSeed(t *testing.T) { setupTestSeedStore(t) // Create a seed first original := SeedURL{ URL: "https://update-test.example.com", Name: "Original Name", Category: "test", Enabled: true, } created, _ := seedStore.CreateSeed(original) // Update the seed updates := SeedURL{ Name: "Updated Name", TrustBoost: 0.75, Enabled: false, } updated, found, err := seedStore.UpdateSeed(created.ID, updates) if err != nil { t.Fatalf("UpdateSeed failed: %v", err) } if !found { t.Fatal("Seed not found for update") } if updated.Name != "Updated Name" { t.Errorf("Expected name 'Updated Name', got %q", updated.Name) } if updated.TrustBoost != 0.75 { t.Errorf("Expected TrustBoost 0.75, got %f", updated.TrustBoost) } if updated.Enabled != false { t.Error("Expected Enabled to be false") } // URL should remain unchanged since we didn't provide it if updated.URL != original.URL { t.Errorf("URL should remain unchanged, expected %q, got %q", original.URL, updated.URL) } } func TestSeedStore_UpdateSeed_NotFound(t *testing.T) { setupTestSeedStore(t) updates := SeedURL{Name: "New Name"} _, found, err := seedStore.UpdateSeed("nonexistent-id", updates) if err != nil { t.Fatalf("Unexpected error: %v", err) } if found { t.Error("Expected seed not to be found") } } func TestSeedStore_DeleteSeed(t *testing.T) { setupTestSeedStore(t) // Create a seed first newSeed := SeedURL{ URL: "https://delete-test.example.com", Name: "Delete Test", Category: "test", } created, _ := seedStore.CreateSeed(newSeed) // Delete the seed deleted := seedStore.DeleteSeed(created.ID) if !deleted { t.Error("Expected delete to succeed") } // Verify it's gone _, found := seedStore.GetSeed(created.ID) if found { t.Error("Seed should have been deleted") } } func TestSeedStore_DeleteSeed_NotFound(t *testing.T) { setupTestSeedStore(t) deleted := seedStore.DeleteSeed("nonexistent-id") if deleted { t.Error("Expected delete to return false for nonexistent seed") } } func TestSeedStore_Persistence(t *testing.T) { dir := t.TempDir() // Create and populate seed store err := InitSeedStore(dir) if err != nil { t.Fatal(err) } newSeed := SeedURL{ URL: "https://persist-test.example.com", Name: "Persistence Test", Category: "test", } created, err := seedStore.CreateSeed(newSeed) if err != nil { t.Fatal(err) } // Re-initialize from the same directory seedStore = nil err = InitSeedStore(dir) if err != nil { t.Fatal(err) } // Check if the seed persisted retrieved, found := seedStore.GetSeed(created.ID) if !found { t.Error("Seed should have persisted") } if retrieved.URL != newSeed.URL { t.Errorf("Persisted seed URL mismatch: expected %q, got %q", newSeed.URL, retrieved.URL) } } func TestAdminGetSeeds(t *testing.T) { dir := setupTestSeedStore(t) h := &Handler{} router := gin.New() SetupRoutes(router, h, "test-key") // Initialize seed store for the test InitSeedStore(dir) req, _ := http.NewRequest("GET", "/v1/admin/seeds", nil) req.Header.Set("Authorization", "Bearer test-key") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("Expected status 200, got %d", w.Code) } var seeds []SeedURL if err := json.Unmarshal(w.Body.Bytes(), &seeds); err != nil { t.Fatalf("Failed to parse response: %v", err) } // Should have default seeds if len(seeds) == 0 { t.Error("Expected seeds to be returned") } } func TestAdminCreateSeed(t *testing.T) { dir := setupTestSeedStore(t) h := &Handler{} router := gin.New() SetupRoutes(router, h, "test-key") InitSeedStore(dir) newSeed := map[string]interface{}{ "url": "https://new-seed.example.com", "name": "New Seed", "category": "test", "description": "Test description", "trustBoost": 0.5, "enabled": true, } body, _ := json.Marshal(newSeed) req, _ := http.NewRequest("POST", "/v1/admin/seeds", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer test-key") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusCreated { t.Errorf("Expected status 201, got %d: %s", w.Code, w.Body.String()) } var created SeedURL if err := json.Unmarshal(w.Body.Bytes(), &created); err != nil { t.Fatalf("Failed to parse response: %v", err) } if created.ID == "" { t.Error("Expected ID to be generated") } if created.URL != "https://new-seed.example.com" { t.Errorf("Expected URL to match, got %q", created.URL) } } func TestAdminCreateSeed_MissingURL(t *testing.T) { dir := setupTestSeedStore(t) h := &Handler{} router := gin.New() SetupRoutes(router, h, "test-key") InitSeedStore(dir) newSeed := map[string]interface{}{ "name": "No URL Seed", "category": "test", } body, _ := json.Marshal(newSeed) req, _ := http.NewRequest("POST", "/v1/admin/seeds", bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer test-key") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Errorf("Expected status 400 for missing URL, got %d", w.Code) } } func TestAdminUpdateSeed(t *testing.T) { dir := setupTestSeedStore(t) h := &Handler{} router := gin.New() SetupRoutes(router, h, "test-key") InitSeedStore(dir) // Create a seed first newSeed := SeedURL{ URL: "https://update-api-test.example.com", Name: "API Update Test", Category: "test", } created, _ := seedStore.CreateSeed(newSeed) // Update via API updates := map[string]interface{}{ "name": "Updated via API", "trustBoost": 0.8, } body, _ := json.Marshal(updates) req, _ := http.NewRequest("PUT", "/v1/admin/seeds/"+created.ID, bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer test-key") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("Expected status 200, got %d: %s", w.Code, w.Body.String()) } var updated SeedURL if err := json.Unmarshal(w.Body.Bytes(), &updated); err != nil { t.Fatalf("Failed to parse response: %v", err) } if updated.Name != "Updated via API" { t.Errorf("Expected name 'Updated via API', got %q", updated.Name) } } func TestAdminDeleteSeed(t *testing.T) { dir := setupTestSeedStore(t) h := &Handler{} router := gin.New() SetupRoutes(router, h, "test-key") InitSeedStore(dir) // Create a seed first newSeed := SeedURL{ URL: "https://delete-api-test.example.com", Name: "API Delete Test", Category: "test", } created, _ := seedStore.CreateSeed(newSeed) // Delete via API req, _ := http.NewRequest("DELETE", "/v1/admin/seeds/"+created.ID, nil) req.Header.Set("Authorization", "Bearer test-key") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("Expected status 200, got %d", w.Code) } // Verify it's deleted _, found := seedStore.GetSeed(created.ID) if found { t.Error("Seed should have been deleted") } } func TestAdminDeleteSeed_NotFound(t *testing.T) { dir := setupTestSeedStore(t) h := &Handler{} router := gin.New() SetupRoutes(router, h, "test-key") InitSeedStore(dir) req, _ := http.NewRequest("DELETE", "/v1/admin/seeds/nonexistent-id", nil) req.Header.Set("Authorization", "Bearer test-key") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Errorf("Expected status 404, got %d", w.Code) } } func TestAdminGetStats(t *testing.T) { dir := setupTestSeedStore(t) h := &Handler{} router := gin.New() SetupRoutes(router, h, "test-key") InitSeedStore(dir) req, _ := http.NewRequest("GET", "/v1/admin/stats", nil) req.Header.Set("Authorization", "Bearer test-key") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("Expected status 200, got %d", w.Code) } var stats CrawlStats if err := json.Unmarshal(w.Body.Bytes(), &stats); err != nil { t.Fatalf("Failed to parse response: %v", err) } // Check that stats structure is populated if stats.CrawlStatus == "" { t.Error("Expected CrawlStatus to be set") } if stats.DocumentsPerCategory == nil { t.Error("Expected DocumentsPerCategory to be set") } } func TestAdminStartCrawl(t *testing.T) { dir := setupTestSeedStore(t) h := &Handler{} router := gin.New() SetupRoutes(router, h, "test-key") InitSeedStore(dir) // Reset crawl status crawlStatus = "idle" req, _ := http.NewRequest("POST", "/v1/admin/crawl/start", nil) req.Header.Set("Authorization", "Bearer test-key") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusAccepted { t.Errorf("Expected status 202, got %d: %s", w.Code, w.Body.String()) } var response map[string]interface{} if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("Failed to parse response: %v", err) } if response["status"] != "started" { t.Errorf("Expected status 'started', got %v", response["status"]) } } func TestAdminStartCrawl_AlreadyRunning(t *testing.T) { dir := setupTestSeedStore(t) h := &Handler{} router := gin.New() SetupRoutes(router, h, "test-key") InitSeedStore(dir) // Set crawl status to running crawlStatus = "running" req, _ := http.NewRequest("POST", "/v1/admin/crawl/start", nil) req.Header.Set("Authorization", "Bearer test-key") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusConflict { t.Errorf("Expected status 409, got %d", w.Code) } // Reset for other tests crawlStatus = "idle" } func TestConcurrentSeedAccess(t *testing.T) { setupTestSeedStore(t) // Test concurrent reads and writes done := make(chan bool, 10) // Concurrent readers for i := 0; i < 5; i++ { go func() { seedStore.GetAllSeeds() done <- true }() } // Concurrent writers for i := 0; i < 5; i++ { go func(n int) { seed := SeedURL{ URL: "https://concurrent-" + string(rune('A'+n)) + ".example.com", Name: "Concurrent Test", Category: "test", } seedStore.CreateSeed(seed) done <- true }(i) } // Wait for all goroutines for i := 0; i < 10; i++ { <-done } // If we get here without deadlock or race, test passes }