254 lines
7.7 KiB
Go
254 lines
7.7 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"testing"
|
|
|
|
"git.threesix.ai/jordan/slack5-1770529463/pkg/logging"
|
|
"git.threesix.ai/jordan/slack5-1770529463/services/preferences-api/internal/domain"
|
|
"git.threesix.ai/jordan/slack5-1770529463/services/preferences-api/internal/port"
|
|
)
|
|
|
|
// mockPreferenceRepository implements port.PreferenceRepository for testing.
|
|
type mockPreferenceRepository struct {
|
|
mu sync.RWMutex
|
|
prefs map[string]map[string]string // userID -> key -> value
|
|
}
|
|
|
|
var _ port.PreferenceRepository = (*mockPreferenceRepository)(nil)
|
|
|
|
func newMockPreferenceRepository() *mockPreferenceRepository {
|
|
return &mockPreferenceRepository{
|
|
prefs: make(map[string]map[string]string),
|
|
}
|
|
}
|
|
|
|
func (m *mockPreferenceRepository) GetByUserID(ctx context.Context, userID string) ([]port.PreferenceRow, error) {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
|
|
userPrefs, ok := m.prefs[userID]
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
|
|
rows := make([]port.PreferenceRow, 0, len(userPrefs))
|
|
for k, v := range userPrefs {
|
|
rows = append(rows, port.PreferenceRow{
|
|
UserID: userID,
|
|
Key: k,
|
|
Value: v,
|
|
})
|
|
}
|
|
return rows, nil
|
|
}
|
|
|
|
func (m *mockPreferenceRepository) Upsert(ctx context.Context, userID string, key string, value string) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
if m.prefs[userID] == nil {
|
|
m.prefs[userID] = make(map[string]string)
|
|
}
|
|
m.prefs[userID][key] = value
|
|
return nil
|
|
}
|
|
|
|
func TestPreferenceService_GetPreferences(t *testing.T) {
|
|
repo := newMockPreferenceRepository()
|
|
svc := NewPreferenceService(repo, logging.Nop())
|
|
ctx := context.Background()
|
|
validUserID := "550e8400-e29b-41d4-a716-446655440000"
|
|
|
|
t.Run("returns defaults when no preferences stored", func(t *testing.T) {
|
|
result, err := svc.GetPreferences(ctx, validUserID)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if result.UserID != validUserID {
|
|
t.Errorf("expected user_id %s, got %s", validUserID, result.UserID)
|
|
}
|
|
if result.Preferences["theme"] != "system" {
|
|
t.Errorf("expected default theme 'system', got %v", result.Preferences["theme"])
|
|
}
|
|
if result.Preferences["language"] != "en" {
|
|
t.Errorf("expected default language 'en', got %v", result.Preferences["language"])
|
|
}
|
|
if result.Preferences["notifications_enabled"] != true {
|
|
t.Errorf("expected default notifications_enabled true, got %v", result.Preferences["notifications_enabled"])
|
|
}
|
|
})
|
|
|
|
t.Run("merges stored values with defaults", func(t *testing.T) {
|
|
// Store only theme
|
|
_ = repo.Upsert(ctx, validUserID, "theme", "dark")
|
|
|
|
result, err := svc.GetPreferences(ctx, validUserID)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if result.Preferences["theme"] != "dark" {
|
|
t.Errorf("expected theme 'dark', got %v", result.Preferences["theme"])
|
|
}
|
|
if result.Preferences["language"] != "en" {
|
|
t.Errorf("expected default language 'en', got %v", result.Preferences["language"])
|
|
}
|
|
if result.Preferences["notifications_enabled"] != true {
|
|
t.Errorf("expected default notifications_enabled true, got %v", result.Preferences["notifications_enabled"])
|
|
}
|
|
})
|
|
|
|
t.Run("returns all stored values when all keys present", func(t *testing.T) {
|
|
userID := "660e8400-e29b-41d4-a716-446655440000"
|
|
_ = repo.Upsert(ctx, userID, "theme", "light")
|
|
_ = repo.Upsert(ctx, userID, "language", "fr")
|
|
_ = repo.Upsert(ctx, userID, "notifications_enabled", "false")
|
|
|
|
result, err := svc.GetPreferences(ctx, userID)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if result.Preferences["theme"] != "light" {
|
|
t.Errorf("expected theme 'light', got %v", result.Preferences["theme"])
|
|
}
|
|
if result.Preferences["language"] != "fr" {
|
|
t.Errorf("expected language 'fr', got %v", result.Preferences["language"])
|
|
}
|
|
if result.Preferences["notifications_enabled"] != false {
|
|
t.Errorf("expected notifications_enabled false, got %v", result.Preferences["notifications_enabled"])
|
|
}
|
|
})
|
|
|
|
t.Run("returns error for invalid user_id", func(t *testing.T) {
|
|
_, err := svc.GetPreferences(ctx, "not-a-uuid")
|
|
if err != domain.ErrInvalidUserID {
|
|
t.Errorf("expected ErrInvalidUserID, got %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestPreferenceService_UpdatePreferences(t *testing.T) {
|
|
repo := newMockPreferenceRepository()
|
|
svc := NewPreferenceService(repo, logging.Nop())
|
|
ctx := context.Background()
|
|
validUserID := "550e8400-e29b-41d4-a716-446655440000"
|
|
|
|
t.Run("updates preferences and returns full result", func(t *testing.T) {
|
|
result, err := svc.UpdatePreferences(ctx, validUserID, map[string]any{
|
|
"theme": "dark",
|
|
"language": "fr",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if result.Preferences["theme"] != "dark" {
|
|
t.Errorf("expected theme 'dark', got %v", result.Preferences["theme"])
|
|
}
|
|
if result.Preferences["language"] != "fr" {
|
|
t.Errorf("expected language 'fr', got %v", result.Preferences["language"])
|
|
}
|
|
// notifications_enabled should be default
|
|
if result.Preferences["notifications_enabled"] != true {
|
|
t.Errorf("expected default notifications_enabled true, got %v", result.Preferences["notifications_enabled"])
|
|
}
|
|
})
|
|
|
|
t.Run("handles boolean input for notifications_enabled", func(t *testing.T) {
|
|
result, err := svc.UpdatePreferences(ctx, validUserID, map[string]any{
|
|
"notifications_enabled": false,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if result.Preferences["notifications_enabled"] != false {
|
|
t.Errorf("expected notifications_enabled false, got %v", result.Preferences["notifications_enabled"])
|
|
}
|
|
})
|
|
|
|
t.Run("rejects unknown key", func(t *testing.T) {
|
|
_, err := svc.UpdatePreferences(ctx, validUserID, map[string]any{
|
|
"font_size": "large",
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error for unknown key")
|
|
}
|
|
// Should wrap ErrUnknownPreferenceKey
|
|
if !containsError(err, domain.ErrUnknownPreferenceKey) {
|
|
t.Errorf("expected ErrUnknownPreferenceKey, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("rejects invalid theme value", func(t *testing.T) {
|
|
_, err := svc.UpdatePreferences(ctx, validUserID, map[string]any{
|
|
"theme": "neon",
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error for invalid theme")
|
|
}
|
|
if !containsError(err, domain.ErrInvalidPreferenceValue) {
|
|
t.Errorf("expected ErrInvalidPreferenceValue, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("rejects invalid language value", func(t *testing.T) {
|
|
_, err := svc.UpdatePreferences(ctx, validUserID, map[string]any{
|
|
"language": "123",
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error for invalid language")
|
|
}
|
|
if !containsError(err, domain.ErrInvalidPreferenceValue) {
|
|
t.Errorf("expected ErrInvalidPreferenceValue, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("rejects invalid user_id", func(t *testing.T) {
|
|
_, err := svc.UpdatePreferences(ctx, "bad-id", map[string]any{
|
|
"theme": "dark",
|
|
})
|
|
if err != domain.ErrInvalidUserID {
|
|
t.Errorf("expected ErrInvalidUserID, got %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("is idempotent", func(t *testing.T) {
|
|
userID := "770e8400-e29b-41d4-a716-446655440000"
|
|
input := map[string]any{"theme": "dark"}
|
|
|
|
result1, err := svc.UpdatePreferences(ctx, userID, input)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
result2, err := svc.UpdatePreferences(ctx, userID, input)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if result1.Preferences["theme"] != result2.Preferences["theme"] {
|
|
t.Errorf("expected idempotent result, got %v and %v", result1.Preferences["theme"], result2.Preferences["theme"])
|
|
}
|
|
})
|
|
}
|
|
|
|
// containsError checks if err wraps target using errors.Is-like behavior with fmt.Errorf wrapping.
|
|
func containsError(err, target error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
for e := err; e != nil; {
|
|
if e == target {
|
|
return true
|
|
}
|
|
if e.Error() == target.Error() {
|
|
return true
|
|
}
|
|
u, ok := e.(interface{ Unwrap() error })
|
|
if !ok {
|
|
break
|
|
}
|
|
e = u.Unwrap()
|
|
}
|
|
return false
|
|
}
|