// Security tests exercise DOMPurify, which caches the Node.prototype
// nodeName getter and relies on it returning the tag name for elements.
// happy-dom's base getter returns "" for elements, so DOMPurify there
// strips EVERY tag — run under jsdom, the environment DOMPurify is
// developed and tested against.
// @vitest-environment jsdom
import { describe, it, expect } from 'vitest'
import { renderMarkdown, attachImageLightbox } from '@/composables/useMarkdown.js'
describe('renderMarkdown sanitization', () => {
it('renders plain markdown unchanged', () => {
const html = renderMarkdown('# Hi\n\n**bold** and a list:\n\n- one\n- two')
expect(html).toContain('<h1>Hi</h1>')
expect(html).toContain('<strong>bold</strong>')
expect(html).toContain('<li>one</li>')
})
it('strips script tags', () => {
const html = renderMarkdown('<script>alert(1)</script>ok')
expect(html).not.toContain('<script')
expect(html).toContain('ok')
})
it('strips inline event handlers', () => {
const html = renderMarkdown('<img src=x onerror="alert(1)">')
expect(html).not.toContain('onerror')
})
it('neutralizes javascript: hrefs', () => {
const html = renderMarkdown('[click](javascript:alert(1))')
expect(html).not.toContain('javascript:')
})
it('blocks data:text/html but keeps data:image', () => {
const evil = renderMarkdown('[x](data:text/html;base64,PHNjcmlwdD4=)')
expect(evil).not.toContain('data:text/html')
const img = renderMarkdown('')
expect(img).toContain('data:image/png;base64,aGVsbG8=')
})
it('preserves code block structure: copy button, data-code, hljs classes', () => {
const html = renderMarkdown('```python\nprint("hi")\n```')
expect(html).toContain('code-block')
expect(html).toContain('copy-btn')
expect(html).toContain('data-code="')
expect(html).toContain('hljs language-python')
expect(html).toContain('<button')
})
it('preserves table wrap and task-list checkboxes', () => {
const html = renderMarkdown('| a | b |\n| --- | --- |\n| 1 | 2 |\n\n- [x] done')
expect(html).toContain('<div class="table-wrap"><table')
expect(html).toContain('<input')
expect(html).toContain('type="checkbox"')
})
it('image renderer whitelists URL schemes', () => {
const evil = renderMarkdown(')')
expect(evil).not.toContain('javascript:')
expect(evil).toContain('is-broken')
const proto = renderMarkdown('')
expect(proto).toContain('src="https://example.com/x.png"')
const rel = renderMarkdown('')
expect(rel).toContain('src="/static/x.png"')
const schemeless = renderMarkdown('')
expect(schemeless).not.toContain('//evil.com')
})
it('rendered images carry no inline onerror attribute', () => {
const html = renderMarkdown('')
expect(html).not.toContain('onerror=')
})
})
describe('attachImageLightbox', () => {
it('marks broken images via the error event', () => {
const el = document.createElement('div')
el.innerHTML = renderMarkdown('')
attachImageLightbox(el)
const img = el.querySelector('img.msg-md-image')
const link = el.querySelector('a.msg-md-image-link')
expect(img).toBeTruthy()
img.dispatchEvent(new Event('error'))
expect(img.classList.contains('is-broken')).toBe(true)
expect(link.classList.contains('is-broken')).toBe(true)
})
})