package handlers import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "sync" "testing" "github.com/go-chi/chi/v5" "git.threesix.ai/jordan/slack5-1770603014/pkg/app" "git.threesix.ai/jordan/slack5-1770603014/pkg/logging" "git.threesix.ai/jordan/slack5-1770603014/services/preferences-api/internal/domain" "git.threesix.ai/jordan/slack5-1770603014/services/preferences-api/internal/port" "git.threesix.ai/jordan/slack5-1770603014/services/preferences-api/internal/service" ) // mockPreferencesRepository implements port.PreferencesRepository for testing. type mockPreferencesRepository struct { mu sync.RWMutex store map[domain.UserID]*domain.UserPreferences } var _ port.PreferencesRepository = (*mockPreferencesRepository)(nil) func newMockPreferencesRepository() *mockPreferencesRepository { return &mockPreferencesRepository{ store: make(map[domain.UserID]*domain.UserPreferences), } } func (m *mockPreferencesRepository) Get(ctx context.Context, userID domain.UserID) (*domain.UserPreferences, error) { m.mu.RLock() defer m.mu.RUnlock() p, ok := m.store[userID] if !ok { return nil, nil } cp := *p return &cp, nil } func (m *mockPreferencesRepository) Upsert(ctx context.Context, prefs *domain.UserPreferences) error { m.mu.Lock() defer m.mu.Unlock() cp := *prefs m.store[prefs.UserID] = &cp return nil } func newTestPreferencesHandler() (*Preferences, *mockPreferencesRepository) { repo := newMockPreferencesRepository() svc := service.NewPreferencesService(repo, logging.Nop()) handler := NewPreferences(svc, logging.Nop()) return handler, repo } func TestPreferences_Get(t *testing.T) { handler, repo := newTestPreferencesHandler() // Seed data for existing user repo.mu.Lock() repo.store["550e8400-e29b-41d4-a716-446655440000"] = &domain.UserPreferences{ UserID: "550e8400-e29b-41d4-a716-446655440000", Preferences: domain.Preferences{ Theme: "dark", Language: "fr", Notifications: domain.NotificationSettings{ Email: false, Push: true, Digest: "daily", }, }, } repo.mu.Unlock() tests := []struct { name string userID string wantStatus int checkBody func(t *testing.T, body map[string]any) }{ { name: "returns stored preferences", userID: "550e8400-e29b-41d4-a716-446655440000", wantStatus: http.StatusOK, checkBody: func(t *testing.T, body map[string]any) { data, ok := body["data"].(map[string]any) if !ok { t.Fatal("expected 'data' object in response") } prefs, ok := data["preferences"].(map[string]any) if !ok { t.Fatal("expected 'preferences' object in data") } if prefs["theme"] != "dark" { t.Errorf("expected theme 'dark', got '%v'", prefs["theme"]) } }, }, { name: "returns defaults for unknown user", userID: "550e8400-e29b-41d4-a716-446655440001", wantStatus: http.StatusOK, checkBody: func(t *testing.T, body map[string]any) { data, ok := body["data"].(map[string]any) if !ok { t.Fatal("expected 'data' object in response") } prefs, ok := data["preferences"].(map[string]any) if !ok { t.Fatal("expected 'preferences' object in data") } if prefs["theme"] != "system" { t.Errorf("expected theme 'system', got '%v'", prefs["theme"]) } }, }, { name: "returns 400 for invalid UUID", userID: "not-a-uuid", wantStatus: http.StatusBadRequest, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := chi.NewRouter() r.Get("/api/preferences-api/preferences/{user_id}", app.Wrap(handler.Get)) req := httptest.NewRequest(http.MethodGet, "/api/preferences-api/preferences/"+tt.userID, nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != tt.wantStatus { t.Errorf("expected status %d, got %d", tt.wantStatus, w.Code) } if tt.checkBody != nil { var body map[string]any if err := json.NewDecoder(w.Body).Decode(&body); err != nil { t.Fatalf("failed to decode response: %v", err) } tt.checkBody(t, body) } }) } } func TestPreferences_Upsert(t *testing.T) { handler, _ := newTestPreferencesHandler() tests := []struct { name string userID string body any wantStatus int checkBody func(t *testing.T, body map[string]any) }{ { name: "creates preferences with full update", userID: "550e8400-e29b-41d4-a716-446655440000", body: map[string]any{ "preferences": map[string]any{ "theme": "dark", "language": "fr", "notifications": map[string]any{ "email": false, "push": true, "digest": "daily", }, }, }, wantStatus: http.StatusOK, checkBody: func(t *testing.T, body map[string]any) { data, ok := body["data"].(map[string]any) if !ok { t.Fatal("expected 'data' object in response") } prefs, ok := data["preferences"].(map[string]any) if !ok { t.Fatal("expected 'preferences' object in data") } if prefs["theme"] != "dark" { t.Errorf("expected theme 'dark', got '%v'", prefs["theme"]) } if prefs["language"] != "fr" { t.Errorf("expected language 'fr', got '%v'", prefs["language"]) } }, }, { name: "creates preferences with partial update", userID: "550e8400-e29b-41d4-a716-446655440002", body: map[string]any{ "preferences": map[string]any{ "theme": "light", }, }, wantStatus: http.StatusOK, checkBody: func(t *testing.T, body map[string]any) { data := body["data"].(map[string]any) prefs := data["preferences"].(map[string]any) if prefs["theme"] != "light" { t.Errorf("expected theme 'light', got '%v'", prefs["theme"]) } // Language should be default if prefs["language"] != "en" { t.Errorf("expected default language 'en', got '%v'", prefs["language"]) } }, }, { name: "returns 400 for invalid UUID", userID: "not-a-uuid", body: map[string]any{"preferences": map[string]any{"theme": "dark"}}, wantStatus: http.StatusBadRequest, }, { name: "returns 400 for invalid theme", userID: "550e8400-e29b-41d4-a716-446655440003", body: map[string]any{ "preferences": map[string]any{ "theme": "purple", }, }, wantStatus: http.StatusBadRequest, }, { name: "returns 400 for empty body", userID: "550e8400-e29b-41d4-a716-446655440004", body: nil, wantStatus: http.StatusBadRequest, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := chi.NewRouter() r.Put("/api/preferences-api/preferences/{user_id}", app.Wrap(handler.Upsert)) var bodyBytes []byte if tt.body != nil { var err error bodyBytes, err = json.Marshal(tt.body) if err != nil { t.Fatalf("failed to marshal body: %v", err) } } req := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/"+tt.userID, bytes.NewReader(bodyBytes)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != tt.wantStatus { t.Errorf("expected status %d, got %d; body: %s", tt.wantStatus, w.Code, w.Body.String()) } if tt.checkBody != nil { var body map[string]any if err := json.NewDecoder(w.Body).Decode(&body); err != nil { t.Fatalf("failed to decode response: %v", err) } tt.checkBody(t, body) } }) } }