import { describe, it, expect } from 'vitest' import { readFileSync } from 'node:fs' import { resolve } from 'node:path' const seriesDir = resolve(import.meta.dirname, '../../../server/api/series/[id]') describe('series tickets/purchase.post.js — guest account upsert (Fix #8)', () => { const source = readFileSync(resolve(seriesDir, 'tickets/purchase.post.js'), 'utf-8') it('uses validateBody with seriesTicketPurchaseSchema', () => { expect(source).toContain('validateBody(event, seriesTicketPurchaseSchema)') }) it('Case 1 (free) + Case 2 (paid): upserts a guest Member when unauthenticated buyer provides name+email', () => { // Mirror event endpoint upsert pattern; ALWAYS-CREATE-GUEST (no opt-in // checkbox), so guard is `if (!member)` rather than `if (!member && body.createAccount)`. expect(source).toContain('findOneAndUpdate') expect(source).toContain('$setOnInsert') expect(source).toContain('status: "guest"') expect(source).toContain('upsert: true') expect(source).toContain('circle: "community"') expect(source).toContain('contributionAmount: 0') // ALWAYS-CREATE — must NOT gate on a createAccount flag expect(source).not.toContain('body.createAccount') }) it('Case 3 (idempotency): upsert pattern handles concurrent same-email registrations atomically', () => { // findOneAndUpdate with $setOnInsert + upsert:true is the idempotent pattern; // email has a unique index. No duplicate Member doc created on retry. expect(source).toMatch(/findOneAndUpdate\(\s*\{\s*email:/) expect(source).toContain('upsert: true') expect(source).toContain('new: true') expect(source).toContain('setDefaultsOnInsert: true') }) it('Case 4 (existing real member): does not auto-login real members entered via public form', () => { // Auto-login only for newly-created accounts and existing guests. // Real members (active/pending_payment) get requiresSignIn: true instead. expect(source).toContain('accountCreated || member.status === "guest"') expect(source).toContain('requiresSignIn = true') }) it('Case 5 (authenticated guest): sets auth cookie on signedIn:true response', () => { // setAuthCookie fires for both new accounts and returning guests. expect(source).toContain('setAuthCookie(event, member)') expect(source).toContain('signedIn = true') }) it('Case 6 (missing fields): relies on schema validation to reject missing name/email', () => { // No new validation logic added — existing seriesTicketPurchaseSchema // already requires name+email; validateBody throws 400 if missing. expect(source).toContain('validateBody(event, seriesTicketPurchaseSchema)') }) it('includes accountCreated, signedIn, and requiresSignIn in response (parity with event endpoint)', () => { expect(source).toContain('accountCreated,') expect(source).toContain('signedIn,') expect(source).toContain('requiresSignIn,') }) it('still uses hasMemberAccess to gate member pricing (guest/suspended/cancelled treated as non-members)', () => { expect(source).toContain('hasMemberAccess(member)') }) it('preserves try/catch around requireAuth so unauthenticated callers fall through', () => { // Required for unauth guest-purchase flow to work at all. expect(source).toMatch(/try\s*\{[^}]*requireAuth\(event\)[^}]*\}\s*catch/s) }) it('does not block purchase when confirmation email fails', () => { const emailCallIndex = source.indexOf('await sendSeriesPassConfirmation') 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') }) }) describe('SeriesPassPurchase.vue — client auth refresh (Fix #8)', () => { const source = readFileSync( resolve(import.meta.dirname, '../../../app/components/SeriesPassPurchase.vue'), 'utf-8' ) it('refreshes client auth state via useAuth().checkMemberStatus() when server reports signedIn', () => { expect(source).toContain('useAuth().checkMemberStatus()') expect(source).toMatch(/purchaseResponse\?\.signedIn/) }) it('shows a one-line guest-account hint under the form (no checkbox)', () => { // Per ALWAYS-CREATE-GUEST decision: hint only, no UI control. expect(source).toMatch(/free guest account/i) // Make sure no checkbox was added by mistake. expect(source).not.toMatch(/createAccount/) expect(source).not.toMatch(/]*type="checkbox"/i) }) })