package handlers import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/go-chi/chi/v5" "git.threesix.ai/jordan/slate-test-1770505673/pkg/app" "git.threesix.ai/jordan/slate-test-1770505673/pkg/auth" "git.threesix.ai/jordan/slate-test-1770505673/pkg/logging" "git.threesix.ai/jordan/slate-test-1770505673/services/preferences-api/internal/port" "git.threesix.ai/jordan/slate-test-1770505673/services/preferences-api/internal/service" ) const testUserID = "550e8400-e29b-41d4-a716-446655440000" const otherUserID = "550e8400-e29b-41d4-a716-446655440001" // mockPreferenceRepository implements port.PreferenceRepository for testing. type mockPreferenceRepository struct { data map[string]map[string]string } var _ port.PreferenceRepository = (*mockPreferenceRepository)(nil) func newMockRepo() *mockPreferenceRepository { return &mockPreferenceRepository{ data: make(map[string]map[string]string), } } func (m *mockPreferenceRepository) GetByUserID(_ context.Context, userID string) (map[string]string, error) { prefs, ok := m.data[userID] if !ok { return make(map[string]string), nil } result := make(map[string]string, len(prefs)) for k, v := range prefs { result[k] = v } return result, nil } func (m *mockPreferenceRepository) Upsert(_ context.Context, userID string, prefs map[string]string) error { if m.data[userID] == nil { m.data[userID] = make(map[string]string) } for k, v := range prefs { m.data[userID][k] = v } return nil } func newTestHandler() (*Preference, *mockPreferenceRepository) { repo := newMockRepo() svc := service.NewPreferenceService(repo, logging.Nop()) handler := NewPreference(svc, logging.Nop()) return handler, repo } // setupRouter creates a test router with the handler wrapped in app.Wrap. func setupRouter(handler *Preference) *chi.Mux { r := chi.NewRouter() r.Get("/api/preferences-api/preferences/{user_id}", app.Wrap(handler.Get)) r.Put("/api/preferences-api/preferences/{user_id}", app.Wrap(handler.Update)) return r } // withAuth adds an authenticated user to the request context. func withAuth(r *http.Request, userID string) *http.Request { ctx := auth.SetUser(r.Context(), &auth.User{ID: userID}) return r.WithContext(ctx) } func TestPreference_Get(t *testing.T) { handler, repo := newTestHandler() router := setupRouter(handler) t.Run("returns empty prefs for user with no preferences", func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/api/preferences-api/preferences/"+testUserID, nil) req = withAuth(req, testUserID) w := httptest.NewRecorder() router.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, ok := resp["data"].(map[string]any) if !ok { t.Fatal("expected 'data' to be an object") } if len(data) != 0 { t.Errorf("expected empty data, got %v", data) } }) t.Run("returns preferences for user with data", func(t *testing.T) { repo.data[testUserID] = map[string]string{"theme": "dark", "language": "en"} req := httptest.NewRequest(http.MethodGet, "/api/preferences-api/preferences/"+testUserID, nil) req = withAuth(req, testUserID) w := httptest.NewRecorder() router.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) if data["theme"] != "dark" { t.Errorf("expected theme 'dark', got '%v'", data["theme"]) } if data["language"] != "en" { t.Errorf("expected language 'en', got '%v'", data["language"]) } // Clean up delete(repo.data, testUserID) }) t.Run("returns 400 for invalid UUID", func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/api/preferences-api/preferences/not-a-uuid", nil) req = withAuth(req, testUserID) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Errorf("expected status 400, got %d", w.Code) } }) t.Run("returns 403 for accessing another user's preferences", func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/api/preferences-api/preferences/"+otherUserID, nil) req = withAuth(req, testUserID) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusForbidden { t.Errorf("expected status 403, got %d", w.Code) } }) } func TestPreference_Update(t *testing.T) { handler, _ := newTestHandler() router := setupRouter(handler) t.Run("updates preferences successfully", func(t *testing.T) { body, _ := json.Marshal(map[string]string{"theme": "dark", "language": "fr"}) req := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/"+testUserID, bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") req = withAuth(req, testUserID) w := httptest.NewRecorder() router.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) if data["theme"] != "dark" { t.Errorf("expected theme 'dark', got '%v'", data["theme"]) } if data["language"] != "fr" { t.Errorf("expected language 'fr', got '%v'", data["language"]) } }) t.Run("returns 400 for unknown key", func(t *testing.T) { body, _ := json.Marshal(map[string]string{"unknown_key": "val"}) req := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/"+testUserID, bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") req = withAuth(req, testUserID) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Errorf("expected status 400, got %d; body: %s", w.Code, w.Body.String()) } }) t.Run("returns 400 for invalid value", func(t *testing.T) { body, _ := json.Marshal(map[string]string{"theme": "blue"}) req := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/"+testUserID, bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") req = withAuth(req, testUserID) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Errorf("expected status 400, got %d; body: %s", w.Code, w.Body.String()) } }) t.Run("returns 400 for empty body", func(t *testing.T) { req := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/"+testUserID, nil) req.Header.Set("Content-Type", "application/json") req = withAuth(req, testUserID) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Errorf("expected status 400, got %d; body: %s", w.Code, w.Body.String()) } }) t.Run("returns 400 for invalid UUID", func(t *testing.T) { body, _ := json.Marshal(map[string]string{"theme": "dark"}) req := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/not-a-uuid", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") req = withAuth(req, testUserID) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Errorf("expected status 400, got %d", w.Code) } }) t.Run("returns 403 for accessing another user's preferences", func(t *testing.T) { body, _ := json.Marshal(map[string]string{"theme": "dark"}) req := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/"+otherUserID, bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") req = withAuth(req, testUserID) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusForbidden { t.Errorf("expected status 403, got %d", w.Code) } }) }