// Session 49 — /api/preferences (auth-gated, user_metadata storage). const request = require('supertest'); const mockFrom = jest.fn(); const mockGetUser = jest.fn(); const mockGetUserById = jest.fn(); const mockUpdateUserById = jest.fn(); jest.mock('../../src/utils/supabase', () => ({ getSupabaseClient: () => ({ auth: { getUser: mockGetUser, admin: { getUserById: mockGetUserById, updateUserById: mockUpdateUserById } }, from: mockFrom }), getSupabaseServiceClient: () => ({ auth: { getUser: mockGetUser, admin: { getUserById: mockGetUserById, updateUserById: mockUpdateUserById } }, from: mockFrom }), })); const app = require('../../src/app'); const USER_ID = 'user-1'; function authOk() { mockGetUser.mockResolvedValue({ data: { user: { id: USER_ID, email: 'a@b.com' } }, error: null }); // requireAuth's users-row lookup mockFrom.mockImplementation(() => ({ select: () => ({ eq: () => ({ single: () => Promise.resolve({ data: { id: USER_ID, email: 'a@b.com', tier: 'free' }, error: null }) }) }), })); } function metaWith(preferences) { mockGetUserById.mockResolvedValue({ data: { user: { id: USER_ID, user_metadata: preferences ? { preferences } : {} } }, error: null }); mockUpdateUserById.mockResolvedValue({ data: { user: { id: USER_ID } }, error: null }); } beforeEach(() => { jest.clearAllMocks(); }); describe('GET /api/preferences', () => { it('requires auth (401 without token)', async () => { const res = await request(app).get('/api/preferences'); expect(res.status).toBe(401); }); it('returns defaults for a user with no saved prefs', async () => { authOk(); metaWith(null); const res = await request(app).get('/api/preferences').set('Authorization', 'Bearer t'); expect(res.status).toBe(200); expect(res.body).toEqual({ sports: [], books: [], weekly_limit: null, onboarding_complete: false }); }); it('returns saved preferences', async () => { authOk(); metaWith({ sports: ['mlb', 'nba'], books: ['draftkings'], weekly_limit: 250, onboarding_complete: true }); const res = await request(app).get('/api/preferences').set('Authorization', 'Bearer t'); expect(res.body.sports).toEqual(['mlb', 'nba']); expect(res.body.weekly_limit).toBe(250); expect(res.body.onboarding_complete).toBe(true); }); }); describe('POST /api/preferences', () => { it('requires auth', async () => { const res = await request(app).post('/api/preferences').send({ sports: ['mlb'] }); expect(res.status).toBe(401); }); it('saves and returns the merged preferences (sanitized)', async () => { authOk(); metaWith(null); const res = await request(app).post('/api/preferences').set('Authorization', 'Bearer t') .send({ sports: ['MLB', 'nba', 'cricket'], books: ['draftkings'], weekly_limit: 250, onboarding_complete: true }); expect(res.status).toBe(200); expect(res.body.ok).toBe(true); expect(res.body.preferences.sports).toEqual(['mlb', 'nba']); // lowercased, invalid dropped expect(res.body.preferences.onboarding_complete).toBe(true); expect(mockUpdateUserById).toHaveBeenCalledWith(USER_ID, expect.objectContaining({ user_metadata: expect.objectContaining({ preferences: expect.any(Object) }) })); }); it('partial update merges with existing (does not overwrite other keys)', async () => { authOk(); metaWith({ sports: ['mlb'], books: ['fanduel'], weekly_limit: 100, onboarding_complete: true }); const res = await request(app).post('/api/preferences').set('Authorization', 'Bearer t').send({ weekly_limit: 500 }); expect(res.body.preferences.weekly_limit).toBe(500); expect(res.body.preferences.sports).toEqual(['mlb']); // preserved expect(res.body.preferences.books).toEqual(['fanduel']); // preserved }); });