slack5-1770603014/services/preferences-api/internal/service/preferences_test.go
rdev-worker e5fc44d10e
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
build: /implement-feature user-preferences
2026-02-09 02:37:38 +00:00

254 lines
7.6 KiB
Go

package service
import (
"context"
"sync"
"testing"
"git.threesix.ai/jordan/slack5-1770603014/pkg/logging"
"git.threesix.ai/jordan/slack5-1770603014/services/preferences-api/internal/domain"
"git.threesix.ai/jordan/slack5-1770603014/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()
p, ok := m.store[userID]
if !ok {
return nil, nil
}
cp := *p
return &cp, nil
}
func (m *mockPreferencesRepository) Upsert(ctx context.Context, prefs *domain.UserPreferences) error {
m.mu.Lock()
defer m.mu.Unlock()
cp := *prefs
m.store[prefs.UserID] = &cp
return nil
}
func strPtr(s string) *string { return &s }
func boolPtr(b bool) *bool { return &b }
func TestPreferencesService_GetPreferences(t *testing.T) {
repo := newMockPreferencesRepository()
svc := NewPreferencesService(repo, logging.Nop())
t.Run("returns defaults for unknown user", func(t *testing.T) {
prefs, err := svc.GetPreferences(context.Background(), "unknown-user")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if prefs.Preferences.Theme != "system" {
t.Errorf("expected theme 'system', got '%s'", prefs.Preferences.Theme)
}
if prefs.Preferences.Language != "en" {
t.Errorf("expected language 'en', got '%s'", prefs.Preferences.Language)
}
if prefs.Preferences.Notifications.Email != true {
t.Error("expected notifications.email true")
}
if prefs.Preferences.Notifications.Push != true {
t.Error("expected notifications.push true")
}
if prefs.Preferences.Notifications.Digest != "weekly" {
t.Errorf("expected notifications.digest 'weekly', got '%s'", prefs.Preferences.Notifications.Digest)
}
})
t.Run("returns stored preferences for existing user", func(t *testing.T) {
// Seed data
repo.mu.Lock()
repo.store["user-1"] = &domain.UserPreferences{
UserID: "user-1",
Preferences: domain.Preferences{
Theme: "dark",
Language: "fr",
Notifications: domain.NotificationSettings{
Email: false,
Push: true,
Digest: "daily",
},
},
}
repo.mu.Unlock()
prefs, err := svc.GetPreferences(context.Background(), "user-1")
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)
}
})
}
func TestPreferencesService_UpdatePreferences(t *testing.T) {
repo := newMockPreferencesRepository()
svc := NewPreferencesService(repo, logging.Nop())
t.Run("creates new preferences from defaults", func(t *testing.T) {
prefs, err := svc.UpdatePreferences(context.Background(), "new-user", UpdateInput{
Theme: strPtr("dark"),
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if prefs.Preferences.Theme != "dark" {
t.Errorf("expected theme 'dark', got '%s'", prefs.Preferences.Theme)
}
// Other fields should be defaults
if prefs.Preferences.Language != "en" {
t.Errorf("expected language 'en', got '%s'", prefs.Preferences.Language)
}
if prefs.Preferences.Notifications.Digest != "weekly" {
t.Errorf("expected digest 'weekly', got '%s'", prefs.Preferences.Notifications.Digest)
}
})
t.Run("merges partial data with existing preferences", func(t *testing.T) {
// First set full preferences
_, _ = svc.UpdatePreferences(context.Background(), "merge-user", UpdateInput{
Theme: strPtr("light"),
Language: strPtr("es"),
Notifications: &NotificationsInput{
Email: boolPtr(false),
Push: boolPtr(false),
Digest: strPtr("daily"),
},
})
// Now partial update - only change push notification
prefs, err := svc.UpdatePreferences(context.Background(), "merge-user", UpdateInput{
Notifications: &NotificationsInput{
Push: boolPtr(true),
},
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Push should be updated
if prefs.Preferences.Notifications.Push != true {
t.Error("expected notifications.push true")
}
// Other fields should be unchanged
if prefs.Preferences.Theme != "light" {
t.Errorf("expected theme 'light', got '%s'", prefs.Preferences.Theme)
}
if prefs.Preferences.Language != "es" {
t.Errorf("expected language 'es', got '%s'", prefs.Preferences.Language)
}
if prefs.Preferences.Notifications.Email != false {
t.Error("expected notifications.email false")
}
if prefs.Preferences.Notifications.Digest != "daily" {
t.Errorf("expected digest 'daily', got '%s'", prefs.Preferences.Notifications.Digest)
}
})
t.Run("updates theme only", func(t *testing.T) {
prefs, err := svc.UpdatePreferences(context.Background(), "theme-user", UpdateInput{
Theme: strPtr("light"),
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if prefs.Preferences.Theme != "light" {
t.Errorf("expected theme 'light', got '%s'", prefs.Preferences.Theme)
}
})
t.Run("updates language only", func(t *testing.T) {
prefs, err := svc.UpdatePreferences(context.Background(), "lang-user", UpdateInput{
Language: strPtr("fr"),
})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if prefs.Preferences.Language != "fr" {
t.Errorf("expected language 'fr', got '%s'", prefs.Preferences.Language)
}
})
t.Run("updates all fields together", func(t *testing.T) {
prefs, err := svc.UpdatePreferences(context.Background(), "all-user", UpdateInput{
Theme: strPtr("dark"),
Language: strPtr("de"),
Notifications: &NotificationsInput{
Email: boolPtr(false),
Push: boolPtr(true),
Digest: strPtr("never"),
},
})
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 != "de" {
t.Errorf("expected language 'de', got '%s'", prefs.Preferences.Language)
}
if prefs.Preferences.Notifications.Email != false {
t.Error("expected notifications.email false")
}
if prefs.Preferences.Notifications.Push != true {
t.Error("expected notifications.push true")
}
if prefs.Preferences.Notifications.Digest != "never" {
t.Errorf("expected digest 'never', got '%s'", prefs.Preferences.Notifications.Digest)
}
})
t.Run("rejects invalid theme", func(t *testing.T) {
_, err := svc.UpdatePreferences(context.Background(), "invalid-theme-user", UpdateInput{
Theme: strPtr("purple"),
})
if err != domain.ErrInvalidTheme {
t.Errorf("expected ErrInvalidTheme, got %v", err)
}
})
t.Run("rejects invalid digest", func(t *testing.T) {
_, err := svc.UpdatePreferences(context.Background(), "invalid-digest-user", UpdateInput{
Notifications: &NotificationsInput{
Digest: strPtr("monthly"),
},
})
if err != domain.ErrInvalidDigest {
t.Errorf("expected ErrInvalidDigest, got %v", err)
}
})
t.Run("rejects empty language", func(t *testing.T) {
_, err := svc.UpdatePreferences(context.Background(), "empty-lang-user", UpdateInput{
Language: strPtr(""),
})
if err != domain.ErrInvalidLanguage {
t.Errorf("expected ErrInvalidLanguage, got %v", err)
}
})
}