import { sanitizeInlineHtml, sanitizeRichTextHtml, escapeHtml, } from './sanitize'; import { describe, it, expect } from 'vitest'; describe('escapeHtml', () => { it('escapes the five metacharacters', () => { expect(escapeHtml(`a & b < c > d " e ' f`)).toBe( 'a & b < c > d " e ' f', ); }); it('escapes ampersands first (no double-escape of entities)', () => { expect(escapeHtml('<')).toBe('&lt;'); }); it('returns empty string for empty input', () => { expect(escapeHtml('')).toBe(''); }); it('leaves plain ASCII text untouched', () => { expect(escapeHtml('Paris Adventure 2026')).toBe('Paris Adventure 2026'); }); it('neutralises a script tag without sanitising', () => { expect(escapeHtml('')).toBe( '<script>alert(1)</script>', ); }); }); describe('sanitizeInlineHtml', () => { it('returns empty string for empty input', () => { expect(sanitizeInlineHtml('')).toBe(''); }); it('preserves the allowed inline tags', () => { expect(sanitizeInlineHtml('a b c')).toBe( 'a b c', ); expect(sanitizeInlineHtml('x')).toBe('x'); }); it('strips text'); expect(out).not.toContain(' (no img tag in inline allow-list)', () => { expect(sanitizeInlineHtml('')).toBe(''); }); it('strips on* event handlers from preserved tags', () => { const out = sanitizeInlineHtml('hi'); expect(out).not.toContain('onclick'); expect(out).toContain('hi'); }); it('strips style attribute (CSS-injection surface)', () => { const out = sanitizeInlineHtml( 'x', ); expect(out).not.toContain('style='); expect(out).not.toContain('javascript:'); }); it('strips iframe / object / embed / svg-with-script', () => { expect(sanitizeInlineHtml('')).toBe(''); expect(sanitizeInlineHtml('')).toBe(''); expect(sanitizeInlineHtml('')).toBe(''); expect( sanitizeInlineHtml(''), ).not.toContain('script'); }); it('does not preserve href / target on the inline tag set', () => { // is not in the inline allow-list, so href can never appear here. const out = sanitizeInlineHtml('x'); expect(out).toBe('x'); }); it('keeps user text content when the wrapping tag is stripped', () => { expect(sanitizeInlineHtml('hello')).toBe('hello'); }); }); describe('sanitizeRichTextHtml', () => { it('preserves the full prose tag set', () => { const html = '

hello world

'; const out = sanitizeRichTextHtml(html); expect(out).toContain('

'); expect(out).toContain('world'); expect(out).toContain('

  • one
  • '); }); it('still strips ', ); expect(out).not.toContain('onclick'); expect(out).not.toContain('style='); expect(out).not.toContain(' { const out = sanitizeRichTextHtml('x'); expect(out).not.toContain('javascript:'); }); it('blocks data: hrefs that smuggle scripts', () => { const out = sanitizeRichTextHtml( 'x', ); expect(out).not.toContain('data:text/html'); }); it('keeps http(s) hrefs intact', () => { const out = sanitizeRichTextHtml('link'); expect(out).toContain('href="https://example.com"'); }); it('strips disallowed tags but keeps their content', () => { expect( sanitizeRichTextHtml('

    beforemiddleafter

    '), ).toContain('middle'); }); it('drops mathml + svg shorthand vectors', () => { const mathPayload = ''; const out = sanitizeRichTextHtml(mathPayload); expect(out).not.toContain('