@learncard/email-templates 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -43
- package/dist/previews/account-approved-scenarios.d.ts +2 -1
- package/dist/previews/account-approved-scenarios.d.ts.map +1 -1
- package/dist/previews/endorsement-request-scenarios.d.ts +2 -1
- package/dist/previews/endorsement-request-scenarios.d.ts.map +1 -1
- package/dist/previews/guardian-approval-scenarios.d.ts +2 -1
- package/dist/previews/guardian-approval-scenarios.d.ts.map +1 -1
- package/dist/previews/guardian-credential-scenarios.d.ts +2 -1
- package/dist/previews/guardian-credential-scenarios.d.ts.map +1 -1
- package/dist/previews/inbox-claim-scenarios.d.ts +2 -1
- package/dist/previews/inbox-claim-scenarios.d.ts.map +1 -1
- package/dist/previews/recovery-key-scenarios.d.ts +2 -1
- package/dist/previews/recovery-key-scenarios.d.ts.map +1 -1
- package/dist/previews/verification-code-scenarios.d.ts +2 -1
- package/dist/previews/verification-code-scenarios.d.ts.map +1 -1
- package/dist/templates/account-approved.d.ts +1 -1
- package/dist/templates/account-approved.d.ts.map +1 -1
- package/dist/templates/credential-awaiting-guardian.d.ts +1 -1
- package/dist/templates/credential-awaiting-guardian.d.ts.map +1 -1
- package/dist/templates/email-verification.d.ts +1 -1
- package/dist/templates/email-verification.d.ts.map +1 -1
- package/dist/templates/endorsement-request.d.ts +1 -1
- package/dist/templates/endorsement-request.d.ts.map +1 -1
- package/dist/templates/guardian-approval.d.ts +1 -1
- package/dist/templates/guardian-approval.d.ts.map +1 -1
- package/dist/templates/guardian-approved-claim.d.ts +1 -1
- package/dist/templates/guardian-approved-claim.d.ts.map +1 -1
- package/dist/templates/guardian-credential-approval.d.ts +1 -1
- package/dist/templates/guardian-credential-approval.d.ts.map +1 -1
- package/dist/templates/guardian-email-otp.d.ts +1 -1
- package/dist/templates/guardian-email-otp.d.ts.map +1 -1
- package/dist/templates/guardian-rejected-credential.d.ts +1 -1
- package/dist/templates/guardian-rejected-credential.d.ts.map +1 -1
- package/dist/templates/inbox-claim.d.ts +1 -1
- package/dist/templates/inbox-claim.d.ts.map +1 -1
- package/dist/templates/recovery-key.d.ts +1 -1
- package/dist/templates/recovery-key.d.ts.map +1 -1
- package/dist/templates/verification-code.d.ts +1 -1
- package/dist/templates/verification-code.d.ts.map +1 -1
- package/dist/templates/verification-code.js +9 -9
- package/dist/templates/verification-code.js.map +1 -1
- package/package.json +49 -39
- package/src/__tests__/render.test.ts +432 -0
- package/src/__tests__/sms.test.ts +95 -0
- package/src/__tests__/tenant-registry.test.ts +257 -0
- package/src/branding.ts +61 -0
- package/src/components/CodeBlock.tsx +61 -0
- package/src/components/EmailButton.tsx +36 -0
- package/src/components/IssuerLogo.tsx +41 -0
- package/src/components/Layout.tsx +192 -0
- package/src/components/LinkFallback.tsx +50 -0
- package/src/index.ts +50 -0
- package/src/previews/_fixtures.tsx +51 -0
- package/src/previews/account-approved-scenarios.tsx +31 -0
- package/src/previews/endorsement-request-scenarios.tsx +50 -0
- package/src/previews/guardian-approval-scenarios.tsx +39 -0
- package/src/previews/guardian-credential-scenarios.tsx +88 -0
- package/src/previews/inbox-claim-scenarios.tsx +66 -0
- package/src/previews/recovery-key-scenarios.tsx +32 -0
- package/src/previews/verification-code-scenarios.tsx +60 -0
- package/src/render.ts +409 -0
- package/src/sms.ts +68 -0
- package/src/templates/account-approved.tsx +105 -0
- package/src/templates/credential-awaiting-guardian.tsx +137 -0
- package/src/templates/email-verification.tsx +110 -0
- package/src/templates/endorsement-request.tsx +157 -0
- package/src/templates/guardian-approval.tsx +122 -0
- package/src/templates/guardian-approved-claim.tsx +114 -0
- package/src/templates/guardian-credential-approval.tsx +127 -0
- package/src/templates/guardian-email-otp.tsx +101 -0
- package/src/templates/guardian-rejected-credential.tsx +103 -0
- package/src/templates/inbox-claim.tsx +141 -0
- package/src/templates/index.ts +35 -0
- package/src/templates/recovery-key.tsx +102 -0
- package/src/templates/verification-code.tsx +164 -0
- package/src/tenant-registry.ts +204 -0
- package/LICENSE +0 -21
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { renderEmail } from '../render';
|
|
4
|
+
import type { TemplateId, TemplateDataMap } from '../render';
|
|
5
|
+
import { DEFAULT_BRANDING } from '../branding';
|
|
6
|
+
import type { TenantBranding } from '../branding';
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Fixtures
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
const VETPASS_BRANDING: Partial<TenantBranding> = {
|
|
13
|
+
brandName: 'VetPass',
|
|
14
|
+
logoAlt: 'VetPass',
|
|
15
|
+
primaryColor: '#1B5E20',
|
|
16
|
+
primaryTextColor: '#ffffff',
|
|
17
|
+
supportEmail: 'support@vetpass.app',
|
|
18
|
+
websiteUrl: 'https://www.vetpass.app',
|
|
19
|
+
appUrl: 'https://vetpass.app',
|
|
20
|
+
fromDomain: 'vetpass.app',
|
|
21
|
+
copyrightHolder: 'VetPass',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Minimal valid data for every template ID.
|
|
26
|
+
*
|
|
27
|
+
* Each entry must satisfy its TemplateDataMap shape so renderEmail() can
|
|
28
|
+
* produce output without throwing.
|
|
29
|
+
*/
|
|
30
|
+
const TEMPLATE_FIXTURES: { [K in TemplateId]: TemplateDataMap[K] } = {
|
|
31
|
+
'embed-email-verification': {
|
|
32
|
+
verificationCode: '123456',
|
|
33
|
+
verificationEmail: 'test@example.com',
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
'contact-method-verification': {
|
|
37
|
+
verificationToken: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
'login-verification-code': {
|
|
41
|
+
verificationCode: '654321',
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
'contact-method-verification-1': {
|
|
45
|
+
verificationCode: '111222',
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
'recovery-email-code': {
|
|
49
|
+
verificationCode: '999888',
|
|
50
|
+
verificationEmail: 'recover@example.com',
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
'recovery-email-verification': {
|
|
54
|
+
verificationCode: '777666',
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
'inbox-claim': {
|
|
58
|
+
claimUrl: 'https://learncard.app/claim/abc123',
|
|
59
|
+
credential: { name: 'Test Badge', type: 'badge' },
|
|
60
|
+
issuer: { name: 'Acme Corp' },
|
|
61
|
+
recipient: { name: 'Jane Doe', email: 'jane@example.com' },
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
'universal-inbox-claim': {
|
|
65
|
+
claimUrl: 'https://learncard.app/claim/def456',
|
|
66
|
+
credential: { name: 'Universal Badge' },
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
'guardian-approval': {
|
|
70
|
+
approvalUrl: 'https://learncard.app/guardian/approve',
|
|
71
|
+
approvalToken: 'tok_abc',
|
|
72
|
+
requester: { displayName: 'Alex Student', profileId: 'alex123' },
|
|
73
|
+
guardian: { email: 'parent@example.com' },
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
'account-approved': {
|
|
77
|
+
user: { displayName: 'New User' },
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
'account-approved-email': {
|
|
81
|
+
user: { displayName: 'Email User' },
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
'recovery-key': {
|
|
85
|
+
recoveryKey: 'ABCD-EFGH-1234-5678',
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
'recovery-key-backup': {
|
|
89
|
+
recoveryKey: 'WXYZ-9876-LMNO-4321',
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
'endorsement-request': {
|
|
93
|
+
shareLink: 'https://learncard.app/endorse/xyz',
|
|
94
|
+
recipient: { name: 'Bob Reviewer' },
|
|
95
|
+
issuer: { name: 'Learning Corp' },
|
|
96
|
+
credential: { name: 'Skills Badge' },
|
|
97
|
+
message: 'Please endorse my skills!',
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
'universal-inbox-claim-1': {
|
|
101
|
+
shareLink: 'https://learncard.app/endorse/legacy',
|
|
102
|
+
credential: { name: 'Legacy Badge' },
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
'credential-awaiting-guardian': {
|
|
106
|
+
issuer: { name: 'School District' },
|
|
107
|
+
credential: { name: 'Diploma' },
|
|
108
|
+
recipient: { email: 'student@example.com' },
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
'guardian-approved-claim': {
|
|
112
|
+
issuer: { name: 'School District' },
|
|
113
|
+
credential: { name: 'Diploma' },
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
'guardian-credential-approval': {
|
|
117
|
+
approvalUrl: 'https://learncard.app/guardian/credential/approve',
|
|
118
|
+
approvalToken: 'tok_cred',
|
|
119
|
+
issuer: { name: 'School District' },
|
|
120
|
+
credential: { name: 'Diploma' },
|
|
121
|
+
recipient: { email: 'student@example.com' },
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
'guardian-email-otp': {
|
|
125
|
+
verificationCode: '445566',
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
'guardian-rejected-credential': {
|
|
129
|
+
issuer: { name: 'School District' },
|
|
130
|
+
credential: { name: 'Diploma' },
|
|
131
|
+
recipient: { email: 'student@example.com' },
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const ALL_TEMPLATE_IDS = Object.keys(TEMPLATE_FIXTURES) as TemplateId[];
|
|
136
|
+
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// Smoke tests — every template renders without throwing
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
describe('renderEmail — smoke tests', () => {
|
|
142
|
+
it.each(ALL_TEMPLATE_IDS)(
|
|
143
|
+
'"%s" renders with default branding',
|
|
144
|
+
async (templateId) => {
|
|
145
|
+
const result = await renderEmail(templateId, DEFAULT_BRANDING, TEMPLATE_FIXTURES[templateId]);
|
|
146
|
+
|
|
147
|
+
expect(result.html).toBeTruthy();
|
|
148
|
+
expect(result.html).toContain('<!DOCTYPE');
|
|
149
|
+
expect(result.text).toBeTruthy();
|
|
150
|
+
expect(result.subject).toBeTruthy();
|
|
151
|
+
expect(result.subject.length).toBeGreaterThan(0);
|
|
152
|
+
},
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
it.each(ALL_TEMPLATE_IDS)(
|
|
156
|
+
'"%s" renders with custom (VetPass) branding',
|
|
157
|
+
async (templateId) => {
|
|
158
|
+
const result = await renderEmail(templateId, VETPASS_BRANDING, TEMPLATE_FIXTURES[templateId]);
|
|
159
|
+
|
|
160
|
+
expect(result.html).toBeTruthy();
|
|
161
|
+
expect(result.text).toBeTruthy();
|
|
162
|
+
expect(result.subject).toBeTruthy();
|
|
163
|
+
},
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
it.each(ALL_TEMPLATE_IDS)(
|
|
167
|
+
'"%s" renders with empty branding (falls back to defaults)',
|
|
168
|
+
async (templateId) => {
|
|
169
|
+
const result = await renderEmail(templateId, {}, TEMPLATE_FIXTURES[templateId]);
|
|
170
|
+
|
|
171
|
+
expect(result.html).toBeTruthy();
|
|
172
|
+
expect(result.subject).toBeTruthy();
|
|
173
|
+
},
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// Content assertion tests — dynamic values appear in rendered HTML
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
describe('renderEmail — content assertions', () => {
|
|
182
|
+
it('inbox-claim includes the claim URL and credential name', async () => {
|
|
183
|
+
const data = TEMPLATE_FIXTURES['inbox-claim'];
|
|
184
|
+
const { html } = await renderEmail('inbox-claim', DEFAULT_BRANDING, data);
|
|
185
|
+
|
|
186
|
+
expect(html).toContain(data.claimUrl);
|
|
187
|
+
expect(html).toContain('Test Badge');
|
|
188
|
+
expect(html).toContain('Acme Corp');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('inbox-claim includes recipient name when provided', async () => {
|
|
192
|
+
const data = TEMPLATE_FIXTURES['inbox-claim'];
|
|
193
|
+
const { html } = await renderEmail('inbox-claim', DEFAULT_BRANDING, data);
|
|
194
|
+
|
|
195
|
+
expect(html).toContain('Jane Doe');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('verification code templates include the code', async () => {
|
|
199
|
+
const data = TEMPLATE_FIXTURES['embed-email-verification'];
|
|
200
|
+
const { html } = await renderEmail('embed-email-verification', DEFAULT_BRANDING, data);
|
|
201
|
+
|
|
202
|
+
expect(html).toContain('123456');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('verification code templates include the email when provided', async () => {
|
|
206
|
+
const data = TEMPLATE_FIXTURES['embed-email-verification'];
|
|
207
|
+
const { html } = await renderEmail('embed-email-verification', DEFAULT_BRANDING, data);
|
|
208
|
+
|
|
209
|
+
expect(html).toContain('test@example.com');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('contact-method-verification includes the verify link with token', async () => {
|
|
213
|
+
const data = TEMPLATE_FIXTURES['contact-method-verification'];
|
|
214
|
+
const { html } = await renderEmail('contact-method-verification', DEFAULT_BRANDING, data);
|
|
215
|
+
|
|
216
|
+
expect(html).toContain('verify-email');
|
|
217
|
+
expect(html).toContain(data.verificationToken);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('guardian-approval includes the approval URL', async () => {
|
|
221
|
+
const data = TEMPLATE_FIXTURES['guardian-approval'];
|
|
222
|
+
const { html } = await renderEmail('guardian-approval', DEFAULT_BRANDING, data);
|
|
223
|
+
|
|
224
|
+
expect(html).toContain(data.approvalUrl);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('guardian-approval includes requester name', async () => {
|
|
228
|
+
const data = TEMPLATE_FIXTURES['guardian-approval'];
|
|
229
|
+
const { html } = await renderEmail('guardian-approval', DEFAULT_BRANDING, data);
|
|
230
|
+
|
|
231
|
+
expect(html).toContain('Alex Student');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('account-approved includes user display name', async () => {
|
|
235
|
+
const data = TEMPLATE_FIXTURES['account-approved'];
|
|
236
|
+
const { html } = await renderEmail('account-approved', DEFAULT_BRANDING, data);
|
|
237
|
+
|
|
238
|
+
expect(html).toContain('New User');
|
|
239
|
+
expect(html).toContain('has been approved');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('recovery-key includes the key value', async () => {
|
|
243
|
+
const data = TEMPLATE_FIXTURES['recovery-key'];
|
|
244
|
+
const { html } = await renderEmail('recovery-key', DEFAULT_BRANDING, data);
|
|
245
|
+
|
|
246
|
+
expect(html).toContain('ABCD-EFGH-1234-5678');
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it('endorsement-request includes the share link and message', async () => {
|
|
250
|
+
const data = TEMPLATE_FIXTURES['endorsement-request'];
|
|
251
|
+
const { html } = await renderEmail('endorsement-request', DEFAULT_BRANDING, data);
|
|
252
|
+
|
|
253
|
+
expect(html).toContain(data.shareLink);
|
|
254
|
+
expect(html).toContain('Please endorse my skills!');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('guardian-credential-approval includes approval URL and credential name', async () => {
|
|
258
|
+
const data = TEMPLATE_FIXTURES['guardian-credential-approval'];
|
|
259
|
+
const { html } = await renderEmail('guardian-credential-approval', DEFAULT_BRANDING, data);
|
|
260
|
+
|
|
261
|
+
expect(html).toContain(data.approvalUrl);
|
|
262
|
+
expect(html).toContain('Diploma');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('guardian-email-otp includes the OTP code', async () => {
|
|
266
|
+
const data = TEMPLATE_FIXTURES['guardian-email-otp'];
|
|
267
|
+
const { html } = await renderEmail('guardian-email-otp', DEFAULT_BRANDING, data);
|
|
268
|
+
|
|
269
|
+
expect(html).toContain('445566');
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('guardian-rejected-credential includes issuer and credential names', async () => {
|
|
273
|
+
const data = TEMPLATE_FIXTURES['guardian-rejected-credential'];
|
|
274
|
+
const { html } = await renderEmail('guardian-rejected-credential', DEFAULT_BRANDING, data);
|
|
275
|
+
|
|
276
|
+
expect(html).toContain('School District');
|
|
277
|
+
expect(html).toContain('Diploma');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('credential-awaiting-guardian includes credential name', async () => {
|
|
281
|
+
const data = TEMPLATE_FIXTURES['credential-awaiting-guardian'];
|
|
282
|
+
const { html } = await renderEmail('credential-awaiting-guardian', DEFAULT_BRANDING, data);
|
|
283
|
+
|
|
284
|
+
expect(html).toContain('Diploma');
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('guardian-approved-claim includes credential name', async () => {
|
|
288
|
+
const data = TEMPLATE_FIXTURES['guardian-approved-claim'];
|
|
289
|
+
const { html } = await renderEmail('guardian-approved-claim', DEFAULT_BRANDING, data);
|
|
290
|
+
|
|
291
|
+
expect(html).toContain('Diploma');
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
296
|
+
// Branding tests — custom branding flows through to rendered output
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
|
|
299
|
+
describe('renderEmail — branding', () => {
|
|
300
|
+
it('default branding includes LearnCard brand name', async () => {
|
|
301
|
+
const { html } = await renderEmail('account-approved', DEFAULT_BRANDING, { user: {} });
|
|
302
|
+
|
|
303
|
+
expect(html).toContain('LearnCard');
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('VetPass branding replaces brand name in HTML', async () => {
|
|
307
|
+
const { html } = await renderEmail('account-approved', VETPASS_BRANDING, { user: {} });
|
|
308
|
+
|
|
309
|
+
expect(html).toContain('VetPass');
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('VetPass branding uses custom primary color', async () => {
|
|
313
|
+
const { html } = await renderEmail('account-approved', VETPASS_BRANDING, { user: {} });
|
|
314
|
+
|
|
315
|
+
expect(html).toContain('#1B5E20');
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('VetPass branding includes correct support email', async () => {
|
|
319
|
+
const { html } = await renderEmail('account-approved', VETPASS_BRANDING, { user: {} });
|
|
320
|
+
|
|
321
|
+
expect(html).toContain('support@vetpass.app');
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('VetPass branding includes correct website URL', async () => {
|
|
325
|
+
const { html } = await renderEmail('account-approved', VETPASS_BRANDING, { user: {} });
|
|
326
|
+
|
|
327
|
+
expect(html).toContain('vetpass.app');
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('contact-method-verification uses appUrl for verify link', async () => {
|
|
331
|
+
const { html } = await renderEmail('contact-method-verification', VETPASS_BRANDING, {
|
|
332
|
+
verificationToken: 'test-token-123',
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
expect(html).toContain('https://vetpass.app/verify-email?token=test-token-123');
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// ---------------------------------------------------------------------------
|
|
340
|
+
// Subject line tests
|
|
341
|
+
// ---------------------------------------------------------------------------
|
|
342
|
+
|
|
343
|
+
describe('renderEmail — subjects', () => {
|
|
344
|
+
it('verification code subjects include brand name', async () => {
|
|
345
|
+
const { subject } = await renderEmail('login-verification-code', DEFAULT_BRANDING, {
|
|
346
|
+
verificationCode: '000000',
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
expect(subject).toContain('LearnCard');
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('inbox-claim subject includes credential name when available', async () => {
|
|
353
|
+
const { subject } = await renderEmail('inbox-claim', DEFAULT_BRANDING, {
|
|
354
|
+
claimUrl: 'https://example.com',
|
|
355
|
+
credential: { name: 'My Special Badge' },
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
expect(subject.length).toBeGreaterThan(0);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('contact-method-verification subject is "Verify Your Email"', async () => {
|
|
362
|
+
const { subject } = await renderEmail('contact-method-verification', DEFAULT_BRANDING, {
|
|
363
|
+
verificationToken: 'abc',
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
expect(subject).toBe('Verify Your Email');
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it('Postmark alias pairs produce the same subject', async () => {
|
|
370
|
+
const [a, b] = await Promise.all([
|
|
371
|
+
renderEmail('inbox-claim', DEFAULT_BRANDING, TEMPLATE_FIXTURES['inbox-claim']),
|
|
372
|
+
renderEmail('universal-inbox-claim', DEFAULT_BRANDING, TEMPLATE_FIXTURES['inbox-claim']),
|
|
373
|
+
]);
|
|
374
|
+
|
|
375
|
+
expect(a.subject).toBe(b.subject);
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// ---------------------------------------------------------------------------
|
|
380
|
+
// Plain text rendering
|
|
381
|
+
// ---------------------------------------------------------------------------
|
|
382
|
+
|
|
383
|
+
describe('renderEmail — plain text', () => {
|
|
384
|
+
it('plain text output does not contain HTML tags', async () => {
|
|
385
|
+
const { text } = await renderEmail('inbox-claim', DEFAULT_BRANDING, TEMPLATE_FIXTURES['inbox-claim']);
|
|
386
|
+
|
|
387
|
+
expect(text).not.toMatch(/<[a-z][\s\S]*>/i);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('plain text includes key content', async () => {
|
|
391
|
+
const { text } = await renderEmail('recovery-key', DEFAULT_BRANDING, TEMPLATE_FIXTURES['recovery-key']);
|
|
392
|
+
|
|
393
|
+
expect(text).toContain('ABCD-EFGH-1234-5678');
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// ---------------------------------------------------------------------------
|
|
398
|
+
// Edge cases
|
|
399
|
+
// ---------------------------------------------------------------------------
|
|
400
|
+
|
|
401
|
+
describe('renderEmail — edge cases', () => {
|
|
402
|
+
it('inbox-claim renders with minimal data (no optional fields)', async () => {
|
|
403
|
+
const { html, subject } = await renderEmail('inbox-claim', DEFAULT_BRANDING, {
|
|
404
|
+
claimUrl: 'https://example.com/claim',
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
expect(html).toContain('https://example.com/claim');
|
|
408
|
+
expect(subject).toBeTruthy();
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('endorsement-request renders without message', async () => {
|
|
412
|
+
const { html } = await renderEmail('endorsement-request', DEFAULT_BRANDING, {
|
|
413
|
+
shareLink: 'https://example.com/endorse',
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
expect(html).toContain('https://example.com/endorse');
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('account-approved renders without user display name', async () => {
|
|
420
|
+
const { html } = await renderEmail('account-approved', DEFAULT_BRANDING, {});
|
|
421
|
+
|
|
422
|
+
expect(html).toBeTruthy();
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it('contact-method-verification renders without recipient', async () => {
|
|
426
|
+
const { html } = await renderEmail('contact-method-verification', DEFAULT_BRANDING, {
|
|
427
|
+
verificationToken: 'no-recipient-token',
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
expect(html).toContain('no-recipient-token');
|
|
431
|
+
});
|
|
432
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { renderSms } from '../sms';
|
|
4
|
+
import { DEFAULT_BRANDING } from '../branding';
|
|
5
|
+
import type { TenantBranding } from '../branding';
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Fixtures
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
const VETPASS_BRANDING: Partial<TenantBranding> = {
|
|
12
|
+
brandName: 'VetPass',
|
|
13
|
+
appUrl: 'https://vetpass.app',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// inbox-claim SMS
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
describe('renderSms — inbox-claim', () => {
|
|
21
|
+
it('renders a claim SMS with full data', () => {
|
|
22
|
+
const body = renderSms('inbox-claim', DEFAULT_BRANDING, {
|
|
23
|
+
claimUrl: 'https://learncard.app/claim/abc',
|
|
24
|
+
recipient: { name: 'Jane' },
|
|
25
|
+
issuer: { name: 'Acme' },
|
|
26
|
+
credential: { name: 'Badge', type: 'badge' },
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
expect(body).toBeDefined();
|
|
30
|
+
expect(body).toContain('https://learncard.app/claim/abc');
|
|
31
|
+
expect(body).toContain('Jane');
|
|
32
|
+
expect(body).toContain('Acme');
|
|
33
|
+
expect(body).toContain('Badge');
|
|
34
|
+
expect(body).toContain('badge');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('renders without optional fields', () => {
|
|
38
|
+
const body = renderSms('inbox-claim', DEFAULT_BRANDING, {
|
|
39
|
+
claimUrl: 'https://learncard.app/claim/min',
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(body).toBeDefined();
|
|
43
|
+
expect(body).toContain('https://learncard.app/claim/min');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('falls back to "credential" type when not specified', () => {
|
|
47
|
+
const body = renderSms('inbox-claim', DEFAULT_BRANDING, {
|
|
48
|
+
claimUrl: 'https://example.com',
|
|
49
|
+
credential: { name: 'Some Cert' },
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(body).toContain('credential');
|
|
53
|
+
expect(body).toContain('Some Cert');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('falls back to "Unnamed Credential" when name is missing', () => {
|
|
57
|
+
const body = renderSms('inbox-claim', DEFAULT_BRANDING, {
|
|
58
|
+
claimUrl: 'https://example.com',
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(body).toContain('Unnamed Credential');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// contact-method-verification SMS
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
describe('renderSms — contact-method-verification', () => {
|
|
70
|
+
it('renders a verification SMS with the token', () => {
|
|
71
|
+
const body = renderSms('contact-method-verification', DEFAULT_BRANDING, {
|
|
72
|
+
verificationToken: '123456',
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(body).toBeDefined();
|
|
76
|
+
expect(body).toContain('123456');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('includes the brand name from default branding', () => {
|
|
80
|
+
const body = renderSms('contact-method-verification', DEFAULT_BRANDING, {
|
|
81
|
+
verificationToken: '000000',
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
expect(body).toContain('LearnCard');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('uses custom brand name with VetPass branding', () => {
|
|
88
|
+
const body = renderSms('contact-method-verification', VETPASS_BRANDING, {
|
|
89
|
+
verificationToken: '999999',
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(body).toContain('VetPass');
|
|
93
|
+
expect(body).not.toContain('LearnCard');
|
|
94
|
+
});
|
|
95
|
+
});
|