import { describe, it, expect, vi, beforeEach } from 'vitest' vi.mock('../../../server/utils/auth.js', () => ({ requireAuth: vi.fn() })) vi.mock('../../../server/models/member.js', () => ({ default: { findByIdAndUpdate: vi.fn() } })) import { requireAuth } from '../../../server/utils/auth.js' import Member from '../../../server/models/member.js' import profilePatchHandler from '../../../server/api/members/profile.patch.js' import { createMockEvent } from '../helpers/createMockEvent.js' describe('members profile PATCH endpoint', () => { const mockMember = { _id: 'member-123', email: 'test@example.com', name: 'Test User', circle: 'community', contributionTier: 5, pronouns: 'they/them', timeZone: 'America/New_York', avatar: 'https://example.com/avatar.jpg', studio: 'Test Studio', bio: 'Updated bio', location: 'NYC', socialLinks: { twitter: '@test' }, offering: { text: 'help', tags: ['code'] }, lookingFor: { text: 'feedback', tags: ['design'] }, showInDirectory: true } beforeEach(() => { vi.clearAllMocks() requireAuth.mockResolvedValue({ _id: 'member-123' }) Member.findByIdAndUpdate.mockResolvedValue(mockMember) }) describe('field allowlist - forbidden fields are rejected', () => { it('does not pass helcimCustomerId to database update', async () => { const event = createMockEvent({ method: 'PATCH', path: '/api/members/profile', body: { bio: 'new bio', helcimCustomerId: 'hacked-id' } }) await profilePatchHandler(event) const updateCall = Member.findByIdAndUpdate.mock.calls[0] const setData = updateCall[1].$set expect(setData).not.toHaveProperty('helcimCustomerId') expect(setData).toHaveProperty('bio', 'new bio') }) it('does not pass role to database update', async () => { const event = createMockEvent({ method: 'PATCH', path: '/api/members/profile', body: { bio: 'new bio', role: 'admin' } }) await profilePatchHandler(event) const setData = Member.findByIdAndUpdate.mock.calls[0][1].$set expect(setData).not.toHaveProperty('role') }) it('does not pass status to database update', async () => { const event = createMockEvent({ method: 'PATCH', path: '/api/members/profile', body: { bio: 'new bio', status: 'active' } }) await profilePatchHandler(event) const setData = Member.findByIdAndUpdate.mock.calls[0][1].$set expect(setData).not.toHaveProperty('status') }) it('does not pass email to database update', async () => { const event = createMockEvent({ method: 'PATCH', path: '/api/members/profile', body: { bio: 'new bio', email: 'hacked@evil.com' } }) await profilePatchHandler(event) const setData = Member.findByIdAndUpdate.mock.calls[0][1].$set expect(setData).not.toHaveProperty('email') }) it('does not pass _id to database update', async () => { const event = createMockEvent({ method: 'PATCH', path: '/api/members/profile', body: { bio: 'new bio', _id: 'different-id' } }) await profilePatchHandler(event) const setData = Member.findByIdAndUpdate.mock.calls[0][1].$set expect(setData).not.toHaveProperty('_id') }) }) describe('field allowlist - allowed fields pass through', () => { it('passes allowed profile fields through', async () => { const event = createMockEvent({ method: 'PATCH', path: '/api/members/profile', body: { pronouns: 'they/them', bio: 'Updated bio', studio: 'Test Studio', location: 'NYC', timeZone: 'America/New_York', avatar: 'https://example.com/avatar.jpg', showInDirectory: true, socialLinks: { twitter: '@test' } } }) await profilePatchHandler(event) const setData = Member.findByIdAndUpdate.mock.calls[0][1].$set expect(setData).toHaveProperty('pronouns', 'they/them') expect(setData).toHaveProperty('bio', 'Updated bio') expect(setData).toHaveProperty('studio', 'Test Studio') expect(setData).toHaveProperty('location', 'NYC') expect(setData).toHaveProperty('timeZone', 'America/New_York') expect(setData).toHaveProperty('avatar', 'https://example.com/avatar.jpg') expect(setData).toHaveProperty('showInDirectory', true) expect(setData).toHaveProperty('socialLinks') }) it('passes offering and lookingFor nested objects through', async () => { const event = createMockEvent({ method: 'PATCH', path: '/api/members/profile', body: { offering: { text: 'mentoring', tags: ['code', 'design'] }, lookingFor: { text: 'feedback', tags: ['art'] } } }) await profilePatchHandler(event) const setData = Member.findByIdAndUpdate.mock.calls[0][1].$set expect(setData.offering).toEqual({ text: 'mentoring', tags: ['code', 'design'] }) expect(setData.lookingFor).toEqual({ text: 'feedback', tags: ['art'] }) }) }) })