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-1770529463/pkg/app" "git.threesix.ai/jordan/slack5-1770529463/pkg/logging" "git.threesix.ai/jordan/slack5-1770529463/services/preferences-api/internal/port" "git.threesix.ai/jordan/slack5-1770529463/services/preferences-api/internal/service" ) // mockPreferenceRepository implements port.PreferenceRepository for testing. type mockPreferenceRepository struct { mu sync.RWMutex prefs map[string]map[string]string } var _ port.PreferenceRepository = (*mockPreferenceRepository)(nil) func newMockPreferenceRepository() *mockPreferenceRepository { return &mockPreferenceRepository{ prefs: make(map[string]map[string]string), } } func (m *mockPreferenceRepository) GetByUserID(ctx context.Context, userID string) ([]port.PreferenceRow, error) { m.mu.RLock() defer m.mu.RUnlock() userPrefs, ok := m.prefs[userID] if !ok { return nil, nil } rows := make([]port.PreferenceRow, 0, len(userPrefs)) for k, v := range userPrefs { rows = append(rows, port.PreferenceRow{ UserID: userID, Key: k, Value: v, }) } return rows, nil } func (m *mockPreferenceRepository) Upsert(ctx context.Context, userID string, key string, value string) error { m.mu.Lock() defer m.mu.Unlock() if m.prefs[userID] == nil { m.prefs[userID] = make(map[string]string) } m.prefs[userID][key] = value return nil } func newTestPreferenceHandler() (*Preference, *mockPreferenceRepository) { repo := newMockPreferenceRepository() svc := service.NewPreferenceService(repo, logging.Nop()) handler := NewPreference(svc, logging.Nop()) return handler, repo } func TestPreference_GetPreferences(t *testing.T) { handler, repo := newTestPreferenceHandler() validUserID := "550e8400-e29b-41d4-a716-446655440000" t.Run("returns defaults for new user", func(t *testing.T) { r := chi.NewRouter() r.Get("/api/preferences-api/preferences/{user_id}", app.Wrap(handler.GetPreferences)) req := httptest.NewRequest(http.MethodGet, "/api/preferences-api/preferences/"+validUserID, nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d", w.Code) } var resp map[string]any if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { t.Fatalf("failed to decode response: %v", err) } data, ok := resp["data"].(map[string]any) if !ok { t.Fatal("expected 'data' field as object in response") } if data["user_id"] != validUserID { t.Errorf("expected user_id %s, got %v", validUserID, data["user_id"]) } prefs, ok := data["preferences"].(map[string]any) if !ok { t.Fatal("expected 'preferences' field as object in data") } if prefs["theme"] != "system" { t.Errorf("expected default theme 'system', got %v", prefs["theme"]) } if prefs["language"] != "en" { t.Errorf("expected default language 'en', got %v", prefs["language"]) } if prefs["notifications_enabled"] != true { t.Errorf("expected default notifications_enabled true, got %v", prefs["notifications_enabled"]) } }) t.Run("returns stored preferences merged with defaults", func(t *testing.T) { _ = repo.Upsert(context.Background(), validUserID, "theme", "dark") r := chi.NewRouter() r.Get("/api/preferences-api/preferences/{user_id}", app.Wrap(handler.GetPreferences)) req := httptest.NewRequest(http.MethodGet, "/api/preferences-api/preferences/"+validUserID, nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d", w.Code) } var resp map[string]any _ = json.NewDecoder(w.Body).Decode(&resp) data := resp["data"].(map[string]any) prefs := data["preferences"].(map[string]any) if prefs["theme"] != "dark" { t.Errorf("expected theme 'dark', got %v", prefs["theme"]) } }) t.Run("returns 400 for invalid user_id", func(t *testing.T) { r := chi.NewRouter() r.Get("/api/preferences-api/preferences/{user_id}", app.Wrap(handler.GetPreferences)) req := httptest.NewRequest(http.MethodGet, "/api/preferences-api/preferences/not-a-uuid", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Errorf("expected status 400, got %d", w.Code) } }) t.Run("response has meta field", func(t *testing.T) { r := chi.NewRouter() r.Get("/api/preferences-api/preferences/{user_id}", app.Wrap(handler.GetPreferences)) req := httptest.NewRequest(http.MethodGet, "/api/preferences-api/preferences/"+validUserID, nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) var resp map[string]any _ = json.NewDecoder(w.Body).Decode(&resp) if _, ok := resp["meta"]; !ok { t.Error("expected 'meta' field in response") } }) } func TestPreference_UpdatePreferences(t *testing.T) { handler, _ := newTestPreferenceHandler() validUserID := "550e8400-e29b-41d4-a716-446655440000" t.Run("updates preferences successfully", func(t *testing.T) { r := chi.NewRouter() r.Put("/api/preferences-api/preferences/{user_id}", app.Wrap(handler.UpdatePreferences)) body, _ := json.Marshal(UpdatePreferencesRequest{ Preferences: map[string]any{ "theme": "dark", "language": "fr", }, }) req := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/"+validUserID, bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d; body: %s", w.Code, w.Body.String()) } var resp map[string]any _ = json.NewDecoder(w.Body).Decode(&resp) data := resp["data"].(map[string]any) prefs := data["preferences"].(map[string]any) 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"]) } // Default should still be present if prefs["notifications_enabled"] != true { t.Errorf("expected notifications_enabled true, got %v", prefs["notifications_enabled"]) } }) t.Run("returns 400 for unknown key", func(t *testing.T) { r := chi.NewRouter() r.Put("/api/preferences-api/preferences/{user_id}", app.Wrap(handler.UpdatePreferences)) body, _ := json.Marshal(UpdatePreferencesRequest{ Preferences: map[string]any{ "font_size": "large", }, }) req := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/"+validUserID, bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Errorf("expected status 400, got %d", w.Code) } }) t.Run("returns 400 for invalid value", func(t *testing.T) { r := chi.NewRouter() r.Put("/api/preferences-api/preferences/{user_id}", app.Wrap(handler.UpdatePreferences)) body, _ := json.Marshal(UpdatePreferencesRequest{ Preferences: map[string]any{ "theme": "neon", }, }) req := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/"+validUserID, bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Errorf("expected status 400, got %d", w.Code) } }) t.Run("returns 400 for invalid user_id", func(t *testing.T) { r := chi.NewRouter() r.Put("/api/preferences-api/preferences/{user_id}", app.Wrap(handler.UpdatePreferences)) body, _ := json.Marshal(UpdatePreferencesRequest{ Preferences: map[string]any{ "theme": "dark", }, }) req := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/bad-id", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Errorf("expected status 400, got %d", w.Code) } }) t.Run("handles boolean notifications_enabled", func(t *testing.T) { r := chi.NewRouter() r.Put("/api/preferences-api/preferences/{user_id}", app.Wrap(handler.UpdatePreferences)) body, _ := json.Marshal(UpdatePreferencesRequest{ Preferences: map[string]any{ "notifications_enabled": false, }, }) req := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/"+validUserID, bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d; body: %s", w.Code, w.Body.String()) } var resp map[string]any _ = json.NewDecoder(w.Body).Decode(&resp) data := resp["data"].(map[string]any) prefs := data["preferences"].(map[string]any) if prefs["notifications_enabled"] != false { t.Errorf("expected notifications_enabled false, got %v", prefs["notifications_enabled"]) } }) }