package handlers import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "github.com/breakpilot/edu-search-service/internal/orchestrator" "github.com/gin-gonic/gin" "github.com/google/uuid" ) // MockAudienceRepository implements orchestrator.AudienceRepository for testing type MockAudienceRepository struct { audiences []orchestrator.Audience exports []orchestrator.AudienceExport members []orchestrator.AudienceMember } func NewMockAudienceRepository() *MockAudienceRepository { return &MockAudienceRepository{ audiences: make([]orchestrator.Audience, 0), exports: make([]orchestrator.AudienceExport, 0), members: make([]orchestrator.AudienceMember, 0), } } func (m *MockAudienceRepository) CreateAudience(ctx context.Context, audience *orchestrator.Audience) error { audience.ID = uuid.New() audience.CreatedAt = time.Now() audience.UpdatedAt = time.Now() m.audiences = append(m.audiences, *audience) return nil } func (m *MockAudienceRepository) GetAudience(ctx context.Context, id uuid.UUID) (*orchestrator.Audience, error) { for i := range m.audiences { if m.audiences[i].ID == id { return &m.audiences[i], nil } } return nil, context.DeadlineExceeded // simulate not found } func (m *MockAudienceRepository) ListAudiences(ctx context.Context, activeOnly bool) ([]orchestrator.Audience, error) { if activeOnly { var active []orchestrator.Audience for _, a := range m.audiences { if a.IsActive { active = append(active, a) } } return active, nil } return m.audiences, nil } func (m *MockAudienceRepository) UpdateAudience(ctx context.Context, audience *orchestrator.Audience) error { for i := range m.audiences { if m.audiences[i].ID == audience.ID { m.audiences[i].Name = audience.Name m.audiences[i].Description = audience.Description m.audiences[i].Filters = audience.Filters m.audiences[i].IsActive = audience.IsActive m.audiences[i].UpdatedAt = time.Now() audience.UpdatedAt = m.audiences[i].UpdatedAt return nil } } return nil } func (m *MockAudienceRepository) DeleteAudience(ctx context.Context, id uuid.UUID) error { for i := range m.audiences { if m.audiences[i].ID == id { m.audiences[i].IsActive = false return nil } } return nil } func (m *MockAudienceRepository) GetAudienceMembers(ctx context.Context, id uuid.UUID, limit, offset int) ([]orchestrator.AudienceMember, int, error) { // Return mock members if len(m.members) == 0 { m.members = []orchestrator.AudienceMember{ { ID: uuid.New(), Name: "Prof. Dr. Test Person", Email: "test@university.de", Position: "professor", University: "Test Universität", Department: "Informatik", SubjectArea: "Informatik", PublicationCount: 42, }, { ID: uuid.New(), Name: "Dr. Another Person", Email: "another@university.de", Position: "researcher", University: "Test Universität", Department: "Mathematik", SubjectArea: "Mathematik", PublicationCount: 15, }, } } total := len(m.members) if offset >= total { return []orchestrator.AudienceMember{}, total, nil } end := offset + limit if end > total { end = total } return m.members[offset:end], total, nil } func (m *MockAudienceRepository) UpdateAudienceCount(ctx context.Context, id uuid.UUID) (int, error) { count := len(m.members) for i := range m.audiences { if m.audiences[i].ID == id { m.audiences[i].MemberCount = count now := time.Now() m.audiences[i].LastCountUpdate = &now } } return count, nil } func (m *MockAudienceRepository) CreateExport(ctx context.Context, export *orchestrator.AudienceExport) error { export.ID = uuid.New() export.CreatedAt = time.Now() m.exports = append(m.exports, *export) return nil } func (m *MockAudienceRepository) ListExports(ctx context.Context, audienceID uuid.UUID) ([]orchestrator.AudienceExport, error) { var exports []orchestrator.AudienceExport for _, e := range m.exports { if e.AudienceID == audienceID { exports = append(exports, e) } } return exports, nil } func setupAudienceRouter(repo *MockAudienceRepository) *gin.Engine { gin.SetMode(gin.TestMode) router := gin.New() handler := NewAudienceHandler(repo) v1 := router.Group("/v1") SetupAudienceRoutes(v1, handler) return router } func TestAudienceHandler_ListAudiences_Empty(t *testing.T) { repo := NewMockAudienceRepository() router := setupAudienceRouter(repo) req := httptest.NewRequest(http.MethodGet, "/v1/audiences", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("Expected status %d, got %d", http.StatusOK, w.Code) } var response struct { Audiences []orchestrator.Audience `json:"audiences"` Count int `json:"count"` } if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("Failed to unmarshal response: %v", err) } if response.Count != 0 { t.Errorf("Expected 0 audiences, got %d", response.Count) } } func TestAudienceHandler_CreateAudience(t *testing.T) { repo := NewMockAudienceRepository() router := setupAudienceRouter(repo) body := CreateAudienceRequest{ Name: "Test Audience", Description: "A test audience for professors", Filters: orchestrator.AudienceFilters{ PositionTypes: []string{"professor"}, States: []string{"BW", "BY"}, }, CreatedBy: "test-admin", } bodyJSON, _ := json.Marshal(body) req := httptest.NewRequest(http.MethodPost, "/v1/audiences", bytes.NewBuffer(bodyJSON)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusCreated { t.Errorf("Expected status %d, got %d: %s", http.StatusCreated, w.Code, w.Body.String()) } var response orchestrator.Audience if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("Failed to unmarshal response: %v", err) } if response.Name != "Test Audience" { t.Errorf("Expected name 'Test Audience', got '%s'", response.Name) } if !response.IsActive { t.Errorf("Expected audience to be active") } if len(repo.audiences) != 1 { t.Errorf("Expected 1 audience in repo, got %d", len(repo.audiences)) } } func TestAudienceHandler_CreateAudience_InvalidJSON(t *testing.T) { repo := NewMockAudienceRepository() router := setupAudienceRouter(repo) req := httptest.NewRequest(http.MethodPost, "/v1/audiences", bytes.NewBuffer([]byte("invalid json"))) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Errorf("Expected status %d, got %d", http.StatusBadRequest, w.Code) } } func TestAudienceHandler_CreateAudience_MissingName(t *testing.T) { repo := NewMockAudienceRepository() router := setupAudienceRouter(repo) body := map[string]interface{}{ "description": "Missing name field", } bodyJSON, _ := json.Marshal(body) req := httptest.NewRequest(http.MethodPost, "/v1/audiences", bytes.NewBuffer(bodyJSON)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Errorf("Expected status %d, got %d", http.StatusBadRequest, w.Code) } } func TestAudienceHandler_GetAudience(t *testing.T) { repo := NewMockAudienceRepository() router := setupAudienceRouter(repo) // Create an audience first audience := orchestrator.Audience{ ID: uuid.New(), Name: "Test Audience", Description: "Test description", IsActive: true, CreatedAt: time.Now(), UpdatedAt: time.Now(), } repo.audiences = append(repo.audiences, audience) req := httptest.NewRequest(http.MethodGet, "/v1/audiences/"+audience.ID.String(), nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("Expected status %d, got %d: %s", http.StatusOK, w.Code, w.Body.String()) } var response orchestrator.Audience if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("Failed to unmarshal response: %v", err) } if response.Name != "Test Audience" { t.Errorf("Expected name 'Test Audience', got '%s'", response.Name) } } func TestAudienceHandler_GetAudience_InvalidID(t *testing.T) { repo := NewMockAudienceRepository() router := setupAudienceRouter(repo) req := httptest.NewRequest(http.MethodGet, "/v1/audiences/invalid-uuid", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Errorf("Expected status %d, got %d", http.StatusBadRequest, w.Code) } } func TestAudienceHandler_GetAudience_NotFound(t *testing.T) { repo := NewMockAudienceRepository() router := setupAudienceRouter(repo) req := httptest.NewRequest(http.MethodGet, "/v1/audiences/"+uuid.New().String(), nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Errorf("Expected status %d, got %d", http.StatusNotFound, w.Code) } } func TestAudienceHandler_UpdateAudience(t *testing.T) { repo := NewMockAudienceRepository() router := setupAudienceRouter(repo) // Create an audience first audience := orchestrator.Audience{ ID: uuid.New(), Name: "Old Name", Description: "Old description", IsActive: true, CreatedAt: time.Now(), UpdatedAt: time.Now(), } repo.audiences = append(repo.audiences, audience) body := UpdateAudienceRequest{ Name: "New Name", Description: "New description", IsActive: true, } bodyJSON, _ := json.Marshal(body) req := httptest.NewRequest(http.MethodPut, "/v1/audiences/"+audience.ID.String(), bytes.NewBuffer(bodyJSON)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("Expected status %d, got %d: %s", http.StatusOK, w.Code, w.Body.String()) } // Verify the update if repo.audiences[0].Name != "New Name" { t.Errorf("Expected name 'New Name', got '%s'", repo.audiences[0].Name) } } func TestAudienceHandler_DeleteAudience(t *testing.T) { repo := NewMockAudienceRepository() router := setupAudienceRouter(repo) // Create an audience first audience := orchestrator.Audience{ ID: uuid.New(), Name: "To Delete", IsActive: true, CreatedAt: time.Now(), UpdatedAt: time.Now(), } repo.audiences = append(repo.audiences, audience) req := httptest.NewRequest(http.MethodDelete, "/v1/audiences/"+audience.ID.String(), nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("Expected status %d, got %d", http.StatusOK, w.Code) } // Verify soft delete if repo.audiences[0].IsActive { t.Errorf("Expected audience to be inactive after delete") } } func TestAudienceHandler_GetAudienceMembers(t *testing.T) { repo := NewMockAudienceRepository() router := setupAudienceRouter(repo) // Create an audience first audience := orchestrator.Audience{ ID: uuid.New(), Name: "Test Audience", IsActive: true, CreatedAt: time.Now(), UpdatedAt: time.Now(), } repo.audiences = append(repo.audiences, audience) req := httptest.NewRequest(http.MethodGet, "/v1/audiences/"+audience.ID.String()+"/members", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("Expected status %d, got %d: %s", http.StatusOK, w.Code, w.Body.String()) } var response struct { Members []orchestrator.AudienceMember `json:"members"` Count int `json:"count"` TotalCount int `json:"total_count"` } if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("Failed to unmarshal response: %v", err) } if response.TotalCount != 2 { t.Errorf("Expected 2 total members, got %d", response.TotalCount) } } func TestAudienceHandler_GetAudienceMembers_WithPagination(t *testing.T) { repo := NewMockAudienceRepository() router := setupAudienceRouter(repo) audience := orchestrator.Audience{ ID: uuid.New(), Name: "Test Audience", IsActive: true, CreatedAt: time.Now(), UpdatedAt: time.Now(), } repo.audiences = append(repo.audiences, audience) req := httptest.NewRequest(http.MethodGet, "/v1/audiences/"+audience.ID.String()+"/members?limit=1&offset=0", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("Expected status %d, got %d", http.StatusOK, w.Code) } var response struct { Members []orchestrator.AudienceMember `json:"members"` Count int `json:"count"` Limit int `json:"limit"` Offset int `json:"offset"` } if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("Failed to unmarshal response: %v", err) } if response.Count != 1 { t.Errorf("Expected 1 member in response, got %d", response.Count) } if response.Limit != 1 { t.Errorf("Expected limit 1, got %d", response.Limit) } } func TestAudienceHandler_RefreshAudienceCount(t *testing.T) { repo := NewMockAudienceRepository() router := setupAudienceRouter(repo) audience := orchestrator.Audience{ ID: uuid.New(), Name: "Test Audience", IsActive: true, MemberCount: 0, CreatedAt: time.Now(), UpdatedAt: time.Now(), } repo.audiences = append(repo.audiences, audience) // Pre-initialize members so count works correctly repo.members = []orchestrator.AudienceMember{ {ID: uuid.New(), Name: "Test Person 1"}, {ID: uuid.New(), Name: "Test Person 2"}, } req := httptest.NewRequest(http.MethodPost, "/v1/audiences/"+audience.ID.String()+"/refresh", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("Expected status %d, got %d", http.StatusOK, w.Code) } var response struct { AudienceID string `json:"audience_id"` MemberCount int `json:"member_count"` } if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("Failed to unmarshal response: %v", err) } if response.MemberCount != 2 { t.Errorf("Expected member_count 2, got %d", response.MemberCount) } } func TestAudienceHandler_CreateExport(t *testing.T) { repo := NewMockAudienceRepository() router := setupAudienceRouter(repo) audience := orchestrator.Audience{ ID: uuid.New(), Name: "Test Audience", IsActive: true, CreatedAt: time.Now(), UpdatedAt: time.Now(), } repo.audiences = append(repo.audiences, audience) body := CreateExportRequest{ ExportType: "csv", Purpose: "Newsletter December 2024", ExportedBy: "admin", } bodyJSON, _ := json.Marshal(body) req := httptest.NewRequest(http.MethodPost, "/v1/audiences/"+audience.ID.String()+"/exports", bytes.NewBuffer(bodyJSON)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusCreated { t.Errorf("Expected status %d, got %d: %s", http.StatusCreated, w.Code, w.Body.String()) } var response orchestrator.AudienceExport if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("Failed to unmarshal response: %v", err) } if response.ExportType != "csv" { t.Errorf("Expected export_type 'csv', got '%s'", response.ExportType) } if response.RecordCount != 2 { t.Errorf("Expected record_count 2, got %d", response.RecordCount) } } func TestAudienceHandler_ListExports(t *testing.T) { repo := NewMockAudienceRepository() router := setupAudienceRouter(repo) audience := orchestrator.Audience{ ID: uuid.New(), Name: "Test Audience", IsActive: true, CreatedAt: time.Now(), UpdatedAt: time.Now(), } repo.audiences = append(repo.audiences, audience) // Add an export export := orchestrator.AudienceExport{ ID: uuid.New(), AudienceID: audience.ID, ExportType: "csv", RecordCount: 100, Purpose: "Test export", CreatedAt: time.Now(), } repo.exports = append(repo.exports, export) req := httptest.NewRequest(http.MethodGet, "/v1/audiences/"+audience.ID.String()+"/exports", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("Expected status %d, got %d", http.StatusOK, w.Code) } var response struct { Exports []orchestrator.AudienceExport `json:"exports"` Count int `json:"count"` } if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("Failed to unmarshal response: %v", err) } if response.Count != 1 { t.Errorf("Expected 1 export, got %d", response.Count) } } func TestAudienceHandler_ListAudiences_ActiveOnly(t *testing.T) { repo := NewMockAudienceRepository() router := setupAudienceRouter(repo) // Add active and inactive audiences repo.audiences = []orchestrator.Audience{ {ID: uuid.New(), Name: "Active", IsActive: true, CreatedAt: time.Now(), UpdatedAt: time.Now()}, {ID: uuid.New(), Name: "Inactive", IsActive: false, CreatedAt: time.Now(), UpdatedAt: time.Now()}, } req := httptest.NewRequest(http.MethodGet, "/v1/audiences?active_only=true", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("Expected status %d, got %d", http.StatusOK, w.Code) } var response struct { Audiences []orchestrator.Audience `json:"audiences"` Count int `json:"count"` } if err := json.Unmarshal(w.Body.Bytes(), &response); err != nil { t.Fatalf("Failed to unmarshal response: %v", err) } if response.Count != 1 { t.Errorf("Expected 1 active audience, got %d", response.Count) } if response.Audiences[0].Name != "Active" { t.Errorf("Expected audience 'Active', got '%s'", response.Audiences[0].Name) } }