Newer
Older
navi-1 / webclient / tests / unit / composables / useMarkdown.test.js
// 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('![pic](data:image/png;base64,aGVsbG8=)')
    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('![a](javascript:alert(1))')
    expect(evil).not.toContain('javascript:')
    expect(evil).toContain('is-broken')

    const proto = renderMarkdown('![a](https://example.com/x.png)')
    expect(proto).toContain('src="https://example.com/x.png"')

    const rel = renderMarkdown('![a](/static/x.png)')
    expect(rel).toContain('src="/static/x.png"')

    const schemeless = renderMarkdown('![a](//evil.com/x.png)')
    expect(schemeless).not.toContain('//evil.com')
  })

  it('rendered images carry no inline onerror attribute', () => {
    const html = renderMarkdown('![a](https://example.com/y.png)')
    expect(html).not.toContain('onerror=')
  })
})

describe('attachImageLightbox', () => {
  it('marks broken images via the error event', () => {
    const el = document.createElement('div')
    el.innerHTML = renderMarkdown('![a](https://example.com/broken.png)')
    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)
  })
})