slate-test-1770505673/services/preferences-api/internal/api/handlers/preference_test.go
rdev-worker 868f79c67a
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
build: /implement-feature user-preferences
2026-02-07 23:47:42 +00:00

244 lines
7.7 KiB
Go

package handlers
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/go-chi/chi/v5"
"git.threesix.ai/jordan/slate-test-1770505673/pkg/app"
"git.threesix.ai/jordan/slate-test-1770505673/pkg/auth"
"git.threesix.ai/jordan/slate-test-1770505673/pkg/logging"
"git.threesix.ai/jordan/slate-test-1770505673/services/preferences-api/internal/port"
"git.threesix.ai/jordan/slate-test-1770505673/services/preferences-api/internal/service"
)
const testUserID = "550e8400-e29b-41d4-a716-446655440000"
const otherUserID = "550e8400-e29b-41d4-a716-446655440001"
// mockPreferenceRepository implements port.PreferenceRepository for testing.
type mockPreferenceRepository struct {
data map[string]map[string]string
}
var _ port.PreferenceRepository = (*mockPreferenceRepository)(nil)
func newMockRepo() *mockPreferenceRepository {
return &mockPreferenceRepository{
data: make(map[string]map[string]string),
}
}
func (m *mockPreferenceRepository) GetByUserID(_ context.Context, userID string) (map[string]string, error) {
prefs, ok := m.data[userID]
if !ok {
return make(map[string]string), nil
}
result := make(map[string]string, len(prefs))
for k, v := range prefs {
result[k] = v
}
return result, nil
}
func (m *mockPreferenceRepository) Upsert(_ context.Context, userID string, prefs map[string]string) error {
if m.data[userID] == nil {
m.data[userID] = make(map[string]string)
}
for k, v := range prefs {
m.data[userID][k] = v
}
return nil
}
func newTestHandler() (*Preference, *mockPreferenceRepository) {
repo := newMockRepo()
svc := service.NewPreferenceService(repo, logging.Nop())
handler := NewPreference(svc, logging.Nop())
return handler, repo
}
// setupRouter creates a test router with the handler wrapped in app.Wrap.
func setupRouter(handler *Preference) *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
}
// withAuth adds an authenticated user to the request context.
func withAuth(r *http.Request, userID string) *http.Request {
ctx := auth.SetUser(r.Context(), &auth.User{ID: userID})
return r.WithContext(ctx)
}
func TestPreference_Get(t *testing.T) {
handler, repo := newTestHandler()
router := setupRouter(handler)
t.Run("returns empty prefs for user with no preferences", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/api/preferences-api/preferences/"+testUserID, nil)
req = withAuth(req, testUserID)
w := httptest.NewRecorder()
router.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, ok := resp["data"].(map[string]any)
if !ok {
t.Fatal("expected 'data' to be an object")
}
if len(data) != 0 {
t.Errorf("expected empty data, got %v", data)
}
})
t.Run("returns preferences for user with data", func(t *testing.T) {
repo.data[testUserID] = map[string]string{"theme": "dark", "language": "en"}
req := httptest.NewRequest(http.MethodGet, "/api/preferences-api/preferences/"+testUserID, nil)
req = withAuth(req, testUserID)
w := httptest.NewRecorder()
router.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)
if data["theme"] != "dark" {
t.Errorf("expected theme 'dark', got '%v'", data["theme"])
}
if data["language"] != "en" {
t.Errorf("expected language 'en', got '%v'", data["language"])
}
// Clean up
delete(repo.data, testUserID)
})
t.Run("returns 400 for invalid UUID", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/api/preferences-api/preferences/not-a-uuid", nil)
req = withAuth(req, testUserID)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d", w.Code)
}
})
t.Run("returns 403 for accessing another user's preferences", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/api/preferences-api/preferences/"+otherUserID, nil)
req = withAuth(req, testUserID)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusForbidden {
t.Errorf("expected status 403, got %d", w.Code)
}
})
}
func TestPreference_Update(t *testing.T) {
handler, _ := newTestHandler()
router := setupRouter(handler)
t.Run("updates preferences successfully", func(t *testing.T) {
body, _ := json.Marshal(map[string]string{"theme": "dark", "language": "fr"})
req := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/"+testUserID, bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req = withAuth(req, testUserID)
w := httptest.NewRecorder()
router.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)
if data["theme"] != "dark" {
t.Errorf("expected theme 'dark', got '%v'", data["theme"])
}
if data["language"] != "fr" {
t.Errorf("expected language 'fr', got '%v'", data["language"])
}
})
t.Run("returns 400 for unknown key", func(t *testing.T) {
body, _ := json.Marshal(map[string]string{"unknown_key": "val"})
req := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/"+testUserID, bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req = withAuth(req, testUserID)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d; body: %s", w.Code, w.Body.String())
}
})
t.Run("returns 400 for invalid value", func(t *testing.T) {
body, _ := json.Marshal(map[string]string{"theme": "blue"})
req := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/"+testUserID, bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req = withAuth(req, testUserID)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d; body: %s", w.Code, w.Body.String())
}
})
t.Run("returns 400 for empty body", func(t *testing.T) {
req := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/"+testUserID, nil)
req.Header.Set("Content-Type", "application/json")
req = withAuth(req, testUserID)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d; body: %s", w.Code, w.Body.String())
}
})
t.Run("returns 400 for invalid UUID", func(t *testing.T) {
body, _ := json.Marshal(map[string]string{"theme": "dark"})
req := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/not-a-uuid", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req = withAuth(req, testUserID)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusBadRequest {
t.Errorf("expected status 400, got %d", w.Code)
}
})
t.Run("returns 403 for accessing another user's preferences", func(t *testing.T) {
body, _ := json.Marshal(map[string]string{"theme": "dark"})
req := httptest.NewRequest(http.MethodPut, "/api/preferences-api/preferences/"+otherUserID, bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req = withAuth(req, testUserID)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusForbidden {
t.Errorf("expected status 403, got %d", w.Code)
}
})
}