chore(tests): replace source-grep tests with handler tests
This commit is contained in:
parent
00073ec52c
commit
bafe24b778
3 changed files with 299 additions and 161 deletions
|
|
@ -1,49 +1,139 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
import { readFileSync, existsSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
|
||||
const eventsDir = resolve(import.meta.dirname, '../../../server/api/events/[id]')
|
||||
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'
|
||||
|
||||
describe('waitlist.post.js bypasses validators on event.save()', () => {
|
||||
const source = readFileSync(resolve(eventsDir, 'waitlist.post.js'), 'utf-8')
|
||||
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() }))
|
||||
|
||||
it('calls eventData.save with validateBeforeSave: false', () => {
|
||||
expect(source).toContain('eventData.save({ validateBeforeSave: false })')
|
||||
})
|
||||
vi.stubGlobal('waitlistSchema', waitlistSchema)
|
||||
vi.stubGlobal('waitlistDeleteSchema', waitlistDeleteSchema)
|
||||
|
||||
it('does not contain a bare eventData.save() call', () => {
|
||||
expect(source).not.toMatch(/eventData\.save\(\s*\)/)
|
||||
})
|
||||
})
|
||||
// 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)
|
||||
}))
|
||||
|
||||
describe('waitlist.delete.js bypasses validators on event.save()', () => {
|
||||
const source = readFileSync(resolve(eventsDir, 'waitlist.delete.js'), 'utf-8')
|
||||
|
||||
it('calls eventData.save with validateBeforeSave: false', () => {
|
||||
expect(source).toContain('eventData.save({ validateBeforeSave: false })')
|
||||
})
|
||||
|
||||
it('does not contain a bare eventData.save() call', () => {
|
||||
expect(source).not.toMatch(/eventData\.save\(\s*\)/)
|
||||
})
|
||||
})
|
||||
|
||||
// payment.post.js cases are handled by Fix #3 (file deletion).
|
||||
// If the file still exists, it should also pass the validators bypass.
|
||||
describe.skipIf(!existsSync(resolve(eventsDir, 'payment.post.js')))(
|
||||
'payment.post.js bypasses validators on event.save()',
|
||||
() => {
|
||||
const source = existsSync(resolve(eventsDir, 'payment.post.js'))
|
||||
? readFileSync(resolve(eventsDir, 'payment.post.js'), 'utf-8')
|
||||
: ''
|
||||
|
||||
it('has exactly two eventData.save({ validateBeforeSave: false }) calls', () => {
|
||||
const matches = source.match(/eventData\.save\(\{\s*validateBeforeSave:\s*false\s*\}\)/g) || []
|
||||
expect(matches.length).toBe(2)
|
||||
})
|
||||
|
||||
it('does not contain a bare eventData.save() call', () => {
|
||||
expect(source).not.toMatch(/eventData\.save\(\s*\)/)
|
||||
})
|
||||
/**
|
||||
* 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)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue