import { describe, it, expect, vi, beforeEach } from 'vitest' import Member from '../../../server/models/member.js' import Series from '../../../server/models/series.js' import Event from '../../../server/models/event.js' import { validateSeriesTicketPurchase, completeSeriesTicketPurchase, registerForAllSeriesEvents, hasMemberAccess, } from '../../../server/utils/tickets.js' import { sendSeriesPassConfirmation } from '../../../server/utils/resend.js' import { seriesTicketPurchaseSchema } from '../../../server/utils/schemas.js' import handler from '../../../server/api/series/[id]/tickets/purchase.post.js' import { createMockEvent } from '../helpers/createMockEvent.js' vi.mock('../../../server/models/member.js', () => ({ default: { findOne: vi.fn(), findOneAndUpdate: vi.fn() } })) vi.mock('../../../server/models/series.js', () => ({ default: { findOne: vi.fn() } })) vi.mock('../../../server/models/event.js', () => ({ default: { find: vi.fn(() => ({ sort: vi.fn().mockResolvedValue([]) })) } })) vi.mock('../../../server/utils/mongoose.js', () => ({ connectDB: vi.fn() })) vi.mock('../../../server/utils/tickets.js', () => ({ validateSeriesTicketPurchase: vi.fn(), calculateSeriesTicketPrice: vi.fn(), reserveSeriesTicket: vi.fn(), releaseSeriesTicket: vi.fn(), completeSeriesTicketPurchase: vi.fn().mockResolvedValue(undefined), registerForAllSeriesEvents: vi.fn().mockResolvedValue([]), hasMemberAccess: vi.fn(() => false), })) vi.mock('../../../server/utils/resend.js', () => ({ sendSeriesPassConfirmation: vi.fn().mockResolvedValue(undefined), })) // Auto-imports the handler relies on but the global setup doesn't stub. const setAuthCookieMock = vi.fn() vi.stubGlobal('setAuthCookie', setAuthCookieMock) vi.stubGlobal('seriesTicketPurchaseSchema', seriesTicketPurchaseSchema) // Capture schema passed to validateBody so we can prove the route validates // against seriesTicketPurchaseSchema specifically. const validateBodyCalls = [] const validateBodyMock = vi.fn(async (event, schema) => { validateBodyCalls.push(schema) // Mirror real behavior: parse body via the schema so invalid bodies still throw. const body = await readBody(event) return schema.parse(body) }) vi.stubGlobal('validateBody', validateBodyMock) function buildEvent(body) { const ev = createMockEvent({ method: 'POST', path: '/api/series/series-1/tickets/purchase', body, }) ev.context = { params: { id: 'series-1' } } return ev } const baseSeries = () => ({ _id: 'series-1', id: 'series-1', slug: 'series-slug', title: 'Test Series', description: 'desc', type: 'workshop', registrations: [], }) describe('POST /api/series/[id]/tickets/purchase — guest upsert + auth cookie', () => { beforeEach(() => { vi.clearAllMocks() validateBodyCalls.length = 0 // Default: unauthenticated buyer globalThis.requireAuth = vi.fn().mockRejectedValue( Object.assign(new Error('Unauthorized'), { statusCode: 401 }) ) Series.findOne.mockResolvedValue(baseSeries()) Member.findOne.mockResolvedValue(null) validateSeriesTicketPurchase.mockReturnValue({ valid: true, ticketInfo: { ticketType: 'public', price: 0, currency: 'CAD', isFree: true, }, }) }) it('upserts a guest Member with $setOnInsert + upsert:true when buyer has no account', async () => { Member.findOneAndUpdate.mockResolvedValue({ _id: 'new-member-1', email: 'guest@example.com', status: 'guest', }) await handler(buildEvent({ name: 'Guest Buyer', email: 'guest@example.com', ticketType: 'public', })) expect(Member.findOneAndUpdate).toHaveBeenCalledTimes(1) const [filter, update, options] = Member.findOneAndUpdate.mock.calls[0] expect(filter).toEqual({ email: 'guest@example.com' }) expect(update).toEqual({ $setOnInsert: { email: 'guest@example.com', name: 'Guest Buyer', circle: 'community', contributionAmount: 0, status: 'guest', }, }) expect(options).toEqual({ upsert: true, new: true, setDefaultsOnInsert: true, }) }) it('sets the auth cookie for newly-created guest accounts', async () => { const newMember = { _id: 'new-member-2', email: 'newbie@example.com', status: 'guest', } Member.findOneAndUpdate.mockResolvedValue(newMember) const result = await handler(buildEvent({ name: 'Newbie', email: 'newbie@example.com', ticketType: 'public', })) expect(setAuthCookieMock).toHaveBeenCalledTimes(1) expect(setAuthCookieMock.mock.calls[0][1]).toBe(newMember) expect(result.signedIn).toBe(true) expect(result.accountCreated).toBe(true) expect(result.requiresSignIn).toBe(false) }) it('validates input via seriesTicketPurchaseSchema', async () => { Member.findOneAndUpdate.mockResolvedValue({ _id: 'm', email: 'a@b.com', status: 'guest', }) await handler(buildEvent({ name: 'A', email: 'a@b.com', ticketType: 'public', })) expect(validateBodyMock).toHaveBeenCalled() expect(validateBodyCalls[0]).toBe(seriesTicketPurchaseSchema) }) it('rejects invalid body (no name) via schema validation', async () => { await expect( handler(buildEvent({ email: 'a@b.com', ticketType: 'public' })) ).rejects.toBeDefined() expect(Member.findOneAndUpdate).not.toHaveBeenCalled() expect(setAuthCookieMock).not.toHaveBeenCalled() }) })