Files
breakpilot-lehrer/edu-search-service/internal/api/handlers/audience_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

631 lines
18 KiB
Go

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)
}
}