Files
braidsbymira/supabase/functions/_shared/email.test.ts
belviskhoremk 35e929c5e1 fix: harden Edge Function notification path per final review
Fixes issues raised by the whole-branch review before merge:

- Critical: the previous migration hardcoded the live production
  functions URL and anon key as the committed default, so every local
  `supabase db reset` silently pointed dev/test triggers at the real
  project. Adding a new migration instead of editing the already-applied
  one (checksum/drift safety), with local dev values as the committed
  default; production values are applied out-of-band, never committed.

- Important: verify_jwt=true plus sending the anon key as a bearer
  token didn't actually stop anyone, since the anon key is public
  (ships in the frontend bundle) — confirmed empirically, a request
  with a valid anon key but no shared secret now gets a real 401.
  Added a random shared secret known only to the database and the
  Edge Functions' secret store.

- Important: notification emails interpolated raw contact fields into
  HTML with no escaping. Added escapeHtml() and tests proving injected
  markup renders as literal text, not live markup.

- Important: the URL/key/secret functions moved from public to a
  private schema with EXECUTE revoked from anon/authenticated, closing
  an unintended PostgREST RPC endpoint (verified 404 after the fix).

Also adds a test:functions npm script now that Deno is confirmed
available and the shared email builders' tests pass (12 total incl.
the new escaping cases).

Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
2026-08-12 10:15:34 +02:00

81 lines
3.4 KiB
TypeScript

import { assertEquals, assertStringIncludes } from 'jsr:@std/assert'
import { buildBookingNotificationEmail, buildInquiryNotificationEmail } from './email.ts'
Deno.test('buildBookingNotificationEmail includes the service, date, time, and contact details', () => {
const { subject, html } = buildBookingNotificationEmail({
serviceName: 'Knotless Braids',
date: '2026-09-01',
time: '10:00 AM',
contact: { name: 'Jane Doe', phone: '555-0100', email: 'jane@example.com', notes: 'First time client' },
})
assertEquals(subject, 'New booking: Knotless Braids on 2026-09-01')
assertStringIncludes(html, 'Jane Doe')
assertStringIncludes(html, 'Knotless Braids')
assertStringIncludes(html, '2026-09-01')
assertStringIncludes(html, '10:00 AM')
assertStringIncludes(html, '555-0100')
assertStringIncludes(html, 'jane@example.com')
assertStringIncludes(html, 'First time client')
})
Deno.test('buildBookingNotificationEmail omits the notes paragraph when notes are empty', () => {
const { html } = buildBookingNotificationEmail({
serviceName: 'Cornrows',
date: '2026-09-02',
time: '1:00 PM',
contact: { name: 'Amy', phone: '555-0101', email: 'amy@example.com', notes: '' },
})
if (html.includes('Notes:')) throw new Error('expected no Notes: paragraph')
})
Deno.test('buildInquiryNotificationEmail includes the product and contact details', () => {
const { subject, html } = buildInquiryNotificationEmail({
productName: 'Silk Edge Control',
contact: { name: 'Nadia', email: 'nadia@example.com', notes: 'Is this in stock?' },
})
assertEquals(subject, 'New inquiry: Silk Edge Control')
assertStringIncludes(html, 'Nadia')
assertStringIncludes(html, 'nadia@example.com')
assertStringIncludes(html, 'Silk Edge Control')
assertStringIncludes(html, 'Is this in stock?')
})
Deno.test('buildInquiryNotificationEmail omits the notes paragraph when notes are empty', () => {
const { html } = buildInquiryNotificationEmail({
productName: 'Braid Oil',
contact: { name: 'Kim', email: 'kim@example.com', notes: '' },
})
if (html.includes('Notes:')) throw new Error('expected no Notes: paragraph')
})
Deno.test('buildBookingNotificationEmail escapes HTML in contact fields so they cannot inject markup', () => {
const { html } = buildBookingNotificationEmail({
serviceName: 'Knotless Braids',
date: '2026-09-01',
time: '10:00 AM',
contact: {
name: '<a href="https://evil.example">Click here</a>',
phone: '555-0100',
email: 'jane@example.com',
notes: '<img src=x onerror=alert(1)>',
},
})
if (html.includes('<a href="https://evil.example">')) throw new Error('expected contact name to be escaped')
if (html.includes('<img src=x onerror=alert(1)>')) throw new Error('expected notes to be escaped')
assertStringIncludes(html, '&lt;a href=&quot;https://evil.example&quot;&gt;Click here&lt;/a&gt;')
assertStringIncludes(html, '&lt;img src=x onerror=alert(1)&gt;')
})
Deno.test('buildInquiryNotificationEmail escapes HTML in contact fields so they cannot inject markup', () => {
const { html } = buildInquiryNotificationEmail({
productName: 'Silk Edge Control',
contact: {
name: '<script>alert(1)</script>',
email: 'nadia@example.com',
notes: '',
},
})
if (html.includes('<script>alert(1)</script>')) throw new Error('expected contact name to be escaped')
assertStringIncludes(html, '&lt;script&gt;alert(1)&lt;/script&gt;')
})