fix(email): gate resend wrappers behind ALLOW_DEV_TEST_ENDPOINTS

All five resend.js send wrappers (registration, cancellation, waitlist,
series pass, welcome) dispatched live in dev. Add a skipEmailInDev guard
mirroring the gate in pre-registrants/invite.post.js so dev runs and e2e
don't fire real Resend sends. Also add the monthly-onboarding Slack-timing
line to the welcome email. Unit-tested.
This commit is contained in:
Jennie Robinson Faber 2026-05-24 22:17:24 +01:00
parent a9312c423b
commit dac423afcd
2 changed files with 118 additions and 0 deletions

View file

@ -0,0 +1,85 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import {
sendEventRegistrationEmail,
sendEventCancellationEmail,
sendWaitlistNotificationEmail,
sendSeriesPassConfirmation,
sendWelcomeEmail,
} from '../../../server/utils/resend.js'
// Hoisted spy so the resend mock and the assertions share one reference.
const { sendSpy } = vi.hoisted(() => ({ sendSpy: vi.fn() }))
vi.mock('resend', () => ({
Resend: class {
constructor() {
this.emails = { send: sendSpy }
}
},
}))
const eventData = {
title: 'Co-op Basics',
startDate: '2026-06-01T17:00:00.000Z',
endDate: '2026-06-01T18:00:00.000Z',
location: 'Online',
slug: 'co-op-basics',
}
const registration = { email: 'reg@example.com', name: 'Reg', ticketType: 'member', amountPaid: 0 }
const seriesPassOptions = {
to: 'pass@example.com',
name: 'Pass Holder',
series: { title: 'Workshop Series' },
ticket: { type: 'member', price: 0, currency: 'CAD', isFree: true },
events: [eventData],
paymentId: null,
}
const member = { email: 'welcome@example.com', name: 'New Member', circle: 'Community' }
describe('resend email wrappers — ALLOW_DEV_TEST_ENDPOINTS gate', () => {
beforeEach(() => {
sendSpy.mockReset()
sendSpy.mockResolvedValue({ data: { id: 'email_1' }, error: null })
})
afterEach(() => {
delete process.env.ALLOW_DEV_TEST_ENDPOINTS
})
describe('when ALLOW_DEV_TEST_ENDPOINTS=true', () => {
beforeEach(() => {
process.env.ALLOW_DEV_TEST_ENDPOINTS = 'true'
})
const cases = [
['registration', () => sendEventRegistrationEmail(registration, eventData)],
['cancellation', () => sendEventCancellationEmail(registration, eventData)],
['waitlist', () => sendWaitlistNotificationEmail(registration, eventData)],
['series pass', () => sendSeriesPassConfirmation(seriesPassOptions)],
['welcome', () => sendWelcomeEmail(member)],
]
it.each(cases)('skips the live send for %s', async (_label, call) => {
const result = await call()
expect(result).toEqual({ success: true, skipped: true })
expect(sendSpy).not.toHaveBeenCalled()
})
})
describe('when the gate is off', () => {
it('dispatches a live send and returns success', async () => {
const result = await sendWelcomeEmail(member)
expect(sendSpy).toHaveBeenCalledTimes(1)
expect(result).toEqual({ success: true, data: { id: 'email_1' } })
})
it('includes the monthly-onboarding Slack-timing line in the welcome email', async () => {
await sendWelcomeEmail(member)
const sent = sendSpy.mock.calls[0][0]
expect(sent.text).toContain('monthly onboarding waves')
})
})
})