slack5-1770529463/services/preferences-api/internal/api/handlers/preference_test.go
rdev-worker 73532902e7
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
build: /implement-feature user-preferences
2026-02-08 06:13:10 +00:00

302 lines
8.8 KiB
Go

package handlers
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"sync"
"testing"
"github.com/go-chi/chi/v5"
"git.threesix.ai/jordan/slack5-1770529463/pkg/app"
"git.threesix.ai/jordan/slack5-1770529463/pkg/logging"
"git.threesix.ai/jordan/slack5-1770529463/services/preferences-api/internal/port"
"git.threesix.ai/jordan/slack5-1770529463/services/preferences-api/internal/service"
)
// mockPreferenceRepository implements port.PreferenceRepository for testing.
type mockPreferenceRepository struct {
mu sync.RWMutex
prefs map[string]map[string]string
}
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 newTestPreferenceHandler() (*Preference, *mockPreferenceRepository) {
repo := newMockPreferenceRepository()
svc := service.NewPreferenceService(repo, logging.Nop())
handler := NewPreference(svc, logging.Nop())
return handler, repo
}
func TestPreference_GetPreferences(t *testing.T) {
handler, repo := newTestPreferenceHandler()
validUserID := "550e8400-e29b-41d4-a716-446655440000"
t.Run("returns defaults for new user", func(t *testing.T) {
r := chi.NewRouter()
r.Get("/api/preferences-api/preferences/{user_id}", app.Wrap(handler.GetPreferences))
req := httptest.NewRequest(http.MethodGet, "/api/preferences-api/preferences/"+validUserID, nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d", w.Code)
}
var resp map[string]any
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
t.Fatalf("failed to decode response: %v", err)
}
data, ok := resp["data"].(map[string]any)
if !ok {
t.Fatal("expected 'data' field as object in response")
}
if data["user_id"] != validUserID {
t.Errorf("expected user_id %s, got %v", validUserID, data["user_id"])
}
prefs, ok := data["preferences"].(map[string]any)
if !ok {
t.Fatal("expected 'preferences' field as object in data")
}
if prefs["theme"] != "system" {
t.Errorf("expected default theme 'system', got %v", prefs["theme"])
}
if prefs["language"] != "en" {
t.Errorf("expected default language 'en', got %v", prefs["language"])
}
if prefs["notifications_enabled"] != true {
t.Errorf("expected default notifications_enabled true, got %v", prefs["notifications_enabled"])
}
})
t.Run("returns stored preferences merged with defaults", func(t *testing.T) {
_ = repo.Upsert(context.Background(), validUserID, "theme", "dark")
r := chi.NewRouter()
r.Get("/api/preferences-api/preferences/{user_id}", app.Wrap(handler.GetPreferences))
req := httptest.NewRequest(http.MethodGet, "/api/preferences-api/preferences/"+validUserID, nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d", w.Code)
}
var resp map[string]any
_ = json.NewDecoder(w.Body).Decode(&resp)
data := resp["data"].(map[string]any)
prefs := data["preferences"].(map[string]any)
if prefs["theme"] != "dark" {
t.Errorf("expected theme 'dark', got %v", prefs["theme"])
}
})
t.Run("returns 400 for invalid user_id", func(t *testing.T) {
r := chi.NewRouter()
r.Get("/api/preferences-api/preferences/{user_id}", app.Wrap(handler.GetPreferences))
req := httptest.NewRequest(http.MethodGet, "/api/preferences-api/preferences/not-a-uuid", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d", w.Code)
}
})
t.Run("response has meta field", func(t *testing.T) {
r := chi.NewRouter()
r.Get("/api/preferences-api/preferences/{user_id}", app.Wrap(handler.GetPreferences))
req := httptest.NewRequest(http.MethodGet, "/api/preferences-api/preferences/"+validUserID, nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
var resp map[string]any
_ = json.NewDecoder(w.Body).Decode(&resp)
if _, ok := resp["meta"]; !ok {
t.Error("expected 'meta' field in response")
}
})
}
func TestPreference_UpdatePreferences(t *testing.T) {
handler, _ := newTestPreferenceHandler()
validUserID := "550e8400-e29b-41d4-a716-446655440000"
t.Run("updates preferences successfully", func(t *testing.T) {
r := chi.NewRouter()
r.Put("/api/preferences-api/preferences/{user_id}", app.Wrap(handler.UpdatePreferences))
body, _ := json.Marshal(UpdatePreferencesRequest{
Preferences: map[string]any{
"theme": "dark",
"language": "fr",
},
})
req := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/"+validUserID, bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d; body: %s", w.Code, w.Body.String())
}
var resp map[string]any
_ = json.NewDecoder(w.Body).Decode(&resp)
data := resp["data"].(map[string]any)
prefs := data["preferences"].(map[string]any)
if prefs["theme"] != "dark" {
t.Errorf("expected theme 'dark', got %v", prefs["theme"])
}
if prefs["language"] != "fr" {
t.Errorf("expected language 'fr', got %v", prefs["language"])
}
// Default should still be present
if prefs["notifications_enabled"] != true {
t.Errorf("expected notifications_enabled true, got %v", prefs["notifications_enabled"])
}
})
t.Run("returns 400 for unknown key", func(t *testing.T) {
r := chi.NewRouter()
r.Put("/api/preferences-api/preferences/{user_id}", app.Wrap(handler.UpdatePreferences))
body, _ := json.Marshal(UpdatePreferencesRequest{
Preferences: map[string]any{
"font_size": "large",
},
})
req := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/"+validUserID, bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d", w.Code)
}
})
t.Run("returns 400 for invalid value", func(t *testing.T) {
r := chi.NewRouter()
r.Put("/api/preferences-api/preferences/{user_id}", app.Wrap(handler.UpdatePreferences))
body, _ := json.Marshal(UpdatePreferencesRequest{
Preferences: map[string]any{
"theme": "neon",
},
})
req := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/"+validUserID, bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d", w.Code)
}
})
t.Run("returns 400 for invalid user_id", func(t *testing.T) {
r := chi.NewRouter()
r.Put("/api/preferences-api/preferences/{user_id}", app.Wrap(handler.UpdatePreferences))
body, _ := json.Marshal(UpdatePreferencesRequest{
Preferences: map[string]any{
"theme": "dark",
},
})
req := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/bad-id", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d", w.Code)
}
})
t.Run("handles boolean notifications_enabled", func(t *testing.T) {
r := chi.NewRouter()
r.Put("/api/preferences-api/preferences/{user_id}", app.Wrap(handler.UpdatePreferences))
body, _ := json.Marshal(UpdatePreferencesRequest{
Preferences: map[string]any{
"notifications_enabled": false,
},
})
req := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/"+validUserID, bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected status 200, got %d; body: %s", w.Code, w.Body.String())
}
var resp map[string]any
_ = json.NewDecoder(w.Body).Decode(&resp)
data := resp["data"].(map[string]any)
prefs := data["preferences"].(map[string]any)
if prefs["notifications_enabled"] != false {
t.Errorf("expected notifications_enabled false, got %v", prefs["notifications_enabled"])
}
})
}