package handlers import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/go-chi/chi/v5" "git.threesix.ai/jordan/slack5-1770544098/pkg/app" "git.threesix.ai/jordan/slack5-1770544098/pkg/auth" "git.threesix.ai/jordan/slack5-1770544098/pkg/logging" "git.threesix.ai/jordan/slack5-1770544098/services/preferences-api/internal/domain" "git.threesix.ai/jordan/slack5-1770544098/services/preferences-api/internal/port" "git.threesix.ai/jordan/slack5-1770544098/services/preferences-api/internal/service" ) const testUserID = "550e8400-e29b-41d4-a716-446655440000" const otherUserID = "550e8400-e29b-41d4-a716-446655440001" // mockPrefsRepository implements port.PreferencesRepository for handler testing. type mockPrefsRepository struct { prefs map[string]*domain.UserPreferences } var _ port.PreferencesRepository = (*mockPrefsRepository)(nil) func newMockPrefsRepository() *mockPrefsRepository { return &mockPrefsRepository{ prefs: make(map[string]*domain.UserPreferences), } } func (m *mockPrefsRepository) Get(ctx context.Context, userID string) (*domain.UserPreferences, error) { p, ok := m.prefs[userID] if !ok { return nil, nil } cp := *p return &cp, nil } func (m *mockPrefsRepository) Upsert(ctx context.Context, userID string, prefs map[string]any) (*domain.UserPreferences, error) { existing, ok := m.prefs[userID] if !ok { existing = &domain.UserPreferences{ UserID: userID, Preferences: map[string]any{}, } } merged := make(map[string]any) for k, v := range existing.Preferences { merged[k] = v } for k, v := range prefs { merged[k] = v } result := &domain.UserPreferences{ UserID: userID, Preferences: merged, } m.prefs[userID] = result return result, nil } func newPrefsTestHandler() (*Preferences, *mockPrefsRepository) { repo := newMockPrefsRepository() svc := service.NewPreferencesService(repo, logging.Nop()) handler := NewPreferences(svc, logging.Nop()) return handler, repo } // withAuthUser adds an authenticated user to the request context. func withAuthUser(r *http.Request, userID string) *http.Request { ctx := auth.SetUser(r.Context(), &auth.User{ID: userID}) return r.WithContext(ctx) } func TestPreferences_Get(t *testing.T) { tests := []struct { name string userID string authUserID string seedPrefs map[string]any wantStatus int wantData bool }{ { name: "returns 200 with preferences for existing user", userID: testUserID, authUserID: testUserID, seedPrefs: map[string]any{"theme": "dark", "language": "en"}, wantStatus: http.StatusOK, wantData: true, }, { name: "returns 200 with empty preferences for new user", userID: testUserID, authUserID: testUserID, seedPrefs: nil, wantStatus: http.StatusOK, wantData: true, }, { name: "returns 400 for invalid UUID", userID: "not-a-uuid", authUserID: testUserID, wantStatus: http.StatusBadRequest, }, { name: "returns 403 for ownership mismatch", userID: testUserID, authUserID: otherUserID, wantStatus: http.StatusForbidden, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { handler, repo := newPrefsTestHandler() if tt.seedPrefs != nil { repo.prefs[tt.userID] = &domain.UserPreferences{ UserID: tt.userID, Preferences: tt.seedPrefs, } } 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) req = withAuthUser(req, tt.authUserID) 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.wantData { 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' object in response") } if _, ok := resp["meta"]; !ok { t.Fatal("expected 'meta' field in response") } if data["user_id"] != tt.userID { t.Errorf("expected user_id %s, got %v", tt.userID, data["user_id"]) } prefs, ok := data["preferences"].(map[string]any) if !ok { t.Fatal("expected 'preferences' map in data") } if tt.seedPrefs == nil && len(prefs) != 0 { t.Errorf("expected empty preferences, got %v", prefs) } if tt.seedPrefs != nil { for k, v := range tt.seedPrefs { if prefs[k] != v { t.Errorf("expected preferences[%s] = %v, got %v", k, v, prefs[k]) } } } } }) } } func TestPreferences_Update(t *testing.T) { tests := []struct { name string userID string authUserID string body any wantStatus int wantData bool }{ { name: "returns 200 with merged preferences on success", userID: testUserID, authUserID: testUserID, body: map[string]any{"preferences": map[string]any{"theme": "dark"}}, wantStatus: http.StatusOK, wantData: true, }, { name: "returns 400 for unknown preference keys", userID: testUserID, authUserID: testUserID, body: map[string]any{"preferences": map[string]any{"unknown": "value"}}, wantStatus: http.StatusBadRequest, }, { name: "returns 400 for invalid preference values", userID: testUserID, authUserID: testUserID, body: map[string]any{"preferences": map[string]any{"theme": "blue"}}, wantStatus: http.StatusBadRequest, }, { name: "returns 400 for missing preferences field", userID: testUserID, authUserID: testUserID, body: map[string]any{}, wantStatus: http.StatusBadRequest, }, { name: "returns 400 for invalid UUID", userID: "not-a-uuid", authUserID: testUserID, body: map[string]any{"preferences": map[string]any{"theme": "dark"}}, wantStatus: http.StatusBadRequest, }, { name: "returns 403 for ownership mismatch", userID: testUserID, authUserID: otherUserID, body: map[string]any{"preferences": map[string]any{"theme": "dark"}}, wantStatus: http.StatusForbidden, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { handler, _ := newPrefsTestHandler() r := chi.NewRouter() r.Put("/api/preferences-api/preferences/{user_id}", app.Wrap(handler.Update)) var body []byte if tt.body != nil { var err error body, 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(body)) req.Header.Set("Content-Type", "application/json") req = withAuthUser(req, tt.authUserID) 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.wantData { 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' object in response") } if _, ok := resp["meta"]; !ok { t.Fatal("expected 'meta' field in response") } if data["user_id"] != tt.userID { t.Errorf("expected user_id %s, got %v", tt.userID, data["user_id"]) } prefs, ok := data["preferences"].(map[string]any) if !ok { t.Fatal("expected 'preferences' map in data") } if prefs["theme"] != "dark" { t.Errorf("expected theme 'dark', got %v", prefs["theme"]) } } }) } }