package service import ( "context" "sync" "testing" "git.threesix.ai/jordan/slack5-1770541397/pkg/logging" "git.threesix.ai/jordan/slack5-1770541397/services/preferences-api/internal/domain" "git.threesix.ai/jordan/slack5-1770541397/services/preferences-api/internal/port" ) // mockPreferencesRepository implements port.PreferencesRepository for testing. type mockPreferencesRepository struct { mu sync.RWMutex store map[domain.UserID]*domain.UserPreferences } var _ port.PreferencesRepository = (*mockPreferencesRepository)(nil) func newMockPreferencesRepository() *mockPreferencesRepository { return &mockPreferencesRepository{ store: make(map[domain.UserID]*domain.UserPreferences), } } func (m *mockPreferencesRepository) Get(ctx context.Context, userID domain.UserID) (*domain.UserPreferences, error) { m.mu.RLock() defer m.mu.RUnlock() prefs, ok := m.store[userID] if !ok { return nil, domain.ErrPreferencesNotFound } copy := *prefs return ©, nil } func (m *mockPreferencesRepository) Upsert(ctx context.Context, prefs *domain.UserPreferences) error { m.mu.Lock() defer m.mu.Unlock() copy := *prefs m.store[prefs.UserID] = © return nil } func TestPreferencesService_GetPreferences(t *testing.T) { repo := newMockPreferencesRepository() svc := NewPreferencesService(repo, logging.Nop()) t.Run("returns defaults when no preferences exist", func(t *testing.T) { prefs, err := svc.GetPreferences(context.Background(), "user-123") if err != nil { t.Fatalf("unexpected error: %v", err) } if prefs.UserID != "user-123" { t.Errorf("expected user_id 'user-123', got '%s'", prefs.UserID) } if prefs.Preferences.Theme != "system" { t.Errorf("expected default theme 'system', got '%s'", prefs.Preferences.Theme) } if prefs.Preferences.Language != "en" { t.Errorf("expected default language 'en', got '%s'", prefs.Preferences.Language) } if !prefs.Preferences.Notifications.Email { t.Error("expected default notifications.email to be true") } if !prefs.Preferences.Notifications.Push { t.Error("expected default notifications.push to be true") } if prefs.Preferences.Notifications.SMS { t.Error("expected default notifications.sms to be false") } }) t.Run("returns stored preferences", func(t *testing.T) { // Store preferences first _, err := svc.SetPreferences(context.Background(), "user-456", domain.Preferences{ Theme: "dark", Language: "fr", Notifications: domain.NotificationPreferences{ Email: false, Push: true, SMS: true, }, }) if err != nil { t.Fatalf("unexpected error setting preferences: %v", err) } prefs, err := svc.GetPreferences(context.Background(), "user-456") if err != nil { t.Fatalf("unexpected error: %v", err) } if prefs.Preferences.Theme != "dark" { t.Errorf("expected theme 'dark', got '%s'", prefs.Preferences.Theme) } if prefs.Preferences.Language != "fr" { t.Errorf("expected language 'fr', got '%s'", prefs.Preferences.Language) } if prefs.Preferences.Notifications.Email { t.Error("expected notifications.email to be false") } if prefs.Preferences.Notifications.SMS != true { t.Error("expected notifications.sms to be true") } }) } func TestPreferencesService_SetPreferences(t *testing.T) { repo := newMockPreferencesRepository() svc := NewPreferencesService(repo, logging.Nop()) t.Run("sets valid preferences", func(t *testing.T) { prefs, err := svc.SetPreferences(context.Background(), "user-789", domain.Preferences{ Theme: "light", Language: "en", Notifications: domain.NotificationPreferences{ Email: true, Push: false, SMS: false, }, }) if err != nil { t.Fatalf("unexpected error: %v", err) } if prefs.UserID != "user-789" { t.Errorf("expected user_id 'user-789', got '%s'", prefs.UserID) } if prefs.Preferences.Theme != "light" { t.Errorf("expected theme 'light', got '%s'", prefs.Preferences.Theme) } if prefs.UpdatedAt.IsZero() { t.Error("expected non-zero updated_at") } }) t.Run("rejects invalid theme", func(t *testing.T) { _, err := svc.SetPreferences(context.Background(), "user-789", domain.Preferences{ Theme: "neon", Language: "en", }) if err != domain.ErrInvalidTheme { t.Errorf("expected ErrInvalidTheme, got %v", err) } }) t.Run("rejects language exceeding max length", func(t *testing.T) { _, err := svc.SetPreferences(context.Background(), "user-789", domain.Preferences{ Theme: "dark", Language: "abcdefghijk", // 11 chars }) if err != domain.ErrInvalidLanguage { t.Errorf("expected ErrInvalidLanguage, got %v", err) } }) t.Run("allows empty theme", func(t *testing.T) { prefs, err := svc.SetPreferences(context.Background(), "user-000", domain.Preferences{ Theme: "", Language: "en", }) if err != nil { t.Fatalf("unexpected error: %v", err) } if prefs.Preferences.Theme != "" { t.Errorf("expected empty theme, got '%s'", prefs.Preferences.Theme) } }) t.Run("allows max length language", func(t *testing.T) { _, err := svc.SetPreferences(context.Background(), "user-000", domain.Preferences{ Theme: "dark", Language: "abcdefghij", // exactly 10 chars }) if err != nil { t.Fatalf("unexpected error: %v", err) } }) }