ghostguild-org/tests/server/api/validation-phase3.test.js
Jennie Robinson Faber 208638e374 feat(launch): security and correctness fixes for 2026-05-01 launch
Day-of-launch deep-dive audit and remediation. 11 issues fixed across
security, correctness, and reliability. Tests: 698 → 758 passing
(+60), 0 failing, 2 skipped.

CRITICAL (security)

Fix #1 — HELCIM_API_TOKEN removed from runtimeConfig.public; dead
useHelcim.js deleted. Production token MUST BE ROTATED post-deploy
(was previously exposed in window.__NUXT__ payload).

Fix #2 — /api/helcim/customer gated with origin check + per-IP/email
rate limit + magic-link email verification (replaces unauthenticated
setAuthCookie). Adds payment-bridge token for paid-tier signup so
users can complete Helcim checkout before email verify. New utils:
server/utils/{magicLink,rateLimit}.js. UX: signup success copy now
prompts user to check email.

Fix #3 — /api/events/[id]/payment deleted (dead code with unauth
member-spoof bypass — processHelcimPayment was a permanent stub).
Removes processHelcimPayment export and eventPaymentSchema.

Fix #4 — /api/helcim/initialize-payment re-derives ticket amount
server-side via calculateTicketPrice and calculateSeriesTicketPrice.
Adds new series_ticket metadata type (was being shoved through
event_ticket with seriesId in metadata.eventId).

Fix #5 — /api/helcim/customer upgrades existing status:guest members
in place rather than rejecting with 409. Lowercases email at lookup;
preserves _id so prior event registrations stay linked.

HIGH (correctness / reliability)

Fix #6 — Daily reconciliation cron via Netlify scheduled function
(@daily). New: netlify.toml, netlify/functions/reconcile-payments.mjs,
server/api/internal/reconcile-payments.post.js. Shared-secret auth
via NUXT_RECONCILE_TOKEN env var. Inline 3-retry exponential backoff
on Helcim transactions API.

Fix #7 — validateBeforeSave: false on event subdoc saves (waitlist
endpoints) to dodge legacy location validators.

Fix #8 — /api/series/[id]/tickets/purchase always upserts a guest
Member when caller is unauthenticated, mirrors event-ticket flow
byte-for-byte. SeriesPassPurchase.vue adds guest-account hint and
client auth refresh on signedIn:true response.

Fix #9 — /api/members/cancel-subscription leaves status active per
ratified bylaws (was pending_payment). Adds lastCancelledAt audit
field on Member model. Indirectly fixes false-positive
detectStuckPendingPayment admin alert for cancelled members.

Fix #10 — /api/auth/verify uses validateBody with strict() Zod schema
(verifyMagicLinkSchema, max 2000 chars).

Fix #11 — 8 vitest cases for cancel-subscription handler (was
uncovered).

Specs and audit at docs/superpowers/specs/2026-04-25-fix-*.md and
docs/superpowers/plans/2026-04-25-launch-readiness-fixes.md.
LAUNCH_READINESS.md updated with new test count, 3 deploy-time
tasks (rotate Helcim token, set NUXT_RECONCILE_TOKEN, verify
Netlify scheduled function), and Fixed-2026-04-25 fix log.
2026-04-25 18:42:36 +01:00

567 lines
16 KiB
JavaScript

import { describe, it, expect } from 'vitest'
import {
helcimCreatePlanSchema,
helcimCustomerSchema,
helcimInitializePaymentSchema,
helcimSubscriptionSchema,
helcimUpdateBillingSchema,
ticketPurchaseSchema,
ticketReserveSchema,
ticketEligibilitySchema,
waitlistSchema,
waitlistDeleteSchema,
cancelRegistrationSchema,
checkRegistrationSchema,
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)
})
})
// --- 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',
'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()}`)
}
}
})
}
})