ghostguild-org/tests/server/api/event-save-validators.test.js

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)
})
})