slate-v3-1770514618/services/preferences-api/internal/api/handlers/preferences_test.go
rdev-worker 1afe983cd6
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
build: /implement-feature user-preferences
2026-02-08 02:02:18 +00:00

304 lines
9.9 KiB
Go

package handlers
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/go-chi/chi/v5"
"git.threesix.ai/jordan/slate-v3-1770514618/pkg/app"
"git.threesix.ai/jordan/slate-v3-1770514618/pkg/auth"
"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/service"
)
func newTestPreferencesHandler() *Preferences {
repo := memory.NewPreferencesRepository()
svc := service.NewPreferencesService(repo, logging.Nop())
return NewPreferences(svc)
}
func withAuthUser(r *http.Request, userID string, roles ...string) *http.Request {
user := &auth.User{ID: userID, Roles: roles}
ctx := auth.SetUser(r.Context(), user)
return r.WithContext(ctx)
}
func setupRouter(handler *Preferences) *chi.Mux {
r := chi.NewRouter()
r.Get("/api/preferences-api/preferences/{user_id}", app.Wrap(handler.Get))
r.Put("/api/preferences-api/preferences/{user_id}", app.Wrap(handler.Update))
return r
}
func TestPreferences_Get_NotFound(t *testing.T) {
handler := newTestPreferencesHandler()
router := setupRouter(handler)
req := httptest.NewRequest(http.MethodGet, "/api/preferences-api/preferences/550e8400-e29b-41d4-a716-446655440000", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusNotFound {
t.Errorf("expected 404, got %d", w.Code)
}
}
func TestPreferences_Get_InvalidUUID(t *testing.T) {
handler := newTestPreferencesHandler()
router := setupRouter(handler)
req := httptest.NewRequest(http.MethodGet, "/api/preferences-api/preferences/not-a-uuid", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400, got %d", w.Code)
}
}
func TestPreferences_Get_Forbidden(t *testing.T) {
handler := newTestPreferencesHandler()
r := chi.NewRouter()
r.Get("/api/preferences-api/preferences/{user_id}", app.Wrap(handler.Get))
req := httptest.NewRequest(http.MethodGet, "/api/preferences-api/preferences/550e8400-e29b-41d4-a716-446655440000", nil)
req = withAuthUser(req, "different-user-id")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusForbidden {
t.Errorf("expected 403, got %d", w.Code)
}
}
func TestPreferences_Get_AdminAccess(t *testing.T) {
handler := newTestPreferencesHandler()
// First create preferences
r := chi.NewRouter()
r.Put("/api/preferences-api/preferences/{user_id}", app.Wrap(handler.Update))
r.Get("/api/preferences-api/preferences/{user_id}", app.Wrap(handler.Get))
body := `{"preferences":{"theme":"dark"}}`
putReq := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/550e8400-e29b-41d4-a716-446655440000", bytes.NewBufferString(body))
putReq.Header.Set("Content-Type", "application/json")
putReq = withAuthUser(putReq, "550e8400-e29b-41d4-a716-446655440000")
putW := httptest.NewRecorder()
r.ServeHTTP(putW, putReq)
// Admin accesses another user's prefs
getReq := httptest.NewRequest(http.MethodGet, "/api/preferences-api/preferences/550e8400-e29b-41d4-a716-446655440000", nil)
getReq = withAuthUser(getReq, "admin-user-id", "admin")
getW := httptest.NewRecorder()
r.ServeHTTP(getW, getReq)
if getW.Code != http.StatusOK {
t.Errorf("expected 200 for admin access, got %d", getW.Code)
}
}
func TestPreferences_Update_CreateNew(t *testing.T) {
handler := newTestPreferencesHandler()
router := setupRouter(handler)
body := `{"preferences":{"theme":"dark","language":"fr"}}`
req := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/550e8400-e29b-41d4-a716-446655440000", bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("expected 200, got %d; body: %s", w.Code, w.Body.String())
}
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 in response")
}
prefs, ok := data["preferences"].(map[string]any)
if !ok {
t.Fatal("expected 'preferences' field in data")
}
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"])
}
}
func TestPreferences_Update_MergeExisting(t *testing.T) {
handler := newTestPreferencesHandler()
router := setupRouter(handler)
userID := "550e8400-e29b-41d4-a716-446655440000"
// Create initial
body1 := `{"preferences":{"theme":"dark","language":"fr"}}`
req1 := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/"+userID, bytes.NewBufferString(body1))
req1.Header.Set("Content-Type", "application/json")
w1 := httptest.NewRecorder()
router.ServeHTTP(w1, req1)
// Update only theme
body2 := `{"preferences":{"theme":"light"}}`
req2 := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/"+userID, bytes.NewBufferString(body2))
req2.Header.Set("Content-Type", "application/json")
w2 := httptest.NewRecorder()
router.ServeHTTP(w2, req2)
if w2.Code != http.StatusOK {
t.Fatalf("expected 200, got %d; body: %s", w2.Code, w2.Body.String())
}
var resp map[string]any
_ = json.NewDecoder(w2.Body).Decode(&resp)
data := resp["data"].(map[string]any)
prefs := data["preferences"].(map[string]any)
if prefs["theme"] != "light" {
t.Errorf("expected theme 'light', got %v", prefs["theme"])
}
if prefs["language"] != "fr" {
t.Errorf("expected language 'fr' preserved, got %v", prefs["language"])
}
}
func TestPreferences_Update_InvalidTheme(t *testing.T) {
handler := newTestPreferencesHandler()
router := setupRouter(handler)
body := `{"preferences":{"theme":"blue"}}`
req := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/550e8400-e29b-41d4-a716-446655440000", bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400, got %d", w.Code)
}
}
func TestPreferences_Update_UnknownPreferenceKey(t *testing.T) {
handler := newTestPreferencesHandler()
router := setupRouter(handler)
body := `{"preferences":{"theme":"dark","unknown_key":"value"}}`
req := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/550e8400-e29b-41d4-a716-446655440000", bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400 for unknown preference key, got %d; body: %s", w.Code, w.Body.String())
}
}
func TestPreferences_Update_MissingPreferencesField(t *testing.T) {
handler := newTestPreferencesHandler()
router := setupRouter(handler)
body := `{"theme":"dark"}`
req := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/550e8400-e29b-41d4-a716-446655440000", bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400 for missing preferences field, got %d; body: %s", w.Code, w.Body.String())
}
}
func TestPreferences_Update_EmptyBody(t *testing.T) {
handler := newTestPreferencesHandler()
router := setupRouter(handler)
req := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/550e8400-e29b-41d4-a716-446655440000", nil)
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400 for empty body, got %d", w.Code)
}
}
func TestPreferences_Update_Forbidden(t *testing.T) {
handler := newTestPreferencesHandler()
r := chi.NewRouter()
r.Put("/api/preferences-api/preferences/{user_id}", app.Wrap(handler.Update))
body := `{"preferences":{"theme":"dark"}}`
req := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/550e8400-e29b-41d4-a716-446655440000", bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
req = withAuthUser(req, "different-user-id")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != http.StatusForbidden {
t.Errorf("expected 403, got %d", w.Code)
}
}
func TestPreferences_Update_InvalidUUID(t *testing.T) {
handler := newTestPreferencesHandler()
router := setupRouter(handler)
body := `{"preferences":{"theme":"dark"}}`
req := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/not-valid", bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected 400, got %d", w.Code)
}
}
func TestPreferences_GetAfterUpdate(t *testing.T) {
handler := newTestPreferencesHandler()
router := setupRouter(handler)
userID := "550e8400-e29b-41d4-a716-446655440000"
// Create via PUT
body := `{"preferences":{"theme":"dark"}}`
putReq := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/"+userID, bytes.NewBufferString(body))
putReq.Header.Set("Content-Type", "application/json")
putW := httptest.NewRecorder()
router.ServeHTTP(putW, putReq)
// GET
getReq := httptest.NewRequest(http.MethodGet, "/api/preferences-api/preferences/"+userID, nil)
getW := httptest.NewRecorder()
router.ServeHTTP(getW, getReq)
if getW.Code != http.StatusOK {
t.Fatalf("expected 200, got %d; body: %s", getW.Code, getW.Body.String())
}
var resp map[string]any
_ = json.NewDecoder(getW.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"] != "en" {
t.Errorf("expected default language 'en', got %v", prefs["language"])
}
}