import { Agent, ProxyAgent } from 'undici'; import { getProxyAgent, proxyFetch } from 'src/utils/http-proxy-agent'; // Mock the dependencies jest.mock('undici', () => ({ Agent: jest.fn().mockImplementation((options) => ({ type: 'Agent', options })), ProxyAgent: jest.fn().mockImplementation((options) => ({ type: 'ProxyAgent', options })), })); // Mock global fetch global.fetch = jest.fn(); describe('getProxyAgent', () => { // Store original environment variables const originalEnv = { ...process.env }; // Reset environment variables before each test beforeEach(() => { jest.clearAllMocks(); process.env = { ...originalEnv }; delete process.env.HTTP_PROXY; delete process.env.http_proxy; delete process.env.HTTPS_PROXY; delete process.env.https_proxy; delete process.env.NO_PROXY; delete process.env.no_proxy; delete process.env.N8N_AI_TIMEOUT_MAX; }); // Restore original environment after all tests afterAll(() => { process.env = originalEnv; }); describe('backward compatible behavior (no timeout options)', () => { it('should return undefined when no proxy environment variables are set and no timeout options', () => { const agent = getProxyAgent(); expect(agent).toBeUndefined(); expect(ProxyAgent).not.toHaveBeenCalled(); expect(Agent).not.toHaveBeenCalled(); }); it('should return undefined when no proxy is configured for target URL and no timeout options', () => { const agent = getProxyAgent('https://api.openai.com/v1'); expect(agent).toBeUndefined(); expect(ProxyAgent).not.toHaveBeenCalled(); expect(Agent).not.toHaveBeenCalled(); }); it('should create ProxyAgent with default timeouts when HTTPS_PROXY is set', () => { const proxyUrl = 'https://proxy.example.com:8080'; process.env.HTTPS_PROXY = proxyUrl; const agent = getProxyAgent(); expect(ProxyAgent).toHaveBeenCalledWith({ uri: proxyUrl, headersTimeout: 3600000, bodyTimeout: 3600000, }); expect(agent).toEqual({ type: 'ProxyAgent', options: { uri: proxyUrl, headersTimeout: 3600000, bodyTimeout: 3600000 }, }); }); it('should create ProxyAgent when https_proxy is set', () => { const proxyUrl = 'https://proxy.example.com:8080'; process.env.https_proxy = proxyUrl; getProxyAgent(); expect(ProxyAgent).toHaveBeenCalledWith({ uri: proxyUrl, headersTimeout: 3600000, bodyTimeout: 3600000, }); }); it('should respect priority order of proxy environment variables', () => { // Set multiple proxy environment variables process.env.HTTP_PROXY = 'http://http-proxy.example.com:8080'; process.env.http_proxy = 'http://http-proxy-lowercase.example.com:8080'; process.env.HTTPS_PROXY = 'https://https-proxy.example.com:8080'; process.env.https_proxy = 'https://https-proxy-lowercase.example.com:8080'; getProxyAgent(); // Should use https_proxy as it has highest priority now expect(ProxyAgent).toHaveBeenCalledWith( expect.objectContaining({ uri: 'https://https-proxy-lowercase.example.com:8080', }), ); }); }); describe('target URL provided', () => { it('should create ProxyAgent for HTTPS URL when HTTPS_PROXY is set', () => { const proxyUrl = 'https://proxy.example.com:8080'; process.env.HTTPS_PROXY = proxyUrl; getProxyAgent('https://api.openai.com/v1'); expect(ProxyAgent).toHaveBeenCalledWith(expect.objectContaining({ uri: proxyUrl })); }); it('should create ProxyAgent for HTTP URL when HTTP_PROXY is set', () => { const proxyUrl = 'http://proxy.example.com:8080'; process.env.HTTP_PROXY = proxyUrl; getProxyAgent('http://api.example.com'); expect(ProxyAgent).toHaveBeenCalledWith(expect.objectContaining({ uri: proxyUrl })); }); it('should use HTTPS_PROXY for HTTPS URLs even when HTTP_PROXY is set', () => { const httpProxy = 'http://http-proxy.example.com:8080'; const httpsProxy = 'https://https-proxy.example.com:8443'; process.env.HTTP_PROXY = httpProxy; process.env.HTTPS_PROXY = httpsProxy; getProxyAgent('https://api.openai.com/v1'); expect(ProxyAgent).toHaveBeenCalledWith(expect.objectContaining({ uri: httpsProxy })); }); it('should respect NO_PROXY for localhost', () => { const proxyUrl = 'http://proxy.example.com:8080'; process.env.HTTP_PROXY = proxyUrl; process.env.NO_PROXY = 'localhost,127.0.0.1'; const agent = getProxyAgent('http://localhost:3000'); expect(agent).toBeUndefined(); expect(ProxyAgent).not.toHaveBeenCalled(); }); it('should respect NO_PROXY wildcard patterns', () => { const proxyUrl = 'http://proxy.example.com:8080'; process.env.HTTPS_PROXY = proxyUrl; process.env.NO_PROXY = '*.internal.company.com,localhost'; const agent = getProxyAgent('https://api.internal.company.com'); expect(agent).toBeUndefined(); expect(ProxyAgent).not.toHaveBeenCalled(); }); it('should use proxy for URLs not in NO_PROXY', () => { const proxyUrl = 'http://proxy.example.com:8080'; process.env.HTTPS_PROXY = proxyUrl; process.env.NO_PROXY = 'localhost,127.0.0.1'; getProxyAgent('https://api.openai.com/v1'); expect(ProxyAgent).toHaveBeenCalledWith(expect.objectContaining({ uri: proxyUrl })); }); it('should handle mixed case environment variables', () => { const proxyUrl = 'http://proxy.example.com:8080'; process.env.https_proxy = proxyUrl; process.env.no_proxy = 'localhost'; getProxyAgent('https://api.openai.com/v1'); expect(ProxyAgent).toHaveBeenCalledWith(expect.objectContaining({ uri: proxyUrl })); }); }); describe('timeout options', () => { it('should pass custom timeout options to ProxyAgent when proxy is set', () => { const proxyUrl = 'https://proxy.example.com:8080'; process.env.HTTPS_PROXY = proxyUrl; getProxyAgent('https://api.openai.com/v1', { headersTimeout: 120000, bodyTimeout: 180000, }); expect(ProxyAgent).toHaveBeenCalledWith({ uri: proxyUrl, headersTimeout: 120000, bodyTimeout: 180000, }); }); it('should create Agent with timeout options when no proxy is configured', () => { const agent = getProxyAgent('https://api.openai.com/v1', { headersTimeout: 120000, bodyTimeout: 180000, }); expect(Agent).toHaveBeenCalledWith({ headersTimeout: 120000, bodyTimeout: 180000, }); expect(agent).toEqual({ type: 'Agent', options: { headersTimeout: 120000, bodyTimeout: 180000 }, }); }); it('should use default timeouts when empty timeout options object is passed', () => { getProxyAgent('https://api.openai.com/v1', {}); expect(Agent).toHaveBeenCalledWith({ headersTimeout: 3600000, bodyTimeout: 3600000, }); }); it('should include connectTimeout when provided', () => { getProxyAgent('https://api.openai.com/v1', { headersTimeout: 60000, bodyTimeout: 60000, connectTimeout: 30000, }); expect(Agent).toHaveBeenCalledWith({ headersTimeout: 60000, bodyTimeout: 60000, connectTimeout: 30000, }); }); it('should respect custom timeout from environment variable', () => { process.env.N8N_AI_TIMEOUT_MAX = '300000'; // Need to re-import to pick up env vars (or mock module) // For this test, we just verify the default timeout parsing // The actual behavior is tested by integration tests // Empty options should use env var defaults getProxyAgent('https://api.openai.com/v1', {}); // Since we can't easily re-import, we verify the mock was called with defaults expect(Agent).toHaveBeenCalled(); }); }); }); describe('proxyFetch', () => { // Store original environment variables const originalEnv = { ...process.env }; const mockFetch = global.fetch as jest.MockedFunction; // Reset environment variables and mocks before each test beforeEach(() => { jest.clearAllMocks(); process.env = { ...originalEnv }; delete process.env.HTTP_PROXY; delete process.env.http_proxy; delete process.env.HTTPS_PROXY; delete process.env.https_proxy; delete process.env.NO_PROXY; delete process.env.no_proxy; // Setup default fetch mock response mockFetch.mockResolvedValue( new Response('{}', { status: 200, statusText: 'OK', headers: { 'Content-Type': 'application/json' }, }), ); }); // Restore original environment after all tests afterAll(() => { process.env = originalEnv; }); describe('with no proxy configured', () => { it('should call fetch with undefined dispatcher when no proxy is set and no timeout options', async () => { const url = 'https://api.openai.com/v1'; await proxyFetch(url); expect(mockFetch).toHaveBeenCalledWith(url, { dispatcher: undefined, }); }); it('should call fetch with Agent dispatcher when timeout options are provided', async () => { const url = 'https://api.openai.com/v1'; await proxyFetch(url, undefined, { headersTimeout: 60000 }); expect(Agent).toHaveBeenCalled(); expect(mockFetch).toHaveBeenCalledWith(url, { dispatcher: expect.objectContaining({ type: 'Agent' }), }); }); it('should pass through RequestInit options', async () => { const url = 'https://api.openai.com/v1'; const init: RequestInit = { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ test: 'data' }), }; await proxyFetch(url, init); expect(mockFetch).toHaveBeenCalledWith(url, { ...init, dispatcher: undefined, }); }); it('should handle URL objects', async () => { const url = new URL('https://api.openai.com/v1'); await proxyFetch(url); expect(mockFetch).toHaveBeenCalledWith(url, { dispatcher: undefined, }); }); it('should handle Request objects', async () => { const request = new Request('https://api.openai.com/v1', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ test: 'data' }), }); await proxyFetch(request); expect(mockFetch).toHaveBeenCalledWith(request, { dispatcher: undefined, }); }); }); describe('with proxy configured', () => { it('should call fetch with ProxyAgent dispatcher when proxy is set', async () => { const proxyUrl = 'https://proxy.example.com:8080'; process.env.HTTPS_PROXY = proxyUrl; const url = 'https://api.openai.com/v1'; await proxyFetch(url); expect(ProxyAgent).toHaveBeenCalledWith(expect.objectContaining({ uri: proxyUrl })); expect(mockFetch).toHaveBeenCalledWith(url, { dispatcher: expect.objectContaining({ type: 'ProxyAgent' }), }); }); it('should pass through RequestInit options with proxy', async () => { const proxyUrl = 'https://proxy.example.com:8080'; process.env.HTTPS_PROXY = proxyUrl; const url = 'https://api.openai.com/v1'; const init: RequestInit = { method: 'POST', headers: { Authorization: 'Bearer token123' }, }; await proxyFetch(url, init); expect(ProxyAgent).toHaveBeenCalledWith(expect.objectContaining({ uri: proxyUrl })); expect(mockFetch).toHaveBeenCalledWith(url, { ...init, dispatcher: expect.objectContaining({ type: 'ProxyAgent' }), }); }); it('should handle URL objects with proxy', async () => { const proxyUrl = 'http://proxy.example.com:8080'; process.env.HTTP_PROXY = proxyUrl; const url = new URL('http://api.example.com/data'); await proxyFetch(url); expect(ProxyAgent).toHaveBeenCalledWith(expect.objectContaining({ uri: proxyUrl })); expect(mockFetch).toHaveBeenCalledWith(url, { dispatcher: expect.objectContaining({ type: 'ProxyAgent' }), }); }); it('should handle Request objects with proxy', async () => { const proxyUrl = 'https://proxy.example.com:8080'; process.env.HTTPS_PROXY = proxyUrl; const request = new Request('https://api.openai.com/v1'); await proxyFetch(request); expect(ProxyAgent).toHaveBeenCalledWith(expect.objectContaining({ uri: proxyUrl })); expect(mockFetch).toHaveBeenCalledWith(request, { dispatcher: expect.objectContaining({ type: 'ProxyAgent' }), }); }); it('should respect NO_PROXY environment variable', async () => { const proxyUrl = 'http://proxy.example.com:8080'; process.env.HTTPS_PROXY = proxyUrl; process.env.NO_PROXY = 'localhost,127.0.0.1'; const url = 'https://localhost:3000/api'; await proxyFetch(url); // Should not create ProxyAgent for localhost expect(mockFetch).toHaveBeenCalledWith(url, { dispatcher: undefined, }); }); it('should pass timeout options to ProxyAgent when proxy is configured', async () => { const proxyUrl = 'https://proxy.example.com:8080'; process.env.HTTPS_PROXY = proxyUrl; const url = 'https://api.openai.com/v1'; await proxyFetch(url, undefined, { headersTimeout: 300000, bodyTimeout: 300000 }); expect(ProxyAgent).toHaveBeenCalledWith({ uri: proxyUrl, headersTimeout: 300000, bodyTimeout: 300000, }); }); }); describe('return value', () => { it('should return the Response from fetch', async () => { const expectedResponse = new Response('{"success":true}', { status: 200, statusText: 'OK', }); mockFetch.mockResolvedValueOnce(expectedResponse); const url = 'https://api.openai.com/v1'; const result = await proxyFetch(url); expect(result).toBe(expectedResponse); }); it('should propagate fetch errors', async () => { const error = new Error('Network error'); mockFetch.mockRejectedValueOnce(error); const url = 'https://api.openai.com/v1'; await expect(proxyFetch(url)).rejects.toThrow('Network error'); }); it('should return error responses without throwing', async () => { const errorResponse = new Response('Not Found', { status: 404, statusText: 'Not Found', }); mockFetch.mockResolvedValueOnce(errorResponse); const url = 'https://api.openai.com/v1'; const result = await proxyFetch(url); expect(result).toBe(errorResponse); expect(result.status).toBe(404); }); }); });