import { describe, expect, it } from 'vitest'; import { EMAIL_COLORS, brandedEmailTemplate, emailButton, emailHeading, emailHighlight, emailRow, escapeAttr, escapeHtml, rawEmailHtml, } from '@/lib/email-brand'; describe('escapeHtml', () => { it.each([ ['&', '&'], ['<', '<'], ['>', '>'], ['"', '"'], ])('escapes %s as %s', (input, expected) => { expect(escapeHtml(input)).toBe(expected); }); it('neutralises a script tag', () => { expect(escapeHtml('')).toBe( '<script>alert("xss")</script>' ); }); it('escapes the ampersand first so an existing entity is not double-decoded', () => { expect(escapeHtml('<')).toBe('&lt;'); }); // Single-quoted attributes exist in the templates, so leaving the quote alone left a // value able to close one. it('escapes the single quote', () => { expect(escapeHtml("it's")).toBe('it's'); }); it('leaves plain text untouched', () => { expect(escapeHtml('Alice reviewed your video')).toBe('Alice reviewed your video'); }); it('returns an empty string unchanged', () => { expect(escapeHtml('')).toBe(''); }); }); describe('escapeAttr', () => { it('escapes the same four characters as escapeHtml', () => { expect(escapeAttr('&<>"')).toBe('&<>"'); }); it('breaks an attribute injection attempt', () => { const escaped = escapeAttr('https://x.com" onmouseover="alert(1)'); expect(escaped).not.toContain('" onmouseover'); expect(escaped).toContain('" onmouseover="'); }); it('agrees with escapeHtml on every input despite the different replacement order', () => { for (const input of ['&', '<', '>', '"', '<', 'a&bd"e']) { expect(escapeAttr(input)).toBe(escapeHtml(input)); } }); }); describe('brandedEmailTemplate', () => { it('produces a full HTML document carrying the brand colours', () => { const html = brandedEmailTemplate('Body'); expect(html.startsWith('')).toBe(true); expect(html.trimEnd().endsWith('')).toBe(true); expect(html).toContain(EMAIL_COLORS.bg); expect(html).toContain('OpenFrame'); }); it('inserts the body markup verbatim', () => { expect(brandedEmailTemplate('Hello & welcome')).toContain( 'Hello & welcome' ); }); it('omits the footer block when no footer options are given', () => { expect(brandedEmailTemplate('Body')).not.toContain( 'padding:20px 0 0;text-align:center' ); }); it('renders footer text on its own', () => { const html = brandedEmailTemplate('Body', { footerText: 'Sent by OpenFrame' }); expect(html).toContain('Sent by OpenFrame'); expect(html).not.toContain(' { const withTextOnly = brandedEmailTemplate('Body', { footerLinkText: 'Unsubscribe' }); const withBoth = brandedEmailTemplate('Body', { footerLinkText: 'Unsubscribe', footerLinkUrl: 'https://open-frame.net/settings', }); expect(withTextOnly).not.toContain('Unsubscribe'); expect(withBoth).toContain('href="https://open-frame.net/settings"'); expect(withBoth).toContain('>Unsubscribe<'); }); it('escapes the footer link url as an attribute', () => { const html = brandedEmailTemplate('Body', { footerLinkText: 'Unsubscribe', footerLinkUrl: 'https://x.com" onmouseover="alert(1)', }); expect(html).not.toContain('" onmouseover="alert(1)"'); expect(html).toContain('" onmouseover="alert(1)'); }); it('escapes the footer link text as HTML', () => { const html = brandedEmailTemplate('Body', { footerLinkText: '', footerLinkUrl: 'https://open-frame.net', }); expect(html).not.toContain(''); expect(html).toContain('<script>alert(1)</script>'); }); }); describe('email fragment builders', () => { it('emailHeading renders the icon and title in the accent colour', () => { const html = emailHeading('🎬', 'New comment'); expect(html).toContain('🎬'); expect(html).toContain('New comment'); expect(html).toContain(EMAIL_COLORS.accent); }); it('emailRow renders the label and value in a table row', () => { const html = emailRow('Project', 'Launch video'); expect(html.startsWith('')).toBe(true); expect(html).toContain('Project'); expect(html).toContain('Launch video'); }); it('emailRow switches to the highlight style when asked', () => { const plain = emailRow('Project', 'Launch video'); const highlighted = emailRow('Project', 'Launch video', true); expect(plain).toContain(EMAIL_COLORS.textSecondary); expect(highlighted).toContain('font-weight:600'); expect(highlighted).not.toContain(EMAIL_COLORS.textSecondary); }); it('emailButton escapes both the href and the label', () => { const html = emailButton('Open', 'https://x.com" onclick="alert(1)'); expect(html).toContain('" onclick="alert(1)'); expect(html).toContain('<b>Open</b>'); expect(html).not.toContain('Open'); }); // The escaping lives in the helpers rather than in every call site, so a project name // or a display name is safe whether or not the next caller remembers to escape it. it.each([ ['emailHeading title', () => emailHeading('*', '')], ['emailRow label', () => emailRow('', 'value')], ['emailRow value', () => emailRow('label', '')], ['emailHighlight text', () => emailHighlight('')], ['emailButton label', () => emailButton('', 'https://x.test')], ])('%s is escaped', (_label, build) => { const html = build(); expect(html).not.toContain('', }); expect(html).not.toContain('