588 lines
16 KiB
JavaScript
588 lines
16 KiB
JavaScript
import { describe, it, expect } from 'vitest'
|
|
import {
|
|
helcimCreatePlanSchema,
|
|
helcimCustomerSchema,
|
|
helcimInitializePaymentSchema,
|
|
helcimSubscriptionSchema,
|
|
helcimUpdateBillingSchema,
|
|
ticketPurchaseSchema,
|
|
ticketReserveSchema,
|
|
ticketEligibilitySchema,
|
|
waitlistSchema,
|
|
waitlistDeleteSchema,
|
|
cancelRegistrationSchema,
|
|
checkRegistrationSchema,
|
|
eventPaymentSchema,
|
|
updateContributionSchema,
|
|
seriesTicketPurchaseSchema,
|
|
seriesTicketEligibilitySchema,
|
|
adminSeriesCreateSchema,
|
|
adminSeriesUpdateSchema,
|
|
adminSeriesItemUpdateSchema,
|
|
adminSeriesTicketsSchema,
|
|
adminEventUpdateSchema,
|
|
adminMemberCreateSchema
|
|
} from '../../../server/utils/schemas.js'
|
|
|
|
import { readFileSync } from 'node:fs'
|
|
import { resolve } from 'node:path'
|
|
|
|
// --- Helcim schemas ---
|
|
|
|
describe('helcimCreatePlanSchema', () => {
|
|
it('accepts valid plan data', () => {
|
|
const result = helcimCreatePlanSchema.safeParse({
|
|
name: 'Monthly Plan',
|
|
amount: '15.00',
|
|
frequency: 'monthly'
|
|
})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('accepts numeric amount', () => {
|
|
const result = helcimCreatePlanSchema.safeParse({
|
|
name: 'Plan',
|
|
amount: 15,
|
|
frequency: 'monthly'
|
|
})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('rejects missing name', () => {
|
|
const result = helcimCreatePlanSchema.safeParse({
|
|
amount: 15,
|
|
frequency: 'monthly'
|
|
})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects missing amount', () => {
|
|
const result = helcimCreatePlanSchema.safeParse({
|
|
name: 'Plan',
|
|
frequency: 'monthly'
|
|
})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('strips unknown fields', () => {
|
|
const result = helcimCreatePlanSchema.safeParse({
|
|
name: 'Plan',
|
|
amount: 15,
|
|
frequency: 'monthly',
|
|
malicious: 'data'
|
|
})
|
|
expect(result.success).toBe(true)
|
|
expect(result.data).not.toHaveProperty('malicious')
|
|
})
|
|
})
|
|
|
|
describe('helcimCustomerSchema', () => {
|
|
it('accepts valid customer data', () => {
|
|
const result = helcimCustomerSchema.safeParse({
|
|
name: 'Jane Doe',
|
|
email: 'jane@example.com',
|
|
agreedToGuidelines: true
|
|
})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('lowercases email', () => {
|
|
const result = helcimCustomerSchema.safeParse({
|
|
name: 'Jane',
|
|
email: 'JANE@Example.COM',
|
|
agreedToGuidelines: true
|
|
})
|
|
expect(result.success).toBe(true)
|
|
expect(result.data.email).toBe('jane@example.com')
|
|
})
|
|
|
|
it('rejects invalid email', () => {
|
|
const result = helcimCustomerSchema.safeParse({
|
|
name: 'Jane',
|
|
email: 'not-an-email',
|
|
agreedToGuidelines: true
|
|
})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('strips role field', () => {
|
|
const result = helcimCustomerSchema.safeParse({
|
|
name: 'Jane',
|
|
email: 'jane@example.com',
|
|
agreedToGuidelines: true,
|
|
role: 'admin'
|
|
})
|
|
expect(result.success).toBe(true)
|
|
expect(result.data).not.toHaveProperty('role')
|
|
})
|
|
|
|
it('rejects missing agreedToGuidelines', () => {
|
|
const result = helcimCustomerSchema.safeParse({
|
|
name: 'Jane',
|
|
email: 'jane@example.com'
|
|
})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects agreedToGuidelines:false', () => {
|
|
const result = helcimCustomerSchema.safeParse({
|
|
name: 'Jane',
|
|
email: 'jane@example.com',
|
|
agreedToGuidelines: false
|
|
})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('helcimSubscriptionSchema', () => {
|
|
it('accepts valid subscription data', () => {
|
|
const result = helcimSubscriptionSchema.safeParse({
|
|
customerId: '12345',
|
|
contributionAmount: 15,
|
|
customerCode: 'CST123'
|
|
})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('rejects negative contributionAmount', () => {
|
|
const result = helcimSubscriptionSchema.safeParse({
|
|
customerId: '12345',
|
|
contributionAmount: -1,
|
|
customerCode: 'CST123'
|
|
})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects missing customerCode', () => {
|
|
const result = helcimSubscriptionSchema.safeParse({
|
|
customerId: '12345',
|
|
contributionAmount: 15
|
|
})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('accepts cadence: monthly', () => {
|
|
const result = helcimSubscriptionSchema.safeParse({
|
|
customerId: '12345',
|
|
contributionAmount: 15,
|
|
customerCode: 'CST123',
|
|
cadence: 'monthly'
|
|
})
|
|
expect(result.success).toBe(true)
|
|
expect(result.data.cadence).toBe('monthly')
|
|
})
|
|
|
|
it('accepts cadence: annual', () => {
|
|
const result = helcimSubscriptionSchema.safeParse({
|
|
customerId: '12345',
|
|
contributionAmount: 15,
|
|
customerCode: 'CST123',
|
|
cadence: 'annual'
|
|
})
|
|
expect(result.success).toBe(true)
|
|
expect(result.data.cadence).toBe('annual')
|
|
})
|
|
|
|
it('rejects cadence: weekly', () => {
|
|
const result = helcimSubscriptionSchema.safeParse({
|
|
customerId: '12345',
|
|
contributionAmount: 15,
|
|
customerCode: 'CST123',
|
|
cadence: 'weekly'
|
|
})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('defaults cadence to monthly when omitted', () => {
|
|
const result = helcimSubscriptionSchema.safeParse({
|
|
customerId: '12345',
|
|
contributionAmount: 15,
|
|
customerCode: 'CST123'
|
|
})
|
|
expect(result.success).toBe(true)
|
|
expect(result.data.cadence).toBe('monthly')
|
|
})
|
|
})
|
|
|
|
describe('helcimUpdateBillingSchema', () => {
|
|
const validBilling = {
|
|
customerId: '123',
|
|
billingAddress: {
|
|
street: '123 Main St',
|
|
city: 'Toronto',
|
|
country: 'CA',
|
|
postalCode: 'M5V 1A1'
|
|
}
|
|
}
|
|
|
|
it('accepts valid billing data', () => {
|
|
const result = helcimUpdateBillingSchema.safeParse(validBilling)
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('rejects missing street', () => {
|
|
const result = helcimUpdateBillingSchema.safeParse({
|
|
customerId: '123',
|
|
billingAddress: {
|
|
city: 'Toronto',
|
|
country: 'CA',
|
|
postalCode: 'M5V'
|
|
}
|
|
})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('rejects missing billingAddress', () => {
|
|
const result = helcimUpdateBillingSchema.safeParse({
|
|
customerId: '123'
|
|
})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
})
|
|
|
|
// --- Event schemas ---
|
|
|
|
describe('ticketPurchaseSchema', () => {
|
|
it('accepts valid ticket purchase', () => {
|
|
const result = ticketPurchaseSchema.safeParse({
|
|
name: 'Jane',
|
|
email: 'jane@example.com'
|
|
})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('lowercases email', () => {
|
|
const result = ticketPurchaseSchema.safeParse({
|
|
name: 'Jane',
|
|
email: 'JANE@Example.COM'
|
|
})
|
|
expect(result.success).toBe(true)
|
|
expect(result.data.email).toBe('jane@example.com')
|
|
})
|
|
|
|
it('rejects missing name', () => {
|
|
const result = ticketPurchaseSchema.safeParse({
|
|
email: 'jane@example.com'
|
|
})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('strips unknown fields', () => {
|
|
const result = ticketPurchaseSchema.safeParse({
|
|
name: 'Jane',
|
|
email: 'jane@example.com',
|
|
role: 'admin',
|
|
status: 'active'
|
|
})
|
|
expect(result.success).toBe(true)
|
|
expect(result.data).not.toHaveProperty('role')
|
|
expect(result.data).not.toHaveProperty('status')
|
|
})
|
|
})
|
|
|
|
describe('ticketReserveSchema', () => {
|
|
it('accepts valid email', () => {
|
|
const result = ticketReserveSchema.safeParse({ email: 'test@example.com' })
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('rejects missing email', () => {
|
|
const result = ticketReserveSchema.safeParse({})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('waitlistSchema', () => {
|
|
it('accepts valid waitlist entry', () => {
|
|
const result = waitlistSchema.safeParse({
|
|
email: 'test@example.com',
|
|
name: 'Test User'
|
|
})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('accepts email-only entry', () => {
|
|
const result = waitlistSchema.safeParse({ email: 'test@example.com' })
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('rejects missing email', () => {
|
|
const result = waitlistSchema.safeParse({ name: 'Test' })
|
|
expect(result.success).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('eventPaymentSchema', () => {
|
|
it('accepts valid payment data', () => {
|
|
const result = eventPaymentSchema.safeParse({
|
|
name: 'Payer',
|
|
email: 'payer@example.com',
|
|
paymentToken: 'tok_abc123'
|
|
})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('rejects missing paymentToken', () => {
|
|
const result = eventPaymentSchema.safeParse({
|
|
name: 'Payer',
|
|
email: 'payer@example.com'
|
|
})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
})
|
|
|
|
// --- Member schemas ---
|
|
|
|
describe('updateContributionSchema', () => {
|
|
it('accepts valid contributionAmount', () => {
|
|
const result = updateContributionSchema.safeParse({ contributionAmount: 15 })
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('accepts contributionAmount: 0, 7, 9999', () => {
|
|
expect(updateContributionSchema.safeParse({ contributionAmount: 0 }).success).toBe(true)
|
|
expect(updateContributionSchema.safeParse({ contributionAmount: 7 }).success).toBe(true)
|
|
expect(updateContributionSchema.safeParse({ contributionAmount: 9999 }).success).toBe(true)
|
|
})
|
|
|
|
it('rejects invalid contributionAmount values', () => {
|
|
expect(updateContributionSchema.safeParse({ contributionAmount: -1 }).success).toBe(false)
|
|
expect(updateContributionSchema.safeParse({ contributionAmount: 1.5 }).success).toBe(false)
|
|
expect(updateContributionSchema.safeParse({ contributionAmount: '15' }).success).toBe(false)
|
|
})
|
|
|
|
it('strips unknown fields', () => {
|
|
const result = updateContributionSchema.safeParse({
|
|
contributionAmount: 15,
|
|
role: 'admin'
|
|
})
|
|
expect(result.success).toBe(true)
|
|
expect(result.data).not.toHaveProperty('role')
|
|
})
|
|
|
|
it('accepts cadence: monthly', () => {
|
|
const result = updateContributionSchema.safeParse({ contributionAmount: 15, cadence: 'monthly' })
|
|
expect(result.success).toBe(true)
|
|
expect(result.data.cadence).toBe('monthly')
|
|
})
|
|
|
|
it('accepts cadence: annual', () => {
|
|
const result = updateContributionSchema.safeParse({ contributionAmount: 15, cadence: 'annual' })
|
|
expect(result.success).toBe(true)
|
|
expect(result.data.cadence).toBe('annual')
|
|
})
|
|
|
|
it('rejects cadence: weekly', () => {
|
|
const result = updateContributionSchema.safeParse({ contributionAmount: 15, cadence: 'weekly' })
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('defaults cadence to monthly when omitted', () => {
|
|
const result = updateContributionSchema.safeParse({ contributionAmount: 15 })
|
|
expect(result.success).toBe(true)
|
|
expect(result.data.cadence).toBe('monthly')
|
|
})
|
|
})
|
|
|
|
// --- Series schemas ---
|
|
|
|
describe('seriesTicketPurchaseSchema', () => {
|
|
it('accepts valid series ticket purchase', () => {
|
|
const result = seriesTicketPurchaseSchema.safeParse({
|
|
name: 'Buyer',
|
|
email: 'buyer@example.com',
|
|
ticketType: 'member'
|
|
})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('rejects missing name', () => {
|
|
const result = seriesTicketPurchaseSchema.safeParse({
|
|
email: 'buyer@example.com',
|
|
ticketType: 'member'
|
|
})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
})
|
|
|
|
// --- Admin schemas ---
|
|
|
|
describe('adminSeriesCreateSchema', () => {
|
|
it('accepts valid series', () => {
|
|
const result = adminSeriesCreateSchema.safeParse({
|
|
id: 'test-series',
|
|
title: 'Test Series',
|
|
description: 'A test series'
|
|
})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('rejects missing id', () => {
|
|
const result = adminSeriesCreateSchema.safeParse({
|
|
title: 'Test',
|
|
description: 'Desc'
|
|
})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('adminMemberCreateSchema', () => {
|
|
it('accepts valid admin member create', () => {
|
|
const result = adminMemberCreateSchema.safeParse({
|
|
name: 'Admin Created',
|
|
email: 'admin-created@example.com',
|
|
circle: 'founder',
|
|
contributionAmount: 30
|
|
})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('strips role field (mass assignment)', () => {
|
|
const result = adminMemberCreateSchema.safeParse({
|
|
name: 'Admin Created',
|
|
email: 'admin-created@example.com',
|
|
circle: 'founder',
|
|
contributionAmount: 30,
|
|
role: 'admin'
|
|
})
|
|
expect(result.success).toBe(true)
|
|
expect(result.data).not.toHaveProperty('role')
|
|
})
|
|
|
|
it('strips status field (mass assignment)', () => {
|
|
const result = adminMemberCreateSchema.safeParse({
|
|
name: 'Admin Created',
|
|
email: 'admin-created@example.com',
|
|
circle: 'founder',
|
|
contributionAmount: 30,
|
|
status: 'active'
|
|
})
|
|
expect(result.success).toBe(true)
|
|
expect(result.data).not.toHaveProperty('status')
|
|
})
|
|
|
|
it('rejects invalid circle', () => {
|
|
const result = adminMemberCreateSchema.safeParse({
|
|
name: 'Admin Created',
|
|
email: 'admin-created@example.com',
|
|
circle: 'superadmin',
|
|
contributionAmount: 30
|
|
})
|
|
expect(result.success).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('adminEventUpdateSchema', () => {
|
|
const validUpdate = {
|
|
title: 'Updated Event',
|
|
description: 'Updated description',
|
|
startDate: '2026-04-01T10:00:00Z',
|
|
endDate: '2026-04-01T12:00:00Z'
|
|
}
|
|
|
|
it('accepts valid event update', () => {
|
|
const result = adminEventUpdateSchema.safeParse(validUpdate)
|
|
expect(result.success).toBe(true)
|
|
})
|
|
|
|
it('rejects missing title', () => {
|
|
const { title, ...rest } = validUpdate
|
|
const result = adminEventUpdateSchema.safeParse(rest)
|
|
expect(result.success).toBe(false)
|
|
})
|
|
|
|
it('accepts optional tickets', () => {
|
|
const result = adminEventUpdateSchema.safeParse({
|
|
...validUpdate,
|
|
tickets: {
|
|
enabled: true,
|
|
public: {
|
|
available: true,
|
|
price: 25.00
|
|
}
|
|
}
|
|
})
|
|
expect(result.success).toBe(true)
|
|
})
|
|
})
|
|
|
|
// --- Error text forwarding regression tests ---
|
|
|
|
describe('error text forwarding regression', () => {
|
|
const helcimFiles = [
|
|
'create-plan.post.js',
|
|
'customer.post.js',
|
|
'customer-code.get.js',
|
|
'initialize-payment.post.js',
|
|
'subscription.post.js',
|
|
'update-billing.post.js',
|
|
'get-or-create-customer.post.js'
|
|
]
|
|
|
|
for (const file of helcimFiles) {
|
|
it(`${file} does not forward error text to client`, () => {
|
|
const source = readFileSync(
|
|
resolve(import.meta.dirname, `../../../server/api/helcim/${file}`),
|
|
'utf-8'
|
|
)
|
|
// Should not contain template literals that interpolate errorText or error.message into statusMessage
|
|
expect(source).not.toMatch(/statusMessage:.*\$\{errorText\}/)
|
|
expect(source).not.toMatch(/statusMessage:\s*error\.message\b/)
|
|
})
|
|
}
|
|
|
|
it('members/update-contribution.post.js does not forward error text', () => {
|
|
const source = readFileSync(
|
|
resolve(import.meta.dirname, '../../../server/api/members/update-contribution.post.js'),
|
|
'utf-8'
|
|
)
|
|
expect(source).not.toMatch(/statusMessage:.*\$\{errorText\}/)
|
|
expect(source).not.toMatch(/statusMessage:\s*error\.message\b/)
|
|
})
|
|
})
|
|
|
|
// --- validateBody migration coverage ---
|
|
|
|
describe('validateBody migration coverage', () => {
|
|
const endpoints = [
|
|
'helcim/create-plan.post.js',
|
|
'helcim/customer.post.js',
|
|
'helcim/initialize-payment.post.js',
|
|
'helcim/subscription.post.js',
|
|
'helcim/update-billing.post.js',
|
|
'events/[id]/tickets/purchase.post.js',
|
|
'events/[id]/tickets/reserve.post.js',
|
|
'events/[id]/tickets/check-eligibility.post.js',
|
|
'events/[id]/waitlist.post.js',
|
|
'events/[id]/waitlist.delete.js',
|
|
'events/[id]/cancel-registration.post.js',
|
|
'events/[id]/check-registration.post.js',
|
|
'events/[id]/payment.post.js',
|
|
'members/update-contribution.post.js',
|
|
'series/[id]/tickets/purchase.post.js',
|
|
'series/[id]/tickets/check-eligibility.post.js',
|
|
'admin/series.post.js',
|
|
'admin/series.put.js',
|
|
'admin/series/tickets.put.js',
|
|
'admin/series/[id].put.js',
|
|
'admin/events/[id].put.js',
|
|
'admin/members.post.js'
|
|
]
|
|
|
|
for (const endpoint of endpoints) {
|
|
it(`${endpoint} uses validateBody instead of raw readBody`, () => {
|
|
const source = readFileSync(
|
|
resolve(import.meta.dirname, `../../../server/api/${endpoint}`),
|
|
'utf-8'
|
|
)
|
|
expect(source).toContain('validateBody(event')
|
|
// Should not have bare readBody calls (except inside validateBody itself)
|
|
const lines = source.split('\n')
|
|
for (const line of lines) {
|
|
if (line.includes('readBody(event)') && !line.includes('validateBody')) {
|
|
expect.fail(`${endpoint} still has raw readBody: ${line.trim()}`)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
})
|