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('<');
});
// 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('