Files
breakpilot-lehrer/edu-search-service/internal/api/handlers/handlers_test.go
Benjamin Boenisch 414e0f5ec0
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-school (push) Successful in 28s
CI / test-go-edu-search (push) Successful in 27s
CI / test-python-klausur (push) Successful in 1m45s
CI / test-python-agent-core (push) Successful in 16s
CI / test-nodejs-website (push) Successful in 21s
feat: edu-search-service migriert, voice-service/geo-service entfernt
- edu-search-service von breakpilot-pwa nach breakpilot-lehrer kopiert (ohne vendor)
- opensearch + edu-search-service in docker-compose.yml hinzugefuegt
- voice-service aus docker-compose.yml entfernt (jetzt in breakpilot-core)
- geo-service aus docker-compose.yml entfernt (nicht mehr benoetigt)
- CI/CD: edu-search-service zu Gitea Actions und Woodpecker hinzugefuegt
  (Go lint, test mit go mod download, build, SBOM)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 18:36:38 +01:00

646 lines
15 KiB
Go

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
}