ghostguild-org/tests/server/api/series-tickets-purchase.test.js

177 lines
5.4 KiB
JavaScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
import Member from '../../../server/models/member.js'
import Series from '../../../server/models/series.js'
import Event from '../../../server/models/event.js'
import {
validateSeriesTicketPurchase,
completeSeriesTicketPurchase,
registerForAllSeriesEvents,
hasMemberAccess,
} from '../../../server/utils/tickets.js'
import { sendSeriesPassConfirmation } from '../../../server/utils/resend.js'
import { seriesTicketPurchaseSchema } from '../../../server/utils/schemas.js'
import handler from '../../../server/api/series/[id]/tickets/purchase.post.js'
import { createMockEvent } from '../helpers/createMockEvent.js'
vi.mock('../../../server/models/member.js', () => ({
default: { findOne: vi.fn(), findOneAndUpdate: vi.fn() }
}))
vi.mock('../../../server/models/series.js', () => ({
default: { findOne: vi.fn() }
}))
vi.mock('../../../server/models/event.js', () => ({
default: {
find: vi.fn(() => ({ sort: vi.fn().mockResolvedValue([]) }))
}
}))
vi.mock('../../../server/utils/mongoose.js', () => ({ connectDB: vi.fn() }))
vi.mock('../../../server/utils/tickets.js', () => ({
validateSeriesTicketPurchase: vi.fn(),
calculateSeriesTicketPrice: vi.fn(),
reserveSeriesTicket: vi.fn(),
releaseSeriesTicket: vi.fn(),
completeSeriesTicketPurchase: vi.fn().mockResolvedValue(undefined),
registerForAllSeriesEvents: vi.fn().mockResolvedValue([]),
hasMemberAccess: vi.fn(() => false),
}))
vi.mock('../../../server/utils/resend.js', () => ({
sendSeriesPassConfirmation: vi.fn().mockResolvedValue(undefined),
}))
// Auto-imports the handler relies on but the global setup doesn't stub.
const setAuthCookieMock = vi.fn()
vi.stubGlobal('setAuthCookie', setAuthCookieMock)
vi.stubGlobal('seriesTicketPurchaseSchema', seriesTicketPurchaseSchema)
// Capture schema passed to validateBody so we can prove the route validates
// against seriesTicketPurchaseSchema specifically.
const validateBodyCalls = []
const validateBodyMock = vi.fn(async (event, schema) => {
validateBodyCalls.push(schema)
// Mirror real behavior: parse body via the schema so invalid bodies still throw.
const body = await readBody(event)
return schema.parse(body)
})
vi.stubGlobal('validateBody', validateBodyMock)
function buildEvent(body) {
const ev = createMockEvent({
method: 'POST',
path: '/api/series/series-1/tickets/purchase',
body,
})
ev.context = { params: { id: 'series-1' } }
return ev
}
const baseSeries = () => ({
_id: 'series-1',
id: 'series-1',
slug: 'series-slug',
title: 'Test Series',
description: 'desc',
type: 'workshop',
registrations: [],
})
describe('POST /api/series/[id]/tickets/purchase — guest upsert + auth cookie', () => {
beforeEach(() => {
vi.clearAllMocks()
validateBodyCalls.length = 0
// Default: unauthenticated buyer
globalThis.requireAuth = vi.fn().mockRejectedValue(
Object.assign(new Error('Unauthorized'), { statusCode: 401 })
)
Series.findOne.mockResolvedValue(baseSeries())
Member.findOne.mockResolvedValue(null)
validateSeriesTicketPurchase.mockReturnValue({
valid: true,
ticketInfo: {
ticketType: 'public',
price: 0,
currency: 'CAD',
isFree: true,
},
})
})
it('upserts a guest Member with $setOnInsert + upsert:true when buyer has no account', async () => {
Member.findOneAndUpdate.mockResolvedValue({
_id: 'new-member-1',
email: 'guest@example.com',
status: 'guest',
})
await handler(buildEvent({
name: 'Guest Buyer',
email: 'guest@example.com',
ticketType: 'public',
}))
expect(Member.findOneAndUpdate).toHaveBeenCalledTimes(1)
const [filter, update, options] = Member.findOneAndUpdate.mock.calls[0]
expect(filter).toEqual({ email: 'guest@example.com' })
expect(update).toEqual({
$setOnInsert: {
email: 'guest@example.com',
name: 'Guest Buyer',
circle: 'community',
contributionAmount: 0,
status: 'guest',
},
})
expect(options).toEqual({
upsert: true,
new: true,
setDefaultsOnInsert: true,
})
})
it('sets the auth cookie for newly-created guest accounts', async () => {
const newMember = {
_id: 'new-member-2',
email: 'newbie@example.com',
status: 'guest',
}
Member.findOneAndUpdate.mockResolvedValue(newMember)
const result = await handler(buildEvent({
name: 'Newbie',
email: 'newbie@example.com',
ticketType: 'public',
}))
expect(setAuthCookieMock).toHaveBeenCalledTimes(1)
expect(setAuthCookieMock.mock.calls[0][1]).toBe(newMember)
expect(result.signedIn).toBe(true)
expect(result.accountCreated).toBe(true)
expect(result.requiresSignIn).toBe(false)
})
it('validates input via seriesTicketPurchaseSchema', async () => {
Member.findOneAndUpdate.mockResolvedValue({
_id: 'm',
email: 'a@b.com',
status: 'guest',
})
await handler(buildEvent({
name: 'A',
email: 'a@b.com',
ticketType: 'public',
}))
expect(validateBodyMock).toHaveBeenCalled()
expect(validateBodyCalls[0]).toBe(seriesTicketPurchaseSchema)
})
it('rejects invalid body (no name) via schema validation', async () => {
await expect(
handler(buildEvent({ email: 'a@b.com', ticketType: 'public' }))
).rejects.toBeDefined()
expect(Member.findOneAndUpdate).not.toHaveBeenCalled()
expect(setAuthCookieMock).not.toHaveBeenCalled()
})
})