270 lines
8.3 KiB
TypeScript
270 lines
8.3 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { containsHTML, wrapHTMLInCodeBlock } from '../utils/htmlSecurity';
|
|
|
|
describe('HTML Security Detection', () => {
|
|
describe('containsHTML', () => {
|
|
describe('should detect dangerous HTML tags', () => {
|
|
it('detects script tags', () => {
|
|
expect(containsHTML('<script>alert("xss")</script>')).toBe(true);
|
|
expect(containsHTML('<script src="evil.js"></script>')).toBe(true);
|
|
expect(containsHTML('<script>')).toBe(true);
|
|
});
|
|
|
|
it('detects style tags', () => {
|
|
expect(containsHTML('<style>body { display: none; }</style>')).toBe(true);
|
|
expect(containsHTML('<style>')).toBe(true);
|
|
});
|
|
|
|
it('detects iframe tags', () => {
|
|
expect(containsHTML('<iframe src="evil.com"></iframe>')).toBe(true);
|
|
expect(containsHTML('<iframe>')).toBe(true);
|
|
});
|
|
|
|
it('detects form elements', () => {
|
|
expect(containsHTML('<form action="/submit"></form>')).toBe(true);
|
|
expect(containsHTML('<input type="text" name="password">')).toBe(true);
|
|
expect(containsHTML('<button onclick="evil()">Click</button>')).toBe(true);
|
|
});
|
|
|
|
it('detects layout-affecting tags', () => {
|
|
expect(containsHTML('<div class="container">content</div>')).toBe(true);
|
|
expect(containsHTML('<span style="color:red">text</span>')).toBe(true);
|
|
expect(containsHTML('<br/>')).toBe(true);
|
|
expect(containsHTML('<hr>')).toBe(true);
|
|
expect(containsHTML('<img src="image.jpg" alt="test">')).toBe(true);
|
|
});
|
|
|
|
it('detects HTML comments', () => {
|
|
expect(containsHTML('<!-- this is a comment -->')).toBe(true);
|
|
expect(containsHTML('<!-- multi\nline\ncomment -->')).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('should NOT detect safe content', () => {
|
|
it('ignores auto-links', () => {
|
|
expect(containsHTML('<https://example.com>')).toBe(false);
|
|
expect(containsHTML('<http://test.org>')).toBe(false);
|
|
expect(containsHTML('<https://block.dev/docs>')).toBe(false);
|
|
});
|
|
|
|
it('ignores email addresses', () => {
|
|
expect(containsHTML('<user@example.com>')).toBe(false);
|
|
expect(containsHTML('<admin@block.dev>')).toBe(false);
|
|
expect(containsHTML('<test.email+tag@domain.co.uk>')).toBe(false);
|
|
});
|
|
|
|
it('ignores TypeScript generics and placeholders', () => {
|
|
expect(containsHTML('Array<T>')).toBe(false);
|
|
expect(containsHTML('Promise<string>')).toBe(false);
|
|
expect(containsHTML('<project-root>')).toBe(false);
|
|
expect(containsHTML('<filename>')).toBe(false);
|
|
expect(containsHTML('<<not a tag>>')).toBe(false);
|
|
});
|
|
|
|
it('ignores content already in code blocks', () => {
|
|
expect(containsHTML('```html\n<div>safe</div>\n```')).toBe(false);
|
|
expect(containsHTML('`<script>safe</script>`')).toBe(false);
|
|
expect(containsHTML('Here is `<br/>` in inline code')).toBe(false);
|
|
});
|
|
|
|
it('ignores plain text', () => {
|
|
expect(containsHTML('This is just plain text')).toBe(false);
|
|
expect(containsHTML('No HTML here!')).toBe(false);
|
|
expect(containsHTML('')).toBe(false);
|
|
});
|
|
|
|
it('ignores mathematical expressions', () => {
|
|
expect(containsHTML('x < y && y > z')).toBe(false);
|
|
expect(containsHTML('if (a < b && c > d)')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('edge cases', () => {
|
|
it('handles mixed content correctly', () => {
|
|
// Real HTML mixed with safe content
|
|
expect(containsHTML('Visit <https://example.com> and <div>click here</div>')).toBe(true);
|
|
|
|
// Only safe content
|
|
expect(containsHTML('Email <user@test.com> about <project-root> setup')).toBe(false);
|
|
});
|
|
|
|
it('handles malformed HTML', () => {
|
|
expect(containsHTML('<div unclosed')).toBe(false); // This doesn't match our regex pattern
|
|
expect(containsHTML('<>')).toBe(false);
|
|
expect(containsHTML('< div >')).toBe(false);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('wrapHTMLInCodeBlock', () => {
|
|
describe('should wrap dangerous HTML', () => {
|
|
it('wraps single line HTML', () => {
|
|
const input = '<script>alert("xss")</script>';
|
|
const expected = '```html\n<script>alert("xss")</script>\n```';
|
|
expect(wrapHTMLInCodeBlock(input)).toBe(expected);
|
|
});
|
|
|
|
it('wraps HTML comments', () => {
|
|
const input = '<!-- malicious comment -->';
|
|
const expected = '```html\n<!-- malicious comment -->\n```';
|
|
expect(wrapHTMLInCodeBlock(input)).toBe(expected);
|
|
});
|
|
|
|
it('wraps mixed content selectively', () => {
|
|
const input = `Normal text
|
|
<div>This should be wrapped</div>
|
|
More normal text`;
|
|
|
|
const expected = `Normal text
|
|
\`\`\`html
|
|
<div>This should be wrapped</div>
|
|
\`\`\`
|
|
More normal text`;
|
|
|
|
expect(wrapHTMLInCodeBlock(input)).toBe(expected);
|
|
});
|
|
});
|
|
|
|
describe('should preserve safe content', () => {
|
|
it('preserves auto-links', () => {
|
|
const input = 'Visit <https://example.com> for more info';
|
|
expect(wrapHTMLInCodeBlock(input)).toBe(input);
|
|
});
|
|
|
|
it('preserves email addresses', () => {
|
|
const input = 'Contact <admin@example.com> for help';
|
|
expect(wrapHTMLInCodeBlock(input)).toBe(input);
|
|
});
|
|
|
|
it('preserves TypeScript generics', () => {
|
|
const input = 'const arr: Array<string> = []';
|
|
expect(wrapHTMLInCodeBlock(input)).toBe(input);
|
|
});
|
|
|
|
it('preserves existing code blocks', () => {
|
|
const input = `# Title
|
|
|
|
\`\`\`javascript
|
|
const x = "<div>this is safe</div>";
|
|
\`\`\`
|
|
|
|
Normal text`;
|
|
|
|
expect(wrapHTMLInCodeBlock(input)).toBe(input);
|
|
});
|
|
|
|
it('preserves inline code', () => {
|
|
const input = 'Use `<br/>` for line breaks';
|
|
expect(wrapHTMLInCodeBlock(input)).toBe(input);
|
|
});
|
|
});
|
|
|
|
describe('complex scenarios', () => {
|
|
it('handles multiple HTML lines correctly', () => {
|
|
const input = `# Test Message
|
|
|
|
Normal paragraph
|
|
|
|
<div>First HTML line</div>
|
|
<span>Second HTML line</span>
|
|
|
|
More normal text`;
|
|
|
|
const expected = `# Test Message
|
|
|
|
Normal paragraph
|
|
|
|
\`\`\`html
|
|
<div>First HTML line</div>
|
|
\`\`\`
|
|
\`\`\`html
|
|
<span>Second HTML line</span>
|
|
\`\`\`
|
|
|
|
More normal text`;
|
|
|
|
expect(wrapHTMLInCodeBlock(input)).toBe(expected);
|
|
});
|
|
|
|
it('respects existing code block boundaries', () => {
|
|
const input = `Before code block
|
|
|
|
\`\`\`html
|
|
<div>This is already safe</div>
|
|
<script>This is also safe in here</script>
|
|
\`\`\`
|
|
|
|
<div>This should be wrapped</div>`;
|
|
|
|
const expected = `Before code block
|
|
|
|
\`\`\`html
|
|
<div>This is already safe</div>
|
|
<script>This is also safe in here</script>
|
|
\`\`\`
|
|
|
|
\`\`\`html
|
|
<div>This should be wrapped</div>
|
|
\`\`\``;
|
|
|
|
expect(wrapHTMLInCodeBlock(input)).toBe(expected);
|
|
});
|
|
|
|
it('handles the test suite scenarios', () => {
|
|
// Test Message 1: One-liners
|
|
const test1 = `<https://example.com>
|
|
<user@example.com>
|
|
\`<T>\``;
|
|
expect(wrapHTMLInCodeBlock(test1)).toBe(test1);
|
|
|
|
// Test Message 2: Mixed content - our function wraps the entire line, not just the HTML part
|
|
const test2 = `Here's a link <https://example.com> and HTML <div>content</div>`;
|
|
const expected2 = `\`\`\`html
|
|
Here's a link <https://example.com> and HTML <div>content</div>
|
|
\`\`\``;
|
|
expect(wrapHTMLInCodeBlock(test2)).toBe(expected2);
|
|
|
|
// Test Message 7: Comment-only
|
|
const test7 = `<!-- top-level html comment -->`;
|
|
const expected7 = `\`\`\`html
|
|
<!-- top-level html comment -->
|
|
\`\`\``;
|
|
expect(wrapHTMLInCodeBlock(test7)).toBe(expected7);
|
|
});
|
|
});
|
|
|
|
describe('edge cases', () => {
|
|
it('handles empty input', () => {
|
|
expect(wrapHTMLInCodeBlock('')).toBe('');
|
|
});
|
|
|
|
it('handles only whitespace', () => {
|
|
const input = ' \n \n ';
|
|
expect(wrapHTMLInCodeBlock(input)).toBe(input);
|
|
});
|
|
|
|
it('handles nested code block scenarios', () => {
|
|
const input = `\`\`\`
|
|
<div>safe in code block</div>
|
|
\`\`\`
|
|
<div>unsafe outside</div>
|
|
\`\`\`
|
|
<span>also safe in code block</span>
|
|
\`\`\``;
|
|
|
|
const expected = `\`\`\`
|
|
<div>safe in code block</div>
|
|
\`\`\`
|
|
\`\`\`html
|
|
<div>unsafe outside</div>
|
|
\`\`\`
|
|
\`\`\`
|
|
<span>also safe in code block</span>
|
|
\`\`\``;
|
|
|
|
expect(wrapHTMLInCodeBlock(input)).toBe(expected);
|
|
});
|
|
});
|
|
});
|
|
});
|