const jestClearModules = () => { jest.resetModules(); jest.clearAllMocks(); }; /** Mock every external dependency that proxy-util pulls in so tests are isolated. */ const setupMocks = ({ pacDirectives = ['PROXY p.example:8080'] } = {}) => { // Preferences — controls SSL session cache flag jest.doMock('../src/store/preferences', () => ({ preferencesUtil: { isSslSessionCachingEnabled: () => false } })); // @usebruno/requests — agent factories + pac resolver + shared resolveAgentsFromPac jest.doMock('@usebruno/requests', () => { const getOrCreateHttpsAgent = jest.fn(() => ({ type: 'https-agent' })); const getOrCreateHttpAgent = jest.fn(() => ({ type: 'http-agent' })); const getPacResolver = jest.fn(async () => ({ resolve: async () => pacDirectives, dispose: () => {} })); // Inline mock of resolveAgentsFromPac that wires through the mocked factories // so existing assertions on getOrCreateHttp(s)Agent call args still hold. const resolveAgentsFromPac = jest.fn(async ({ pacSource, requestUrl, tlsOptions, httpsAgentRequestFields, timeline, disableCache, hostname }) => { const resolver = await getPacResolver({ pacSource, httpsAgentRequestFields }); const directives = await resolver.resolve(requestUrl); if (!directives || !directives.length) return { directives: null }; const first = directives[0]; if (/^(PROXY|HTTPS?)\s+/i.test(first)) { const parts = first.split(/\s+/); const scheme = parts[0].toUpperCase() === 'HTTPS' ? 'https' : 'http'; const proxyUri = `${scheme}://${parts[1]}`; return { directives, httpAgent: getOrCreateHttpAgent({ proxyUri, options: { keepAlive: true }, timeline, disableCache, hostname }), httpsAgent: getOrCreateHttpsAgent({ proxyUri, options: tlsOptions, timeline, disableCache, hostname }) }; } if (/^SOCKS/i.test(first)) { const proto = /^SOCKS4\s/i.test(first) ? 'socks4' : 'socks5'; const proxyUri = `${proto}://${first.split(/\s+/)[1]}`; return { directives, httpAgent: getOrCreateHttpAgent({ proxyUri, options: { keepAlive: true }, timeline, disableCache, hostname }), httpsAgent: getOrCreateHttpsAgent({ proxyUri, options: tlsOptions, timeline, disableCache, hostname }) }; } return { directives }; }); return { getOrCreateHttpsAgent, getOrCreateHttpAgent, getPacResolver, resolveAgentsFromPac, PatchedHttpsProxyAgent: class {}, clearPacCache: jest.fn() }; }); }; describe('proxy-util', () => { beforeEach(() => jestClearModules()); afterEach(() => jestClearModules()); test('shouldUseProxy respects wildcard bypass', () => { const { shouldUseProxy } = require('../src/utils/proxy-util'); expect(shouldUseProxy('http://example.com', '*')).toBe(false); }); test('setupProxyAgents: PAC PROXY directive sets http and https agents', async () => { setupMocks({ pacDirectives: ['PROXY p.example:8080', 'DIRECT'] }); const { setupProxyAgents } = require('../src/utils/proxy-util'); const { getOrCreateHttpAgent, getOrCreateHttpsAgent } = require('@usebruno/requests'); const requestConfig = { url: 'http://example.com/resource' }; const timeline = []; await setupProxyAgents({ requestConfig, proxyMode: 'pac', proxyConfig: { pac: { source: 'http://pac-server/proxy.pac' } }, httpsAgentRequestFields: {}, interpolationOptions: {}, timeline }); expect(requestConfig.httpsAgent).toBeDefined(); expect(requestConfig.httpAgent).toBeDefined(); expect(getOrCreateHttpsAgent).toHaveBeenCalledWith(expect.objectContaining({ proxyUri: 'http://p.example:8080' })); expect(getOrCreateHttpAgent).toHaveBeenCalledWith(expect.objectContaining({ proxyUri: 'http://p.example:8080' })); const hasPacInfo = timeline.some((t) => t.type === 'info' && /PAC directives/.test(t.message)); expect(hasPacInfo).toBe(true); }); test('setupProxyAgents: PAC DIRECT directive bypasses proxy and uses fallback agent', async () => { setupMocks({ pacDirectives: ['DIRECT'] }); const { setupProxyAgents } = require('../src/utils/proxy-util'); const { getOrCreateHttpAgent, getOrCreateHttpsAgent } = require('@usebruno/requests'); const requestConfig = { url: 'http://example.com/resource' }; const timeline = []; await setupProxyAgents({ requestConfig, proxyMode: 'pac', proxyConfig: { pac: { source: 'http://pac-server/proxy.pac' } }, httpsAgentRequestFields: {}, interpolationOptions: {}, timeline }); // DIRECT → no proxy agents set inside PAC block, fallback sets httpAgent for http request expect(requestConfig.httpAgent).toBeDefined(); // httpsAgent should NOT have been set (http request, not https) expect(requestConfig.httpsAgent).toBeUndefined(); // Fallback agent called with null proxyUri expect(getOrCreateHttpAgent).toHaveBeenCalledWith(expect.objectContaining({ proxyUri: null })); expect(getOrCreateHttpsAgent).not.toHaveBeenCalled(); }); test('setupProxyAgents: PAC SOCKS directive sets socks agents', async () => { setupMocks({ pacDirectives: ['SOCKS5 socks.example:1080'] }); const { setupProxyAgents } = require('../src/utils/proxy-util'); const { getOrCreateHttpAgent, getOrCreateHttpsAgent } = require('@usebruno/requests'); const requestConfig = { url: 'http://example.com/resource' }; const timeline = []; await setupProxyAgents({ requestConfig, proxyMode: 'pac', proxyConfig: { pac: { source: 'http://pac-server/proxy.pac' } }, httpsAgentRequestFields: {}, interpolationOptions: {}, timeline }); expect(requestConfig.httpsAgent).toBeDefined(); expect(requestConfig.httpAgent).toBeDefined(); expect(getOrCreateHttpsAgent).toHaveBeenCalledWith( expect.objectContaining({ proxyUri: 'socks5://socks.example:1080' }) ); expect(getOrCreateHttpAgent).toHaveBeenCalledWith( expect.objectContaining({ proxyUri: 'socks5://socks.example:1080' }) ); }); test('setupProxyAgents: PAC resolution error logs to timeline and falls back to direct agent', async () => { jest.doMock('../src/store/preferences', () => ({ preferencesUtil: { isSslSessionCachingEnabled: () => false } })); jest.doMock('@usebruno/requests', () => ({ getOrCreateHttpsAgent: jest.fn(() => ({ type: 'https-agent' })), getOrCreateHttpAgent: jest.fn(() => ({ type: 'http-agent' })), getPacResolver: jest.fn(async () => { throw new Error('PAC fetch timeout'); }), resolveAgentsFromPac: jest.fn(async () => { throw new Error('PAC fetch timeout'); }), PatchedHttpsProxyAgent: class {}, clearPacCache: jest.fn() })); const { setupProxyAgents } = require('../src/utils/proxy-util'); const { getOrCreateHttpAgent } = require('@usebruno/requests'); const requestConfig = { url: 'http://example.com/resource' }; const timeline = []; await setupProxyAgents({ requestConfig, proxyMode: 'pac', proxyConfig: { pac: { source: 'http://unreachable/proxy.pac' } }, httpsAgentRequestFields: {}, interpolationOptions: {}, timeline }); // Error should be logged to timeline const hasError = timeline.some((t) => t.type === 'error' && /PAC resolution failed/.test(t.message)); expect(hasError).toBe(true); // Fallback direct agent should be set for the http request expect(requestConfig.httpAgent).toBeDefined(); expect(getOrCreateHttpAgent).toHaveBeenCalledWith(expect.objectContaining({ proxyUri: null })); }); });