ghostguild-org/tests/client/composables/useMarkdown.test.js
Jennie Robinson Faber 29c96a207e Add Vitest security test suite and update security evaluation doc
Set up Vitest with server (node) and client (jsdom) test projects.
79 tests across 8 files verify all Phase 0-1 security controls:
escapeHtml sanitization, DOMPurify markdown XSS prevention, CSRF
enforcement, security headers, rate limiting, auth guards, profile
field allowlist, and login anti-enumeration. Updated SECURITY_EVALUATION.md
with remediation status, implementation summary, and automated test
coverage details.
2026-03-01 12:30:06 +00:00

110 lines
3.4 KiB
JavaScript

import { describe, it, expect } from 'vitest'
import { useMarkdown } from '../../../app/composables/useMarkdown.js'
describe('useMarkdown', () => {
const { render } = useMarkdown()
describe('XSS prevention', () => {
it('strips script tags', () => {
const result = render('Hello <script>alert("xss")</script> world')
expect(result).not.toContain('<script>')
expect(result).not.toContain('</script>')
expect(result).toContain('Hello')
expect(result).toContain('world')
})
it('strips onerror attributes', () => {
const result = render('<img onerror="alert(1)" src="x">')
expect(result).not.toContain('onerror')
})
it('strips onclick attributes', () => {
const result = render('<a onclick="alert(1)" href="#">click</a>')
expect(result).not.toContain('onclick')
})
it('strips iframe tags', () => {
const result = render('<iframe src="https://evil.com"></iframe>')
expect(result).not.toContain('<iframe')
})
it('strips object tags', () => {
const result = render('<object data="exploit.swf"></object>')
expect(result).not.toContain('<object')
})
it('strips embed tags', () => {
const result = render('<embed src="exploit.swf">')
expect(result).not.toContain('<embed')
})
it('sanitizes javascript: URIs', () => {
const result = render('[click me](javascript:alert(1))')
expect(result).not.toContain('javascript:')
})
it('strips img tags (not in allowed list)', () => {
const result = render('![alt](https://example.com/img.png)')
expect(result).not.toContain('<img')
})
})
describe('preserves safe markdown', () => {
it('renders bold and italic', () => {
const result = render('**bold** and *italic*')
expect(result).toContain('<strong>bold</strong>')
expect(result).toContain('<em>italic</em>')
})
it('renders links with href', () => {
const result = render('[Ghost Guild](https://ghostguild.org)')
expect(result).toContain('<a')
expect(result).toContain('href="https://ghostguild.org"')
})
it('preserves headings h1-h6', () => {
for (let i = 1; i <= 6; i++) {
const hashes = '#'.repeat(i)
const result = render(`${hashes} Heading ${i}`)
expect(result).toContain(`<h${i}>`)
}
})
it('preserves code blocks', () => {
const result = render('`inline code` and\n\n```\nblock code\n```')
expect(result).toContain('<code>')
expect(result).toContain('<pre>')
})
it('preserves blockquotes', () => {
const result = render('> This is a quote')
expect(result).toContain('<blockquote>')
})
it('preserves lists', () => {
const result = render('- item 1\n- item 2')
expect(result).toContain('<ul>')
expect(result).toContain('<li>')
})
it('preserves allowed attributes: href, target, rel, class', () => {
// DOMPurify allows href on <a> tags
const result = render('[link](https://example.com)')
expect(result).toContain('href=')
})
})
describe('edge cases', () => {
it('returns empty string for null', () => {
expect(render(null)).toBe('')
})
it('returns empty string for undefined', () => {
expect(render(undefined)).toBe('')
})
it('returns empty string for empty string', () => {
expect(render('')).toBe('')
})
})
})