fix(mail-templates): add sample values for the upload-link placeholders (#260)

submitUrl, label and submissionsAllowed were added to the uploadLink template but SAMPLE_VALUES had no entries for any of them, so an admin opening Email templates, Upload link for a contributor, Preview saw literal {{submitUrl}} in the body — the preview being the only way to check an edit before saving.

The unit test meant to catch exactly this, in emailTemplates.test.ts, iterated a hardcoded KEYS array that predated intakeDraft and uploadLink, so it never checked either template's samples. KEYS is now Object.keys(TEMPLATES) as TemplateKey[], so the guard covers every template automatically and cannot go stale the same way again. intakeDraft already had samples for all its placeholders and passes once included, as expected.

One other test in the same file, "every template can address the customer", asserts that available contains greeting/firstName/lastName — a real invariant of the six customer-facing templates, but not of intakeDraft or uploadLink, which notify the shop and a contributor rather than a customer with a name on file. Switching that test to the new all-templates KEYS would have made it fail for both, so it now uses its own explicit CUSTOMER_FACING_KEYS list instead. That is a deliberate, commented exception: a hardcoded list is correct there because the claim itself does not extend to every template, whereas the SAMPLE_VALUES guard's claim does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-03 18:31:03 -05:00
co-authored by Claude Opus 5
parent 0dd4ac36ff
commit ba26ee99f0
2 changed files with 25 additions and 10 deletions
+4 -1
View File
@@ -281,7 +281,10 @@ export const SAMPLE_VALUES: Record<string, string> = {
regenerateUrl: 'https://example.com/api/intake-actions/1/regenerate?expires=0&sig=sample',
discardUrl: 'https://example.com/api/intake-actions/1/discard?expires=0&sig=sample',
expiresIn: 'one hour',
holdDuration: '24 hours'
holdDuration: '24 hours',
submitUrl: 'https://example.com/submit/sample-token',
label: 'Autumn drop-off',
submissionsAllowed: '25 items'
};
export interface StoredTemplate {
+21 -9
View File
@@ -8,14 +8,11 @@ import {
SAMPLE_VALUES
} from '../../src/emailTemplates';
const KEYS: TemplateKey[] = [
'verification',
'passwordReset',
'favoriteSold',
'favoriteWithdrawn',
'cartReminder',
'emailChanged'
];
// Derived from TEMPLATES rather than hardcoded, so a new template is covered
// by every it.each below the moment it is added. A hardcoded list silently
// stops covering anything added after it was written — which is exactly how
// intakeDraft and uploadLink went untested by the SAMPLE_VALUES guard below.
const KEYS = Object.keys(TEMPLATES) as TemplateKey[];
describe('the built-in templates', () => {
it.each(KEYS)('%s has a default subject and body', (key) => {
@@ -268,7 +265,22 @@ describe('greeting, built from the configured format', () => {
});
describe('every template can address the customer', () => {
it.each(KEYS)('%s offers greeting, firstName and lastName', (key) => {
// Not KEYS: this is an invariant of the six customer-facing templates only.
// intakeDraft and uploadLink notify the shop and a contributor respectively,
// not a customer with a name on file, so they are deliberately not held to
// it — a hardcoded list is correct here rather than a staleness risk,
// because the set of templates this claim applies to does not grow just
// because TEMPLATES does.
const CUSTOMER_FACING_KEYS: TemplateKey[] = [
'verification',
'passwordReset',
'favoriteSold',
'favoriteWithdrawn',
'cartReminder',
'emailChanged'
];
it.each(CUSTOMER_FACING_KEYS)('%s offers greeting, firstName and lastName', (key) => {
expect(TEMPLATES[key].available).toEqual(
expect.arrayContaining(['greeting', 'firstName', 'lastName'])
);