/** * JavaScript tests for safe-fetch.js * Run with: npm test tests/infrastructure_tests/test_safe_fetch.test.js * * Tests the safeFetch function which wraps fetch() with URL validation. */ // Mock fetch global.fetch = jest.fn(() => Promise.resolve({ ok: true })); // Mock SafeLogger (used by url-validator.js) global.SafeLogger = { warn: jest.fn(), error: jest.fn(), info: jest.fn(), debug: jest.fn() }; // Mock window.location for URL parsing global.window = { location: { href: 'http://localhost:5000/', origin: 'http://localhost:5000' } }; // Load the URL validator first (dependency) and expose as global const URLValidator = require('../../src/local_deep_research/web/static/js/security/url-validator.js'); global.URLValidator = URLValidator; // Load safe-fetch module and expose as global const { safeFetch } = require('../../src/local_deep_research/web/static/js/security/safe-fetch.js'); global.safeFetch = safeFetch; describe('safeFetch', () => { beforeEach(() => { jest.clearAllMocks(); global.fetch.mockClear(); }); describe('Internal URLs (starting with /)', () => { test('should allow internal API URLs without validation', async () => { await safeFetch('/api/settings'); expect(global.fetch).toHaveBeenCalledWith('/api/settings', {}); }); test('should allow internal page URLs', async () => { await safeFetch('/history'); expect(global.fetch).toHaveBeenCalledWith('/history', {}); }); test('should pass options through for internal URLs', async () => { const options = { method: 'POST', body: JSON.stringify({ key: 'value' }) }; await safeFetch('/api/data', options); expect(global.fetch).toHaveBeenCalledWith('/api/data', options); }); test('should allow deep internal paths', async () => { await safeFetch('/api/v1/research/123/status'); expect(global.fetch).toHaveBeenCalledWith('/api/v1/research/123/status', {}); }); }); describe('External URLs with safe schemes', () => { test('should allow https URLs', async () => { await safeFetch('https://example.com/api'); expect(global.fetch).toHaveBeenCalledWith('https://example.com/api', {}); }); test('should allow http URLs', async () => { await safeFetch('http://example.com/api'); expect(global.fetch).toHaveBeenCalledWith('http://example.com/api', {}); }); }); describe('Unsafe URLs', () => { test('should block javascript: URLs', async () => { await expect(safeFetch('javascript:alert(1)')).rejects.toThrow('Blocked unsafe URL'); expect(global.fetch).not.toHaveBeenCalled(); }); test('should block data: URLs', async () => { await expect(safeFetch('data:text/html,')).rejects.toThrow('Blocked unsafe URL'); expect(global.fetch).not.toHaveBeenCalled(); }); test('should block vbscript: URLs', async () => { await expect(safeFetch('vbscript:msgbox(1)')).rejects.toThrow('Blocked unsafe URL'); expect(global.fetch).not.toHaveBeenCalled(); }); test('should block javascript: with mixed case', async () => { await expect(safeFetch('JavaScript:alert(1)')).rejects.toThrow('Blocked unsafe URL'); expect(global.fetch).not.toHaveBeenCalled(); }); test('should block javascript: with leading whitespace', async () => { await expect(safeFetch(' javascript:alert(1)')).rejects.toThrow('Blocked unsafe URL'); expect(global.fetch).not.toHaveBeenCalled(); }); }); describe('Edge cases', () => { test('should handle empty options object', async () => { await safeFetch('/api/test', {}); expect(global.fetch).toHaveBeenCalledWith('/api/test', {}); }); test('should handle URLs with query parameters', async () => { await safeFetch('/api/search?q=test&limit=10'); expect(global.fetch).toHaveBeenCalledWith('/api/search?q=test&limit=10', {}); }); test('should handle URLs with fragments', async () => { await safeFetch('/page#section'); expect(global.fetch).toHaveBeenCalledWith('/page#section', {}); }); }); }); describe('URLValidator integration', () => { test('URLValidator should be available globally', () => { expect(global.URLValidator).toBeDefined(); expect(typeof global.URLValidator.isSafeUrl).toBe('function'); }); test('URLValidator.isSafeUrl should validate https URLs', () => { expect(global.URLValidator.isSafeUrl('https://example.com')).toBe(true); }); test('URLValidator.isSafeUrl should reject javascript URLs', () => { expect(global.URLValidator.isSafeUrl('javascript:alert(1)')).toBe(false); }); test('URLValidator.isUnsafeScheme should detect unsafe schemes', () => { expect(global.URLValidator.isUnsafeScheme('javascript:void(0)')).toBe(true); expect(global.URLValidator.isUnsafeScheme('data:text/html,test')).toBe(true); expect(global.URLValidator.isUnsafeScheme('https://example.com')).toBe(false); }); });