notifications(P4d): form submission submitter + admin alerts
submitForm now enqueues two kinds of notifications, gated per the design:
- form_submission_received: ONLY when the submitter is authenticated
(members or public_auth visibility). Recipient is the verified auth
email (user.email), never the typed body.email. Anonymous public
submissions skip the confirmation because confirming to a typed
address is a victim-spam vector.
- form_submission_admin: one row per member who holds forms.manage in
the space (mirrors the forms-guard / forms RLS gate). Dedupe by
(submission, admin) so a replay is a no-op.
Finding all permission holders without N round-trips needs a new SQL
helper. Migration 047 adds members_with_permission(sid, perm): the
inverted, set-returning form of user_has_permission, with the same
current/late status gate (046) and the same admin shortcut +
space_role_permissions / user_effective_roles fallback. Additive,
introduces no new tables or policies; existing callers unchanged.
Mirrored into schema.sql; types/database.ts updated.
renderFormEmail + formDedupeKey live in notifications-logic; 6 new
render/dedupe unit tests cover both types, fallback labels, escaping.
Suite 565, build clean.
This commit is contained in:
@@ -10,6 +10,8 @@ import {
|
||||
renderBookingEmail,
|
||||
classDedupeKey,
|
||||
renderClassEmail,
|
||||
formDedupeKey,
|
||||
renderFormEmail,
|
||||
} from '@/lib/notifications-logic'
|
||||
|
||||
describe('isTerminalAttempt', () => {
|
||||
@@ -309,3 +311,75 @@ describe('renderClassEmail', () => {
|
||||
expect(r.html).toContain('<b>x</b>')
|
||||
})
|
||||
})
|
||||
|
||||
describe('formDedupeKey', () => {
|
||||
it('submitter confirmation is one row per submission', () => {
|
||||
expect(formDedupeKey('form_submission_received', { submissionId: 's1' })).toBe(
|
||||
'form_submission_received:s1',
|
||||
)
|
||||
})
|
||||
it('admin alert fans out one row per (submission, admin)', () => {
|
||||
expect(
|
||||
formDedupeKey('form_submission_admin', { submissionId: 's1', adminMemberId: 'a1' }),
|
||||
).toBe('form_submission_admin:s1:a1')
|
||||
expect(
|
||||
formDedupeKey('form_submission_admin', { submissionId: 's1', adminMemberId: 'a1' }),
|
||||
).not.toBe(
|
||||
formDedupeKey('form_submission_admin', { submissionId: 's1', adminMemberId: 'a2' }),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('renderFormEmail', () => {
|
||||
it('submitter received: thank-you subject + body', () => {
|
||||
const r = renderFormEmail({
|
||||
type: 'form_submission_received',
|
||||
spaceName: 'Acme',
|
||||
recipientName: 'Ada',
|
||||
formTitle: 'Liability Waiver',
|
||||
manageUrl: 'https://x.test/me',
|
||||
})
|
||||
expect(r.subject).toBe('Received: Liability Waiver at Acme')
|
||||
expect(r.text).toContain('Hi Ada,')
|
||||
expect(r.text).toContain('received your submission to Liability Waiver at Acme')
|
||||
expect(r.text).toContain('https://x.test/me')
|
||||
})
|
||||
|
||||
it('admin alert: subject names the form; body names the submitter and the link goes to the results URL', () => {
|
||||
const r = renderFormEmail({
|
||||
type: 'form_submission_admin',
|
||||
spaceName: 'Acme',
|
||||
recipientName: 'Boss',
|
||||
formTitle: 'Liability Waiver',
|
||||
manageUrl: 'https://x.test/forms/abc/results',
|
||||
submitterLabel: 'Ada Lovelace',
|
||||
})
|
||||
expect(r.subject).toBe('New submission: Liability Waiver at Acme')
|
||||
expect(r.text).toContain('Ada Lovelace submitted Liability Waiver at Acme')
|
||||
expect(r.text).toContain('https://x.test/forms/abc/results')
|
||||
})
|
||||
|
||||
it('admin alert: falls back to "someone" when submitterLabel is missing', () => {
|
||||
const r = renderFormEmail({
|
||||
type: 'form_submission_admin',
|
||||
spaceName: 'Acme',
|
||||
formTitle: 'Liability Waiver',
|
||||
manageUrl: 'https://x.test/forms/abc/results',
|
||||
})
|
||||
expect(r.text.toLowerCase()).toContain('someone submitted')
|
||||
})
|
||||
|
||||
it('escapes HTML in form title and submitter label', () => {
|
||||
const r = renderFormEmail({
|
||||
type: 'form_submission_admin',
|
||||
spaceName: 'Acme',
|
||||
formTitle: '<script>',
|
||||
manageUrl: 'https://x.test/forms/abc/results',
|
||||
submitterLabel: '<img>',
|
||||
})
|
||||
expect(r.html).not.toContain('<script>')
|
||||
expect(r.html).not.toContain('<img>')
|
||||
expect(r.html).toContain('<script>')
|
||||
expect(r.html).toContain('<img>')
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user