/** * HTMLSanitizer 模块单元测试 * 覆盖 XSS 防护、HTML 转义、URL 安全验证等核心功能 */ import HTMLSanitizer from '../../src/js/modules/html-sanitizer'; describe('HTMLSanitizer', () => { describe('escapeHtml()', () => { it('应该转义所有 HTML 特殊字符', () => { const input = ''; const result = HTMLSanitizer.escapeHtml(input); expect(result).toBe('<script>alert("xss")</script>'); }); it('应该转义 & 符号', () => { expect(HTMLSanitizer.escapeHtml('a&b')).toBe('a&b'); }); it('应该转义引号', () => { expect(HTMLSanitizer.escapeHtml('"test"')).toBe('"test"'); expect(HTMLSanitizer.escapeHtml("'test'")).toBe(''test''); }); it('应该转义斜杠', () => { expect(HTMLSanitizer.escapeHtml('/path')).toBe('/path'); }); it('应该处理 null 和 undefined', () => { expect(HTMLSanitizer.escapeHtml(null)).toBe(''); expect(HTMLSanitizer.escapeHtml(undefined)).toBe(''); }); it('应该处理数字类型', () => { expect(HTMLSanitizer.escapeHtml(123)).toBe('123'); }); it('应该处理空字符串', () => { expect(HTMLSanitizer.escapeHtml('')).toBe(''); }); }); describe('escapeAttr()', () => { it('应该转义属性值中的引号', () => { expect(HTMLSanitizer.escapeAttr('value"onclick="alert(1)')).toBe('value"onclick="alert(1)'); }); it('应该转义单引号', () => { expect(HTMLSanitizer.escapeAttr("it's")).toBe('it's'); }); it('应该处理 null 和 undefined', () => { expect(HTMLSanitizer.escapeAttr(null)).toBe(''); expect(HTMLSanitizer.escapeAttr(undefined)).toBe(''); }); it('应该转义尖括号', () => { expect(HTMLSanitizer.escapeAttr('')).toBe('<img>'); }); }); describe('sanitizeURL()', () => { it('应该阻止 javascript: 协议', () => { expect(HTMLSanitizer.sanitizeURL('javascript:alert(1)')).toBe('#'); }); it('应该阻止 vbscript: 协议', () => { expect(HTMLSanitizer.sanitizeURL('vbscript:msgbox(1)')).toBe('#'); }); it('应该阻止 data:text/html 协议', () => { // 使用不含尖括号的字符串避免 jsdom 解析干扰 const result = HTMLSanitizer.sanitizeURL('data:text/html,hello'); expect(result).toBe('#'); }); it('应该允许 http: 协议', () => { expect(HTMLSanitizer.sanitizeURL('http://example.com')).toBe('http://example.com'); }); it('应该允许 https: 协议', () => { expect(HTMLSanitizer.sanitizeURL('https://example.com')).toBe('https://example.com'); }); it('应该处理空值', () => { expect(HTMLSanitizer.sanitizeURL('')).toBe(''); expect(HTMLSanitizer.sanitizeURL(null)).toBe(''); }); }); describe('isSafeURL()', () => { it('应该允许 http 协议', () => { expect(HTMLSanitizer.isSafeURL('http://example.com')).toBe(true); }); it('应该允许 https 协议', () => { expect(HTMLSanitizer.isSafeURL('https://example.com')).toBe(true); }); it('应该阻止 javascript: 协议', () => { expect(HTMLSanitizer.isSafeURL('javascript:alert(1)')).toBe(false); }); it('应该阻止 file: 协议', () => { expect(HTMLSanitizer.isSafeURL('file:///etc/passwd')).toBe(false); }); it('应该处理空值', () => { expect(HTMLSanitizer.isSafeURL('')).toBe(false); expect(HTMLSanitizer.isSafeURL(null)).toBe(false); }); }); describe('setInnerHTML()', () => { it('应该安全地设置 innerHTML', () => { const element = document.createElement('div'); HTMLSanitizer.setInnerHTML(element, 'bold', ['b']); expect(element.innerHTML).toBe('bold'); }); it('应该移除不在白名单中的标签并保留文本', () => { const element = document.createElement('div'); HTMLSanitizer.setInnerHTML(element, 'safe', ['b']); // 脚本标签应被替换为文本节点,安全标签保留 expect(element.textContent).toContain('bad'); expect(element.textContent).toContain('safe'); expect(element.querySelector('b')).not.toBeNull(); }); it('应该处理 null 元素', () => { expect(() => { HTMLSanitizer.setInnerHTML(null, 'test'); }).not.toThrow(); }); }); describe('createElement()', () => { it('应该创建带安全属性的元素', () => { const el = HTMLSanitizer.createElement('div', { id: 'test', class: 'foo' }, 'hello'); expect(el.tagName).toBe('DIV'); expect(el.id).toBe('test'); expect(el.textContent).toBe('hello'); }); it('应该阻止事件处理器属性', () => { const warnSpy = jest.spyOn(console, 'warn').mockImplementation(); const el = HTMLSanitizer.createElement('div', { onclick: 'alert(1)' }); expect(el.hasAttribute('onclick')).toBe(false); expect(warnSpy).toHaveBeenCalled(); warnSpy.mockRestore(); }); it('应该支持子元素数组', () => { const child = document.createElement('span'); const el = HTMLSanitizer.createElement('div', {}, [child, 'text']); expect(el.children.length).toBe(1); expect(el.childNodes.length).toBe(2); }); }); });