From 78b89df4ad08fa5978f9fe81f9c5c43988da9f9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Rodr=C3=ADguez?= Date: Wed, 14 Jan 2026 16:07:04 +0000 Subject: [PATCH] test: Add proxy configuration tests for GraphApiService --- api/server/services/GraphApiService.spec.js | 212 ++++++++++++++++++++ 1 file changed, 212 insertions(+) diff --git a/api/server/services/GraphApiService.spec.js b/api/server/services/GraphApiService.spec.js index 0a625e77e1..79e901eeab 100644 --- a/api/server/services/GraphApiService.spec.js +++ b/api/server/services/GraphApiService.spec.js @@ -800,4 +800,216 @@ describe('GraphApiService', () => { }); }); }); + + describe('Proxy Configuration', () => { + let originalEnv; + const { ProxyAgent } = require('undici'); + const { HttpsProxyAgent } = require('https-proxy-agent'); + + beforeAll(() => { + originalEnv = { ...process.env }; + }); + + beforeEach(() => { + jest.resetModules(); + process.env = { ...originalEnv }; + jest.clearAllMocks(); + }); + + afterEach(() => { + process.env = originalEnv; + }); + + describe('Graph Client Proxy Configuration', () => { + it('should configure ProxyAgent dispatcher in Graph Client when PROXY env is set', async () => { + process.env.PROXY = 'http://proxy.example.com:8080'; + mockTokensCache.get.mockResolvedValue(null); + + await GraphApiService.createGraphClient('test-token', 'test-user'); + + expect(Client.init).toHaveBeenCalled(); + const initCall = Client.init.mock.calls[0][0]; + expect(initCall).toHaveProperty('fetchOptions'); + expect(initCall.fetchOptions).toHaveProperty('dispatcher'); + expect(initCall.fetchOptions.dispatcher).toBeInstanceOf(ProxyAgent); + }); + + it('should not configure ProxyAgent dispatcher when PROXY env is not set', async () => { + delete process.env.PROXY; + mockTokensCache.get.mockResolvedValue(null); + + await GraphApiService.createGraphClient('test-token', 'test-user'); + + expect(Client.init).toHaveBeenCalled(); + const initCall = Client.init.mock.calls[0][0]; + + // fetchOptions should either not exist or not have a dispatcher + if (initCall.fetchOptions) { + expect(initCall.fetchOptions.dispatcher).toBeUndefined(); + } + }); + + it('should pass correct proxy URL to ProxyAgent', async () => { + const proxyUrl = 'http://custom-proxy.example.com:3128'; + process.env.PROXY = proxyUrl; + mockTokensCache.get.mockResolvedValue(null); + + await GraphApiService.createGraphClient('test-token', 'test-user'); + + const initCall = Client.init.mock.calls[0][0]; + expect(initCall.fetchOptions.dispatcher).toBeInstanceOf(ProxyAgent); + // ProxyAgent stores the URL internally + expect(initCall.fetchOptions.dispatcher.proxy).toBeDefined(); + }); + }); + + describe('OpenID Client Proxy Configuration', () => { + it('should configure HttpsProxyAgent for openid-client custom fetch when PROXY env is set', async () => { + process.env.PROXY = 'http://proxy.example.com:8080'; + mockTokensCache.get.mockResolvedValue(null); + + await GraphApiService.exchangeTokenForGraphAccess( + mockOpenIdConfig, + 'test-token', + 'test-user', + ); + + if (client.genericGrantRequest) { + expect(client.genericGrantRequest).toHaveBeenCalled(); + const clientOptions = client.genericGrantRequest.mock.calls[0][3]; + + expect(clientOptions).toBeDefined(); + expect(clientOptions).toHaveProperty(Symbol.for('openid-client.custom.fetch')); + expect(typeof clientOptions[Symbol.for('openid-client.custom.fetch')]).toBe('function'); + } + }); + + it('should not configure custom fetch when PROXY env is not set', async () => { + delete process.env.PROXY; + mockTokensCache.get.mockResolvedValue(null); + + await GraphApiService.exchangeTokenForGraphAccess( + mockOpenIdConfig, + 'test-token', + 'test-user', + ); + + if (client.genericGrantRequest) { + expect(client.genericGrantRequest).toHaveBeenCalled(); + const clientOptions = client.genericGrantRequest.mock.calls[0][3]; + + // clientOptions should be empty object or not have custom fetch + expect(clientOptions).toEqual({}); + } + }); + + it('should use HttpsProxyAgent with correct proxy URL', async () => { + const proxyUrl = 'http://custom-proxy.example.com:3128'; + process.env.PROXY = proxyUrl; + mockTokensCache.get.mockResolvedValue(null); + + await GraphApiService.exchangeTokenForGraphAccess( + mockOpenIdConfig, + 'test-token', + 'test-user', + ); + + if (client.genericGrantRequest) { + const clientOptions = client.genericGrantRequest.mock.calls[0][3]; + const customFetch = clientOptions[Symbol.for('openid-client.custom.fetch')]; + + expect(customFetch).toBeDefined(); + expect(typeof customFetch).toBe('function'); + } + }); + + it('should pass HttpsProxyAgent through custom fetch to nodeFetch', async () => { + const nodeFetch = require('node-fetch'); + jest.spyOn(nodeFetch, 'default'); + + process.env.PROXY = 'http://proxy.example.com:8080'; + mockTokensCache.get.mockResolvedValue(null); + + await GraphApiService.exchangeTokenForGraphAccess( + mockOpenIdConfig, + 'test-token', + 'test-user', + ); + + if (client.genericGrantRequest) { + const clientOptions = client.genericGrantRequest.mock.calls[0][3]; + const customFetch = clientOptions[Symbol.for('openid-client.custom.fetch')]; + + // Test that custom fetch function properly includes agent + if (customFetch) { + const mockUrl = 'https://test.example.com'; + const mockOptions = { method: 'POST' }; + + // The custom fetch should be callable and should merge options with agent + expect(() => customFetch(mockUrl, mockOptions)).not.toThrow(); + } + } + }); + }); + + describe('Proxy Integration Tests', () => { + it('should maintain proxy configuration throughout token exchange and graph client creation', async () => { + process.env.PROXY = 'http://proxy.example.com:8080'; + mockTokensCache.get.mockResolvedValue(null); + + // First, exchange token (tests openid-client proxy) + const graphToken = await GraphApiService.exchangeTokenForGraphAccess( + mockOpenIdConfig, + 'test-token', + 'test-user', + ); + + expect(graphToken).toBe('mocked-graph-token'); + + if (client.genericGrantRequest) { + const clientOptions = client.genericGrantRequest.mock.calls[0][3]; + expect(clientOptions[Symbol.for('openid-client.custom.fetch')]).toBeDefined(); + } + + // Then, create graph client (tests Graph API proxy) + await GraphApiService.createGraphClient('test-token', 'test-user'); + + const initCall = Client.init.mock.calls[0][0]; + expect(initCall.fetchOptions.dispatcher).toBeInstanceOf(ProxyAgent); + }); + + it('should handle different proxy protocols', async () => { + const proxyProtocols = [ + 'http://proxy.example.com:8080', + 'https://secure-proxy.example.com:8443', + 'socks5://socks-proxy.example.com:1080', + ]; + + for (const proxyUrl of proxyProtocols) { + process.env.PROXY = proxyUrl; + mockTokensCache.get.mockResolvedValue(null); + jest.clearAllMocks(); + + await GraphApiService.createGraphClient('test-token', 'test-user'); + + const initCall = Client.init.mock.calls[0][0]; + expect(initCall.fetchOptions.dispatcher).toBeInstanceOf(ProxyAgent); + } + }); + + it('should not fail when PROXY is set to empty string', async () => { + process.env.PROXY = ''; + mockTokensCache.get.mockResolvedValue(null); + + await expect( + GraphApiService.createGraphClient('test-token', 'test-user'), + ).resolves.not.toThrow(); + + const initCall = Client.init.mock.calls[0][0]; + if (initCall.fetchOptions) { + expect(initCall.fetchOptions.dispatcher).toBeUndefined(); + } + }); + }); + }); });