177 lines
5.4 KiB
JavaScript
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()
|
|
})
|
|
})
|