ghostguild-org/tests/server/api/event-registration.test.js
Jennie Robinson Faber 15329e3e84 refactor(events): gate member benefits on hasMemberAccess
Extracts hasMemberAccess(member) in tickets.js and uses it across event
registration, ticket purchase, and series purchase flows so guest, suspended,
and cancelled records no longer count as members while pending_payment still
does.
2026-04-18 17:06:17 +01:00

113 lines
4.1 KiB
JavaScript

import { describe, it, expect } from 'vitest'
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
const eventsDir = resolve(import.meta.dirname, '../../../server/api/events/[id]')
describe('register.post.js', () => {
const source = readFileSync(resolve(eventsDir, 'register.post.js'), 'utf-8')
it('uses validateBody for input validation', () => {
expect(source).toContain('validateBody(event')
})
it('checks for duplicate registration with case-insensitive email', () => {
expect(source).toContain('email.toLowerCase()')
})
it('checks membersOnly restriction', () => {
expect(source).toContain('membersOnly')
})
it('checks capacity via maxAttendees', () => {
expect(source).toContain('maxAttendees')
})
it('does not let email failure block registration', () => {
// The await call (not the import) must be wrapped in try/catch
const emailCallIndex = source.indexOf('await sendEventRegistrationEmail')
expect(emailCallIndex).toBeGreaterThan(-1)
// A try block immediately precedes the email call
const precedingSource = source.slice(0, emailCallIndex)
const lastTryIndex = precedingSource.lastIndexOf('try')
expect(lastTryIndex).toBeGreaterThan(-1)
// The catch after the email call should log but not re-throw
const afterEmail = source.slice(emailCallIndex)
const catchBlock = afterEmail.match(/catch\s*\(\w+\)\s*\{[^}]*\}/s)
expect(catchBlock).not.toBeNull()
expect(catchBlock[0]).toContain('console.error')
})
})
describe('cancel-registration.post.js', () => {
const source = readFileSync(resolve(eventsDir, 'cancel-registration.post.js'), 'utf-8')
it('uses validateBody for input validation', () => {
expect(source).toContain('validateBody(event')
})
it('finds registration by email', () => {
expect(source).toContain('email.toLowerCase()')
})
it('notifies waitlist after cancellation', () => {
expect(source).toContain('waitlist')
expect(source).toContain('sendWaitlistNotificationEmail')
})
it('does not require auth', () => {
expect(source).not.toContain('requireAuth')
})
})
describe('tickets/purchase.post.js', () => {
const source = readFileSync(resolve(eventsDir, 'tickets/purchase.post.js'), 'utf-8')
it('uses validateBody for input validation', () => {
expect(source).toContain('validateBody(event, ticketPurchaseSchema)')
})
it('upserts a guest Member when consent given and no existing Member', () => {
expect(source).toContain('body.createAccount')
expect(source).toContain('findOneAndUpdate')
expect(source).toContain('$setOnInsert')
expect(source).toContain('status: "guest"')
expect(source).toContain('upsert: true')
})
it('treats inactive Members (guest, suspended, cancelled) as non-members for pricing and validation', () => {
// Only members with access (active or pending_payment) get member pricing.
// hasMemberAccess is the shared gate in server/utils/tickets.js.
expect(source).toContain('hasMemberAccess(member)')
})
it('sets an auth cookie for new guests and returning guests', () => {
expect(source).toContain('setAuthCookie(event, member)')
expect(source).toContain('accountCreated || member.status === "guest"')
})
it('does not auto-login existing non-guest members (hijack prevention)', () => {
// Requires sign-in flag for existing real members — plan §2
expect(source).toContain('requiresSignIn = true')
})
it('passes requiresSignIn to confirmation email', () => {
expect(source).toContain('sendEventRegistrationEmail(registration, eventData, { requiresSignIn })')
})
it('includes accountCreated and signedIn in response', () => {
expect(source).toContain('accountCreated,')
expect(source).toContain('signedIn,')
})
it('does not block registration when email fails', () => {
const emailCallIndex = source.indexOf('await sendEventRegistrationEmail')
expect(emailCallIndex).toBeGreaterThan(-1)
const afterEmail = source.slice(emailCallIndex)
const catchBlock = afterEmail.match(/catch\s*\(\w+\)\s*\{[^}]*\}/s)
expect(catchBlock).not.toBeNull()
expect(catchBlock[0]).toContain('console.error')
})
})