139 lines
4.2 KiB
JavaScript
139 lines
4.2 KiB
JavaScript
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)
|
|
})
|
|
})
|