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-1770606136/pkg/httperror" "git.threesix.ai/jordan/slack5-1770606136/pkg/logging" "git.threesix.ai/jordan/slack5-1770606136/services/preferences-api/internal/domain" "git.threesix.ai/jordan/slack5-1770606136/services/preferences-api/internal/port" "git.threesix.ai/jordan/slack5-1770606136/services/preferences-api/internal/service" ) // mockPreferenceRepository implements port.PreferenceRepository for handler testing. type mockPreferenceRepository struct { mu sync.RWMutex prefs map[string]*domain.UserPreferences } var _ port.PreferenceRepository = (*mockPreferenceRepository)(nil) func newMockPrefRepository() *mockPreferenceRepository { return &mockPreferenceRepository{ prefs: make(map[string]*domain.UserPreferences), } } func (m *mockPreferenceRepository) Get(ctx context.Context, userID string) (*domain.UserPreferences, error) { m.mu.RLock() defer m.mu.RUnlock() p, ok := m.prefs[userID] if !ok { return nil, nil } cp := *p cpPrefs := make(map[string]any) for k, v := range p.Preferences { cpPrefs[k] = v } cp.Preferences = cpPrefs return &cp, nil } func (m *mockPreferenceRepository) Upsert(ctx context.Context, prefs *domain.UserPreferences) error { m.mu.Lock() defer m.mu.Unlock() cp := *prefs cpPrefs := make(map[string]any) for k, v := range prefs.Preferences { cpPrefs[k] = v } cp.Preferences = cpPrefs m.prefs[prefs.UserID] = &cp return nil } func newTestPreferenceHandler() (*Preference, *mockPreferenceRepository) { repo := newMockPrefRepository() svc := service.NewPreferenceService(repo, logging.Nop()) handler := NewPreference(svc, logging.Nop()) return handler, repo } func TestPreference_Get(t *testing.T) { handler, repo := newTestPreferenceHandler() // Seed data repo.prefs["550e8400-e29b-41d4-a716-446655440000"] = &domain.UserPreferences{ UserID: "550e8400-e29b-41d4-a716-446655440000", Preferences: map[string]any{"theme": "dark"}, } tests := []struct { name string userID string wantStatus int checkBody func(t *testing.T, body map[string]any) }{ { name: "returns existing 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' 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"]) } }, }, { name: "returns empty preferences 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' field in response") } prefs, ok := data["preferences"].(map[string]any) if !ok { t.Fatal("expected 'preferences' field in data") } if len(prefs) != 0 { t.Errorf("expected empty preferences, got %v", prefs) } }, }, { 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("/preferences/{user_id}", func(w http.ResponseWriter, r *http.Request) { if err := handler.Get(w, r); err != nil { // Write error status for test verification httpErr, ok := err.(*httperror.HTTPError) if ok { w.WriteHeader(httpErr.Status) } else { w.WriteHeader(http.StatusInternalServerError) } return } }) req := httptest.NewRequest(http.MethodGet, "/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 && w.Code == http.StatusOK { 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 TestPreference_Upsert(t *testing.T) { handler, _ := newTestPreferenceHandler() tests := []struct { name string userID string body any wantStatus int checkBody func(t *testing.T, body map[string]any) }{ { name: "creates preferences successfully", userID: "550e8400-e29b-41d4-a716-446655440000", body: UpdatePreferencesRequest{ Preferences: map[string]any{ "theme": "dark", "language": "en", }, }, 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' field in response") } prefs := data["preferences"].(map[string]any) if prefs["theme"] != "dark" { t.Errorf("expected theme 'dark', got '%v'", prefs["theme"]) } if data["user_id"] != "550e8400-e29b-41d4-a716-446655440000" { t.Errorf("expected user_id in response, got '%v'", data["user_id"]) } }, }, { name: "returns 400 for invalid theme", userID: "550e8400-e29b-41d4-a716-446655440000", body: UpdatePreferencesRequest{ Preferences: map[string]any{ "theme": "neon", }, }, wantStatus: http.StatusBadRequest, }, { name: "returns 400 for missing preferences field", userID: "550e8400-e29b-41d4-a716-446655440000", body: map[string]any{}, wantStatus: http.StatusBadRequest, }, { name: "returns 400 for invalid UUID", userID: "not-a-uuid", body: UpdatePreferencesRequest{Preferences: map[string]any{"theme": "dark"}}, wantStatus: http.StatusBadRequest, }, { name: "returns 400 for empty body", userID: "550e8400-e29b-41d4-a716-446655440000", body: nil, wantStatus: http.StatusBadRequest, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := chi.NewRouter() r.Put("/preferences/{user_id}", func(w http.ResponseWriter, r *http.Request) { if err := handler.Upsert(w, r); err != nil { httpErr, ok := err.(*httperror.HTTPError) if ok { w.WriteHeader(httpErr.Status) } else { w.WriteHeader(http.StatusInternalServerError) } return } }) 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, "/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 && w.Code == http.StatusOK { 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) } }) } }