import { describe, it, expect, vi, beforeEach } from 'vitest' import registerHandler from '../../../../server/api/events/[id]/register.post.js' import reserveHandler from '../../../../server/api/events/[id]/tickets/reserve.post.js' import availableHandler from '../../../../server/api/events/[id]/tickets/available.get.js' import { createMockEvent } from '../../helpers/createMockEvent.js' const { mockLoadPublicEvent, mockMemberFindOne, mockFindByIdAndUpdate, mockReserveTicket } = vi.hoisted(() => ({ mockLoadPublicEvent: vi.fn(), mockMemberFindOne: vi.fn(), mockFindByIdAndUpdate: vi.fn(), mockReserveTicket: vi.fn() })) vi.mock('../../../../server/models/event.js', () => ({ default: { findByIdAndUpdate: mockFindByIdAndUpdate } })) vi.mock('../../../../server/models/member.js', () => ({ default: { findOne: mockMemberFindOne } })) vi.mock('../../../../server/utils/mongoose.js', () => ({ connectDB: vi.fn() })) vi.mock('../../../../server/utils/loadEvent.js', () => ({ loadPublicEvent: mockLoadPublicEvent })) vi.mock('../../../../server/utils/resend.js', () => ({ sendEventRegistrationEmail: vi.fn().mockResolvedValue(null) })) vi.mock('../../../../server/utils/tickets.js', async () => { const actual = await vi.importActual('../../../../server/utils/tickets.js') return { ...actual, reserveTicket: mockReserveTicket } }) const future = (ms) => new Date(Date.now() + ms) const past = (ms) => new Date(Date.now() - ms) function baseFreeEvent(overrides = {}) { return { _id: 'event-1', slug: 'deadline-event', title: 'Deadline Test', startDate: future(3 * 86400000), isCancelled: false, registrations: [], tickets: { enabled: false }, isVisible: true, ...overrides } } function baseTicketedEvent(overrides = {}) { return { _id: 'event-ticketed', slug: 'ticketed-event', title: 'Ticketed', startDate: future(3 * 86400000), isCancelled: false, registrations: [], tickets: { enabled: true, currency: 'CAD', capacity: { total: 50 }, member: { available: true, price: 0, isFree: true, name: 'Member' }, public: { available: true, price: 15, quantity: 50, sold: 0, reserved: 0, name: 'GA' }, waitlist: { enabled: false } }, isVisible: true, ...overrides } } function mkReq({ method = 'POST', path = '/', body, query = '', id = 'event-slug' } = {}) { const req = createMockEvent({ method, path: path + (query ? `?${query}` : ''), body }) req.context = { params: { id } } return req } describe('Registration deadline — register.post.js', () => { beforeEach(() => { vi.clearAllMocks() mockMemberFindOne.mockResolvedValue(null) mockFindByIdAndUpdate.mockImplementation((id, update) => Promise.resolve({ registrations: [{ ...update.$push.registrations, _id: 'r1' }] }) ) }) it('returns 400 with deadline reason when registrationDeadline has passed', async () => { mockLoadPublicEvent.mockResolvedValue( baseFreeEvent({ registrationDeadline: past(86400000) }) ) const req = mkReq({ path: '/api/events/deadline-event/register', body: { email: 'a@example.com', name: 'A' } }) await expect(registerHandler(req)).rejects.toMatchObject({ statusCode: 400, statusMessage: expect.stringMatching(/deadline/i) }) expect(mockFindByIdAndUpdate).not.toHaveBeenCalled() }) it('succeeds on happy path when deadline is in the future', async () => { mockLoadPublicEvent.mockResolvedValue( baseFreeEvent({ registrationDeadline: future(86400000) }) ) const req = mkReq({ path: '/api/events/deadline-event/register', body: { email: 'a@example.com', name: 'A' } }) const result = await registerHandler(req) expect(result.success).toBe(true) }) it('no longer routes tickets.enabled events through legacy pricing 402', async () => { mockLoadPublicEvent.mockResolvedValue( baseTicketedEvent({ pricing: { paymentRequired: true, isFree: false } }) ) const req = mkReq({ path: '/api/events/ticketed-event/register', body: { email: 'a@example.com', name: 'A' } }) const result = await registerHandler(req) expect(result.success).toBe(true) }) }) describe('Registration deadline — reserve.post.js', () => { beforeEach(() => { vi.clearAllMocks() mockMemberFindOne.mockResolvedValue(null) mockReserveTicket.mockResolvedValue({ success: true, reservationId: 'res-1', expiresAt: new Date(Date.now() + 600000) }) }) it('returns 400 with deadline reason when registrationDeadline has passed', async () => { mockLoadPublicEvent.mockResolvedValue( baseTicketedEvent({ registrationDeadline: past(86400000) }) ) const req = mkReq({ path: '/api/events/ticketed-event/tickets/reserve', body: { email: 'a@example.com' } }) await expect(reserveHandler(req)).rejects.toMatchObject({ statusCode: 400, statusMessage: expect.stringMatching(/deadline/i) }) expect(mockReserveTicket).not.toHaveBeenCalled() }) it('succeeds when deadline is in the future', async () => { mockLoadPublicEvent.mockResolvedValue( baseTicketedEvent({ registrationDeadline: future(86400000) }) ) const req = mkReq({ path: '/api/events/ticketed-event/tickets/reserve', body: { email: 'a@example.com' } }) const result = await reserveHandler(req) expect(result.success).toBe(true) }) }) describe('Registration deadline — available.get.js', () => { beforeEach(() => { vi.clearAllMocks() mockMemberFindOne.mockResolvedValue(null) }) it('returns available:false with deadline reason when deadline has passed', async () => { mockLoadPublicEvent.mockResolvedValue( baseTicketedEvent({ registrationDeadline: past(86400000) }) ) const req = mkReq({ method: 'GET', path: '/api/events/ticketed-event/tickets/available' }) const result = await availableHandler(req) expect(result.available).toBe(false) expect(result.reason).toMatch(/deadline/i) }) it('returns availability normally when deadline is in the future', async () => { mockLoadPublicEvent.mockResolvedValue( baseTicketedEvent({ registrationDeadline: future(86400000) }) ) const req = mkReq({ method: 'GET', path: '/api/events/ticketed-event/tickets/available' }) const result = await availableHandler(req) expect(result.available).toBe(true) }) })