package service import ( "context" "testing" "git.threesix.ai/jordan/slate-v3-1770514618/pkg/logging" "git.threesix.ai/jordan/slate-v3-1770514618/services/preferences-api/internal/adapter/memory" "git.threesix.ai/jordan/slate-v3-1770514618/services/preferences-api/internal/domain" ) func strPtr(s string) *string { return &s } func boolPtr(b bool) *bool { return &b } func TestPreferencesService_Get(t *testing.T) { repo := memory.NewPreferencesRepository() svc := NewPreferencesService(repo, logging.Nop()) ctx := context.Background() t.Run("returns not found when no preferences exist", func(t *testing.T) { _, err := svc.Get(ctx, "user-1") if err != domain.ErrPreferencesNotFound { t.Errorf("expected ErrPreferencesNotFound, got %v", err) } }) t.Run("returns preferences after upsert", func(t *testing.T) { _, _ = svc.Upsert(ctx, "user-1", &domain.PreferencesUpdate{ Theme: strPtr("dark"), }) prefs, err := svc.Get(ctx, "user-1") if err != nil { t.Fatalf("unexpected error: %v", err) } if prefs.Theme != "dark" { t.Errorf("expected theme 'dark', got '%s'", prefs.Theme) } }) } func TestPreferencesService_Upsert(t *testing.T) { repo := memory.NewPreferencesRepository() svc := NewPreferencesService(repo, logging.Nop()) ctx := context.Background() t.Run("creates new with defaults and merges", func(t *testing.T) { prefs, err := svc.Upsert(ctx, "user-new", &domain.PreferencesUpdate{ Theme: strPtr("light"), }) if err != nil { t.Fatalf("unexpected error: %v", err) } if prefs.Theme != "light" { t.Errorf("expected theme 'light', got '%s'", prefs.Theme) } if prefs.Language != "en" { t.Errorf("expected default language 'en', got '%s'", prefs.Language) } if !prefs.Notifications.Email { t.Error("expected default email=true") } }) t.Run("updates existing with merge", func(t *testing.T) { // First upsert _, _ = svc.Upsert(ctx, "user-merge", &domain.PreferencesUpdate{ Theme: strPtr("dark"), Language: strPtr("fr"), }) // Second upsert - only change language prefs, err := svc.Upsert(ctx, "user-merge", &domain.PreferencesUpdate{ Language: strPtr("es"), }) if err != nil { t.Fatalf("unexpected error: %v", err) } if prefs.Theme != "dark" { t.Errorf("expected theme 'dark' unchanged, got '%s'", prefs.Theme) } if prefs.Language != "es" { t.Errorf("expected language 'es', got '%s'", prefs.Language) } }) t.Run("rejects invalid theme", func(t *testing.T) { _, err := svc.Upsert(ctx, "user-invalid", &domain.PreferencesUpdate{ Theme: strPtr("blue"), }) if err != domain.ErrInvalidTheme { t.Errorf("expected ErrInvalidTheme, got %v", err) } }) t.Run("rejects invalid language", func(t *testing.T) { _, err := svc.Upsert(ctx, "user-invalid2", &domain.PreferencesUpdate{ Language: strPtr("XYZ"), }) if err != domain.ErrInvalidLanguage { t.Errorf("expected ErrInvalidLanguage, got %v", err) } }) t.Run("rejects invalid digest", func(t *testing.T) { _, err := svc.Upsert(ctx, "user-invalid3", &domain.PreferencesUpdate{ Notifications: &domain.NotificationSettingsUpdate{ Digest: strPtr("monthly"), }, }) if err != domain.ErrInvalidDigest { t.Errorf("expected ErrInvalidDigest, got %v", err) } }) t.Run("sets UpdatedAt", func(t *testing.T) { prefs, err := svc.Upsert(ctx, "user-time", &domain.PreferencesUpdate{ Theme: strPtr("dark"), }) if err != nil { t.Fatalf("unexpected error: %v", err) } if prefs.UpdatedAt.IsZero() { t.Error("expected non-zero UpdatedAt") } }) t.Run("notification sub-field merge preserves unset fields", func(t *testing.T) { _, _ = svc.Upsert(ctx, "user-notif", &domain.PreferencesUpdate{ Notifications: &domain.NotificationSettingsUpdate{ Email: boolPtr(false), Push: boolPtr(false), Digest: strPtr("daily"), }, }) prefs, err := svc.Upsert(ctx, "user-notif", &domain.PreferencesUpdate{ Notifications: &domain.NotificationSettingsUpdate{ Email: boolPtr(true), }, }) if err != nil { t.Fatalf("unexpected error: %v", err) } if !prefs.Notifications.Email { t.Error("expected email=true") } if prefs.Notifications.Push != false { t.Error("expected push=false preserved") } if prefs.Notifications.Digest != "daily" { t.Errorf("expected digest 'daily' preserved, got '%s'", prefs.Notifications.Digest) } }) }