package handlers import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/go-chi/chi/v5" "git.threesix.ai/jordan/slate-v3-1770514618/pkg/app" "git.threesix.ai/jordan/slate-v3-1770514618/pkg/auth" "git.threesix.ai/jordan/slate-v3-1770514618/pkg/logging" "git.threesix.ai/jordan/slate-v3-1770514618/services/preferences-api/internal/adapter/memory" "git.threesix.ai/jordan/slate-v3-1770514618/services/preferences-api/internal/service" ) func newTestPreferencesHandler() *Preferences { repo := memory.NewPreferencesRepository() svc := service.NewPreferencesService(repo, logging.Nop()) return NewPreferences(svc) } func withAuthUser(r *http.Request, userID string, roles ...string) *http.Request { user := &auth.User{ID: userID, Roles: roles} ctx := auth.SetUser(r.Context(), user) return r.WithContext(ctx) } func setupRouter(handler *Preferences) *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 } func TestPreferences_Get_NotFound(t *testing.T) { handler := newTestPreferencesHandler() router := setupRouter(handler) req := httptest.NewRequest(http.MethodGet, "/api/preferences-api/preferences/550e8400-e29b-41d4-a716-446655440000", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Errorf("expected 404, got %d", w.Code) } } func TestPreferences_Get_InvalidUUID(t *testing.T) { handler := newTestPreferencesHandler() router := setupRouter(handler) req := httptest.NewRequest(http.MethodGet, "/api/preferences-api/preferences/not-a-uuid", nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Errorf("expected 400, got %d", w.Code) } } func TestPreferences_Get_Forbidden(t *testing.T) { handler := newTestPreferencesHandler() r := chi.NewRouter() r.Get("/api/preferences-api/preferences/{user_id}", app.Wrap(handler.Get)) req := httptest.NewRequest(http.MethodGet, "/api/preferences-api/preferences/550e8400-e29b-41d4-a716-446655440000", nil) req = withAuthUser(req, "different-user-id") w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusForbidden { t.Errorf("expected 403, got %d", w.Code) } } func TestPreferences_Get_AdminAccess(t *testing.T) { handler := newTestPreferencesHandler() // First create preferences r := chi.NewRouter() r.Put("/api/preferences-api/preferences/{user_id}", app.Wrap(handler.Update)) r.Get("/api/preferences-api/preferences/{user_id}", app.Wrap(handler.Get)) body := `{"preferences":{"theme":"dark"}}` putReq := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/550e8400-e29b-41d4-a716-446655440000", bytes.NewBufferString(body)) putReq.Header.Set("Content-Type", "application/json") putReq = withAuthUser(putReq, "550e8400-e29b-41d4-a716-446655440000") putW := httptest.NewRecorder() r.ServeHTTP(putW, putReq) // Admin accesses another user's prefs getReq := httptest.NewRequest(http.MethodGet, "/api/preferences-api/preferences/550e8400-e29b-41d4-a716-446655440000", nil) getReq = withAuthUser(getReq, "admin-user-id", "admin") getW := httptest.NewRecorder() r.ServeHTTP(getW, getReq) if getW.Code != http.StatusOK { t.Errorf("expected 200 for admin access, got %d", getW.Code) } } func TestPreferences_Update_CreateNew(t *testing.T) { handler := newTestPreferencesHandler() router := setupRouter(handler) body := `{"preferences":{"theme":"dark","language":"fr"}}` req := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/550e8400-e29b-41d4-a716-446655440000", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected 200, got %d; body: %s", w.Code, w.Body.String()) } 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 in response") } prefs, ok := data["preferences"].(map[string]any) if !ok { t.Fatal("expected 'preferences' field 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"]) } } func TestPreferences_Update_MergeExisting(t *testing.T) { handler := newTestPreferencesHandler() router := setupRouter(handler) userID := "550e8400-e29b-41d4-a716-446655440000" // Create initial body1 := `{"preferences":{"theme":"dark","language":"fr"}}` req1 := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/"+userID, bytes.NewBufferString(body1)) req1.Header.Set("Content-Type", "application/json") w1 := httptest.NewRecorder() router.ServeHTTP(w1, req1) // Update only theme body2 := `{"preferences":{"theme":"light"}}` req2 := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/"+userID, bytes.NewBufferString(body2)) req2.Header.Set("Content-Type", "application/json") w2 := httptest.NewRecorder() router.ServeHTTP(w2, req2) if w2.Code != http.StatusOK { t.Fatalf("expected 200, got %d; body: %s", w2.Code, w2.Body.String()) } var resp map[string]any _ = json.NewDecoder(w2.Body).Decode(&resp) data := resp["data"].(map[string]any) prefs := data["preferences"].(map[string]any) if prefs["theme"] != "light" { t.Errorf("expected theme 'light', got %v", prefs["theme"]) } if prefs["language"] != "fr" { t.Errorf("expected language 'fr' preserved, got %v", prefs["language"]) } } func TestPreferences_Update_InvalidTheme(t *testing.T) { handler := newTestPreferencesHandler() router := setupRouter(handler) body := `{"preferences":{"theme":"blue"}}` req := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/550e8400-e29b-41d4-a716-446655440000", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Errorf("expected 400, got %d", w.Code) } } func TestPreferences_Update_UnknownPreferenceKey(t *testing.T) { handler := newTestPreferencesHandler() router := setupRouter(handler) body := `{"preferences":{"theme":"dark","unknown_key":"value"}}` req := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/550e8400-e29b-41d4-a716-446655440000", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Errorf("expected 400 for unknown preference key, got %d; body: %s", w.Code, w.Body.String()) } } func TestPreferences_Update_MissingPreferencesField(t *testing.T) { handler := newTestPreferencesHandler() router := setupRouter(handler) body := `{"theme":"dark"}` req := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/550e8400-e29b-41d4-a716-446655440000", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Errorf("expected 400 for missing preferences field, got %d; body: %s", w.Code, w.Body.String()) } } func TestPreferences_Update_EmptyBody(t *testing.T) { handler := newTestPreferencesHandler() router := setupRouter(handler) req := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/550e8400-e29b-41d4-a716-446655440000", nil) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Errorf("expected 400 for empty body, got %d", w.Code) } } func TestPreferences_Update_Forbidden(t *testing.T) { handler := newTestPreferencesHandler() r := chi.NewRouter() r.Put("/api/preferences-api/preferences/{user_id}", app.Wrap(handler.Update)) body := `{"preferences":{"theme":"dark"}}` req := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/550e8400-e29b-41d4-a716-446655440000", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") req = withAuthUser(req, "different-user-id") w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusForbidden { t.Errorf("expected 403, got %d", w.Code) } } func TestPreferences_Update_InvalidUUID(t *testing.T) { handler := newTestPreferencesHandler() router := setupRouter(handler) body := `{"preferences":{"theme":"dark"}}` req := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/not-valid", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) if w.Code != http.StatusBadRequest { t.Errorf("expected 400, got %d", w.Code) } } func TestPreferences_GetAfterUpdate(t *testing.T) { handler := newTestPreferencesHandler() router := setupRouter(handler) userID := "550e8400-e29b-41d4-a716-446655440000" // Create via PUT body := `{"preferences":{"theme":"dark"}}` putReq := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/"+userID, bytes.NewBufferString(body)) putReq.Header.Set("Content-Type", "application/json") putW := httptest.NewRecorder() router.ServeHTTP(putW, putReq) // GET getReq := httptest.NewRequest(http.MethodGet, "/api/preferences-api/preferences/"+userID, nil) getW := httptest.NewRecorder() router.ServeHTTP(getW, getReq) if getW.Code != http.StatusOK { t.Fatalf("expected 200, got %d; body: %s", getW.Code, getW.Body.String()) } var resp map[string]any _ = json.NewDecoder(getW.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"] != "en" { t.Errorf("expected default language 'en', got %v", prefs["language"]) } }