/** * JavaScript tests for XSS Protection and Dropdown Highlighting * Run with: npm test tests/infrastructure_tests/test_xss_protection.test.js * * Tests the safeSetInnerHTML function and dropdown search highlighting * which requires DOMPurify to be loaded. */ // Mock DOMPurify for testing const mockDOMPurify = { sanitize: jest.fn((content) => { // Simple mock that allows span tags with class attribute // but removes script tags let result = content; // Remove script tags result = result.replace(/'); expect(element.textContent).toBe(''); }); test('should handle null element gracefully', () => { const { safeSetTextContent } = global.window; expect(() => safeSetTextContent(null, 'content')).not.toThrow(); }); }); describe('sanitizeHtml', () => { test('should use DOMPurify to sanitize HTML', () => { const { sanitizeHtml } = global.window; const dirty = 'safe'; const result = sanitizeHtml(dirty); expect(mockDOMPurify.sanitize).toHaveBeenCalledWith( dirty, expect.any(Object) ); }); test('should return empty string for empty input', () => { const { sanitizeHtml } = global.window; expect(sanitizeHtml('')).toBe(''); expect(sanitizeHtml(null)).toBe(''); expect(sanitizeHtml(undefined)).toBe(''); }); }); }); describe('Dropdown Highlight Rendering', () => { describe('Highlight HTML Structure', () => { test('highlight span should have correct class', () => { const highlightHtml = 'search'; expect(highlightHtml).toContain('class="ldr-highlight"'); }); test('highlight pattern should match search terms', () => { // Simulate the highlightText function from custom_dropdown.js (indexOf implementation) const showAllOptions = false; function highlightText(text, search) { if (!search.trim() || showAllOptions) return text; const lowerText = text.toLowerCase(); const lowerSearch = search.toLowerCase(); let result = ''; let lastIdx = 0; let idx; while ((idx = lowerText.indexOf(lowerSearch, lastIdx)) !== -1) { result += text.substring(lastIdx, idx); result += '' + text.substring(idx, idx + search.length) + ''; lastIdx = idx + search.length; } result += text.substring(lastIdx); return result; } const result = highlightText('bytedance-seed/seed-1.6-flash-h', 'flas'); expect(result).toBe('bytedance-seed/seed-1.6-flash-h'); }); test('highlight should be case-insensitive', () => { const showAllOptions = false; function highlightText(text, search) { if (!search.trim() || showAllOptions) return text; const lowerText = text.toLowerCase(); const lowerSearch = search.toLowerCase(); let result = ''; let lastIdx = 0; let idx; while ((idx = lowerText.indexOf(lowerSearch, lastIdx)) !== -1) { result += text.substring(lastIdx, idx); result += '' + text.substring(idx, idx + search.length) + ''; lastIdx = idx + search.length; } result += text.substring(lastIdx); return result; } const result = highlightText('FLASH Model', 'flash'); expect(result).toContain('ldr-highlight'); expect(result).toContain('FLASH'); }); test('highlight should handle multiple matches', () => { const showAllOptions = false; function highlightText(text, search) { if (!search.trim() || showAllOptions) return text; const lowerText = text.toLowerCase(); const lowerSearch = search.toLowerCase(); let result = ''; let lastIdx = 0; let idx; while ((idx = lowerText.indexOf(lowerSearch, lastIdx)) !== -1) { result += text.substring(lastIdx, idx); result += '' + text.substring(idx, idx + search.length) + ''; lastIdx = idx + search.length; } result += text.substring(lastIdx); return result; } const result = highlightText('test test test', 'test'); const matches = result.match(/ldr-highlight/g); expect(matches).toHaveLength(3); }); test('highlight should handle special characters in search without error', () => { const showAllOptions = false; function highlightText(text, search) { if (!search.trim() || showAllOptions) return text; const lowerText = text.toLowerCase(); const lowerSearch = search.toLowerCase(); let result = ''; let lastIdx = 0; let idx; while ((idx = lowerText.indexOf(lowerSearch, lastIdx)) !== -1) { result += text.substring(lastIdx, idx); result += '' + text.substring(idx, idx + search.length) + ''; lastIdx = idx + search.length; } result += text.substring(lastIdx); return result; } // indexOf naturally handles special characters — no regex to break expect(() => highlightText('test (parentheses)', '(paren')).not.toThrow(); expect(() => highlightText('test [brackets]', '[brack')).not.toThrow(); expect(() => highlightText('test.dot', '.')).not.toThrow(); }); }); describe('DOMPurify Sanitization Config', () => { test('should allow span tags', () => { const { sanitizeHtml } = global.window; const input = 'text'; sanitizeHtml(input); // Verify DOMPurify was called with config that allows span expect(mockDOMPurify.sanitize).toHaveBeenCalledWith( input, expect.objectContaining({ ALLOWED_TAGS: expect.arrayContaining(['span']) }) ); }); test('should allow class attribute', () => { const { sanitizeHtml } = global.window; const input = 'text'; sanitizeHtml(input); expect(mockDOMPurify.sanitize).toHaveBeenCalledWith( input, expect.objectContaining({ ALLOWED_ATTR: expect.arrayContaining(['class']) }) ); }); test('should forbid script tags', () => { const { sanitizeHtml } = global.window; const input = ''; sanitizeHtml(input); expect(mockDOMPurify.sanitize).toHaveBeenCalledWith( input, expect.objectContaining({ FORBID_TAGS: expect.arrayContaining(['script']) }) ); }); test('should forbid event handlers', () => { const { sanitizeHtml } = global.window; const input = '