package service import ( "context" "errors" "testing" "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" ) // mockPreferencesRepository implements port.PreferencesRepository for testing. type mockPreferencesRepository struct { prefs map[string]*domain.UserPreferences err error // inject error for testing error paths } var _ port.PreferencesRepository = (*mockPreferencesRepository)(nil) func newMockPreferencesRepository() *mockPreferencesRepository { return &mockPreferencesRepository{ prefs: make(map[string]*domain.UserPreferences), } } func (m *mockPreferencesRepository) Get(ctx context.Context, userID string) (*domain.UserPreferences, error) { if m.err != nil { return nil, m.err } p, ok := m.prefs[userID] if !ok { return nil, nil } cp := *p return &cp, nil } func (m *mockPreferencesRepository) Upsert(ctx context.Context, userID string, prefs map[string]any) (*domain.UserPreferences, error) { if m.err != nil { return nil, m.err } existing, ok := m.prefs[userID] if !ok { existing = &domain.UserPreferences{ UserID: userID, Preferences: map[string]any{}, } } // Merge preferences 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 TestPreferencesService_Get(t *testing.T) { t.Run("returns empty preferences for new user", func(t *testing.T) { repo := newMockPreferencesRepository() svc := NewPreferencesService(repo, logging.Nop()) prefs, err := svc.Get(context.Background(), "user-1") if err != nil { t.Fatalf("unexpected error: %v", err) } if prefs.UserID != "user-1" { t.Errorf("expected user_id 'user-1', got '%s'", prefs.UserID) } if len(prefs.Preferences) != 0 { t.Errorf("expected empty preferences, got %v", prefs.Preferences) } }) t.Run("returns existing preferences", func(t *testing.T) { repo := newMockPreferencesRepository() repo.prefs["user-1"] = &domain.UserPreferences{ UserID: "user-1", Preferences: map[string]any{"theme": "dark"}, } svc := NewPreferencesService(repo, logging.Nop()) prefs, err := svc.Get(context.Background(), "user-1") if err != nil { t.Fatalf("unexpected error: %v", err) } if prefs.Preferences["theme"] != "dark" { t.Errorf("expected theme 'dark', got '%v'", prefs.Preferences["theme"]) } }) t.Run("returns error on repository failure", func(t *testing.T) { repo := newMockPreferencesRepository() repo.err = errors.New("db connection failed") svc := NewPreferencesService(repo, logging.Nop()) _, err := svc.Get(context.Background(), "user-1") if err == nil { t.Fatal("expected error, got nil") } }) } func TestPreferencesService_Update(t *testing.T) { t.Run("updates with valid preferences", func(t *testing.T) { repo := newMockPreferencesRepository() svc := NewPreferencesService(repo, logging.Nop()) result, err := svc.Update(context.Background(), "user-1", map[string]any{ "theme": "dark", }) if err != nil { t.Fatalf("unexpected error: %v", err) } if result.Preferences["theme"] != "dark" { t.Errorf("expected theme 'dark', got '%v'", result.Preferences["theme"]) } }) t.Run("rejects unknown preference key", func(t *testing.T) { repo := newMockPreferencesRepository() svc := NewPreferencesService(repo, logging.Nop()) _, err := svc.Update(context.Background(), "user-1", map[string]any{ "unknown_key": "value", }) if !errors.Is(err, domain.ErrInvalidPreferenceKey) { t.Errorf("expected ErrInvalidPreferenceKey, got %v", err) } }) t.Run("rejects invalid theme value", func(t *testing.T) { repo := newMockPreferencesRepository() svc := NewPreferencesService(repo, logging.Nop()) _, err := svc.Update(context.Background(), "user-1", map[string]any{ "theme": "blue", }) if !errors.Is(err, domain.ErrInvalidPreferenceValue) { t.Errorf("expected ErrInvalidPreferenceValue, got %v", err) } }) t.Run("rejects invalid language format", func(t *testing.T) { repo := newMockPreferencesRepository() svc := NewPreferencesService(repo, logging.Nop()) _, err := svc.Update(context.Background(), "user-1", map[string]any{ "language": "english", }) if !errors.Is(err, domain.ErrInvalidPreferenceValue) { t.Errorf("expected ErrInvalidPreferenceValue, got %v", err) } }) t.Run("rejects non-boolean notifications_enabled", func(t *testing.T) { repo := newMockPreferencesRepository() svc := NewPreferencesService(repo, logging.Nop()) _, err := svc.Update(context.Background(), "user-1", map[string]any{ "notifications_enabled": "yes", }) if !errors.Is(err, domain.ErrInvalidPreferenceValue) { t.Errorf("expected ErrInvalidPreferenceValue, got %v", err) } }) t.Run("returns error on repository failure", func(t *testing.T) { repo := newMockPreferencesRepository() repo.err = errors.New("db write failed") svc := NewPreferencesService(repo, logging.Nop()) _, err := svc.Update(context.Background(), "user-1", map[string]any{ "theme": "dark", }) if err == nil { t.Fatal("expected error, got nil") } }) t.Run("merges with existing preferences", func(t *testing.T) { repo := newMockPreferencesRepository() svc := NewPreferencesService(repo, logging.Nop()) // Set initial preference _, err := svc.Update(context.Background(), "user-1", map[string]any{ "theme": "dark", }) if err != nil { t.Fatalf("unexpected error on first update: %v", err) } // Update with different key result, err := svc.Update(context.Background(), "user-1", map[string]any{ "language": "en", }) if err != nil { t.Fatalf("unexpected error on second update: %v", err) } if result.Preferences["theme"] != "dark" { t.Errorf("expected theme 'dark' to be preserved, got '%v'", result.Preferences["theme"]) } if result.Preferences["language"] != "en" { t.Errorf("expected language 'en', got '%v'", result.Preferences["language"]) } }) }