Pre-launch P0 fixes surfaced by docs/specs/events-functional-test-matrix.md (Findings 1, 2, 3). 1. Series-pass bypass (Finding 1 / matrix S1 P3): register.post.js now loads the linked Series when tickets.requiresSeriesTicket is set and rejects drop-in registration unless series.allowIndividualEventTickets is true or the user has a valid pass. Data-integrity 500 if the referenced series is missing. 2. Hidden-event leak (Finding 2 / matrix E11): extract loadPublicEvent into server/utils/loadEvent.js. All five public event endpoints ([id].get, register, tickets/available, tickets/reserve, tickets/purchase) now go through the helper, which 404s when isVisible === false and the requester is not an admin. Admin detection uses a new non-throwing getOptionalMember() in server/utils/auth.js (extracted from the pattern already inlined in api/auth/status.get.js). 3. Deadline enforcement + legacy pricing retirement (Finding 3 / matrix E8): register.post.js and tickets/reserve.post.js delegate gating to validateTicketPurchase (which already covers deadline, cancelled, started, members-only, sold-out, and already-registered); tickets/available.get.js gets an explicit registrationDeadline check. Legacy pricing.paymentRequired 402 branch removed from register.post.js.
126 lines
4.7 KiB
JavaScript
126 lines
4.7 KiB
JavaScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
|
|
import { loadPublicEvent } from '../../../server/utils/loadEvent.js'
|
|
import { createMockEvent } from '../helpers/createMockEvent.js'
|
|
|
|
const { mockFindById, mockFindOne, mockObjectIdIsValid, mockGetOptionalMember } = vi.hoisted(() => ({
|
|
mockFindById: vi.fn(),
|
|
mockFindOne: vi.fn(),
|
|
mockObjectIdIsValid: vi.fn(),
|
|
mockGetOptionalMember: vi.fn()
|
|
}))
|
|
|
|
vi.mock('../../../server/models/event.js', () => ({
|
|
default: { findById: mockFindById, findOne: mockFindOne }
|
|
}))
|
|
|
|
vi.mock('../../../server/utils/mongoose.js', () => ({
|
|
connectDB: vi.fn()
|
|
}))
|
|
|
|
vi.mock('../../../server/utils/auth.js', () => ({
|
|
getOptionalMember: mockGetOptionalMember
|
|
}))
|
|
|
|
vi.mock('mongoose', () => ({
|
|
default: {
|
|
Types: {
|
|
ObjectId: { isValid: mockObjectIdIsValid }
|
|
}
|
|
}
|
|
}))
|
|
|
|
function chainableResult(value) {
|
|
const query = {
|
|
select: vi.fn().mockReturnThis(),
|
|
lean: vi.fn().mockReturnThis(),
|
|
then: (resolve) => Promise.resolve(value).then(resolve)
|
|
}
|
|
return query
|
|
}
|
|
|
|
describe('loadPublicEvent', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
mockObjectIdIsValid.mockReturnValue(false)
|
|
mockGetOptionalMember.mockResolvedValue(null)
|
|
})
|
|
|
|
it('throws 400 when identifier is missing', async () => {
|
|
const reqEvent = createMockEvent({ method: 'GET', path: '/api/events/' })
|
|
await expect(loadPublicEvent(reqEvent, '')).rejects.toMatchObject({ statusCode: 400 })
|
|
})
|
|
|
|
it('throws 404 when event is not found by slug', async () => {
|
|
mockFindOne.mockReturnValue(chainableResult(null))
|
|
const reqEvent = createMockEvent({ method: 'GET', path: '/api/events/missing' })
|
|
|
|
await expect(loadPublicEvent(reqEvent, 'missing')).rejects.toMatchObject({ statusCode: 404 })
|
|
expect(mockFindOne).toHaveBeenCalledWith({ slug: 'missing' })
|
|
})
|
|
|
|
it('returns visible event for any caller (no admin check)', async () => {
|
|
const eventDoc = { _id: 'e1', slug: 'visible-slug', isVisible: true }
|
|
mockFindOne.mockReturnValue(chainableResult(eventDoc))
|
|
const reqEvent = createMockEvent({ method: 'GET', path: '/api/events/visible-slug' })
|
|
|
|
const result = await loadPublicEvent(reqEvent, 'visible-slug')
|
|
|
|
expect(result).toBe(eventDoc)
|
|
expect(mockGetOptionalMember).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('returns hidden event for admin caller', async () => {
|
|
const eventDoc = { _id: 'e1', slug: 'hidden-slug', isVisible: false }
|
|
mockFindOne.mockReturnValue(chainableResult(eventDoc))
|
|
mockGetOptionalMember.mockResolvedValue({ _id: 'm1', role: 'admin' })
|
|
const reqEvent = createMockEvent({ method: 'GET', path: '/api/events/hidden-slug' })
|
|
|
|
const result = await loadPublicEvent(reqEvent, 'hidden-slug')
|
|
|
|
expect(result).toBe(eventDoc)
|
|
})
|
|
|
|
it('throws 404 on hidden event for non-admin member', async () => {
|
|
const eventDoc = { _id: 'e1', slug: 'hidden-slug', isVisible: false }
|
|
mockFindOne.mockReturnValue(chainableResult(eventDoc))
|
|
mockGetOptionalMember.mockResolvedValue({ _id: 'm1', role: 'member' })
|
|
const reqEvent = createMockEvent({ method: 'GET', path: '/api/events/hidden-slug' })
|
|
|
|
await expect(loadPublicEvent(reqEvent, 'hidden-slug')).rejects.toMatchObject({ statusCode: 404 })
|
|
})
|
|
|
|
it('throws 404 on hidden event for unauthenticated caller', async () => {
|
|
const eventDoc = { _id: 'e1', slug: 'hidden-slug', isVisible: false }
|
|
mockFindOne.mockReturnValue(chainableResult(eventDoc))
|
|
mockGetOptionalMember.mockResolvedValue(null)
|
|
const reqEvent = createMockEvent({ method: 'GET', path: '/api/events/hidden-slug' })
|
|
|
|
await expect(loadPublicEvent(reqEvent, 'hidden-slug')).rejects.toMatchObject({ statusCode: 404 })
|
|
})
|
|
|
|
it('applies select and lean options when provided', async () => {
|
|
const eventDoc = { _id: 'e1', slug: 'x', isVisible: true }
|
|
const query = chainableResult(eventDoc)
|
|
mockFindOne.mockReturnValue(query)
|
|
const reqEvent = createMockEvent({ method: 'GET', path: '/api/events/x' })
|
|
|
|
await loadPublicEvent(reqEvent, 'x', { select: '-registrations.email', lean: true })
|
|
|
|
expect(query.select).toHaveBeenCalledWith('-registrations.email')
|
|
expect(query.lean).toHaveBeenCalled()
|
|
})
|
|
|
|
it('looks up by ObjectId when identifier is a valid ObjectId', async () => {
|
|
const eventDoc = { _id: 'e1', slug: 'x', isVisible: true }
|
|
mockObjectIdIsValid.mockReturnValue(true)
|
|
mockFindById.mockReturnValue(chainableResult(eventDoc))
|
|
const reqEvent = createMockEvent({ method: 'GET', path: '/api/events/507f1f77bcf86cd799439011' })
|
|
|
|
const result = await loadPublicEvent(reqEvent, '507f1f77bcf86cd799439011')
|
|
|
|
expect(mockFindById).toHaveBeenCalledWith('507f1f77bcf86cd799439011')
|
|
expect(mockFindOne).not.toHaveBeenCalled()
|
|
expect(result).toBe(eventDoc)
|
|
})
|
|
})
|