import { describe, it, expect, vi, beforeEach } from 'vitest' import Event from '../../../server/models/event.js' import Member from '../../../server/models/member.js' import { waitlistSchema, waitlistDeleteSchema } from '../../../server/utils/schemas.js' import waitlistPostHandler from '../../../server/api/events/[id]/waitlist.post.js' import waitlistDeleteHandler from '../../../server/api/events/[id]/waitlist.delete.js' import { createMockEvent } from '../helpers/createMockEvent.js' vi.mock('../../../server/models/event.js', () => ({ default: { findOne: vi.fn() } })) vi.mock('../../../server/models/member.js', () => ({ default: { findOne: vi.fn() } })) vi.mock('../../../server/utils/mongoose.js', () => ({ connectDB: vi.fn() })) vi.stubGlobal('waitlistSchema', waitlistSchema) vi.stubGlobal('waitlistDeleteSchema', waitlistDeleteSchema) // Override the global validateBody stub so the route actually parses against // the schema it passed in. vi.stubGlobal('validateBody', vi.fn(async (event, schema) => { const body = await readBody(event) return schema.parse(body) })) /** * Build a mock Event document whose `save()` simulates the legacy validator * problem we're protecting against: when called WITHOUT `validateBeforeSave: * false` it throws (mimicking a stale `location` validator failing on * unrelated writes). When called WITH `validateBeforeSave: false` it resolves * normally. The route is correct iff it bypasses validators. */ function makeMockEvent(overrides = {}) { const doc = { _id: 'event-1', slug: 'event-slug', tickets: { waitlist: { enabled: true, maxSize: 10, entries: [], }, }, registrations: [], ...overrides, } doc.save = vi.fn(async (options) => { if (!options || options.validateBeforeSave !== false) { const err = new Error('Validation failed: location: legacy field invalid') err.name = 'ValidationError' throw err } return doc }) return doc } function buildPostEvent(body) { const ev = createMockEvent({ method: 'POST', path: '/api/events/event-slug/waitlist', body, }) ev.context = { params: { id: 'event-slug' } } return ev } function buildDeleteEvent(body) { const ev = createMockEvent({ method: 'DELETE', path: '/api/events/event-slug/waitlist', body, }) ev.context = { params: { id: 'event-slug' } } return ev } describe('POST /api/events/[id]/waitlist — bypasses save validators', () => { beforeEach(() => { vi.clearAllMocks() Member.findOne.mockResolvedValue(null) }) it('save() succeeds because the route passes { validateBeforeSave: false }', async () => { const mockEvent = makeMockEvent() Event.findOne.mockResolvedValue(mockEvent) const result = await waitlistPostHandler(buildPostEvent({ name: 'Waiter', email: 'wait@example.com', })) expect(result.success).toBe(true) expect(mockEvent.save).toHaveBeenCalledTimes(1) expect(mockEvent.save).toHaveBeenCalledWith({ validateBeforeSave: false }) // Entry was actually appended. expect(mockEvent.tickets.waitlist.entries).toHaveLength(1) expect(mockEvent.tickets.waitlist.entries[0].email).toBe('wait@example.com') }) }) describe('DELETE /api/events/[id]/waitlist — bypasses save validators', () => { beforeEach(() => { vi.clearAllMocks() }) it('save() succeeds because the route passes { validateBeforeSave: false }', async () => { const mockEvent = makeMockEvent({ tickets: { waitlist: { enabled: true, maxSize: 10, entries: [ { name: 'Waiter', email: 'wait@example.com', membershipLevel: 'non-member', addedAt: new Date(), notified: false, }, ], }, }, }) Event.findOne.mockResolvedValue(mockEvent) const result = await waitlistDeleteHandler(buildDeleteEvent({ email: 'wait@example.com', })) expect(result.success).toBe(true) expect(mockEvent.save).toHaveBeenCalledTimes(1) expect(mockEvent.save).toHaveBeenCalledWith({ validateBeforeSave: false }) // Entry was actually removed. expect(mockEvent.tickets.waitlist.entries).toHaveLength(0) }) })