From 0dd7c507e9580f236626ceb56e8a6effc9b53b89 Mon Sep 17 00:00:00 2001 From: Ilya_Hancharyk Date: Tue, 28 Oct 2025 13:09:24 +0100 Subject: [PATCH 1/5] EPMRPP-109127 || Manage proxy settings directly --- __tests__/oauth.spec.js | 77 +++++++++- __tests__/proxyHelper.spec.js | 277 ++++++++++++++++++++++++++++++++++ lib/oauth.js | 11 ++ lib/proxyHelper.js | 184 ++++++++++++++++++++++ lib/rest.js | 7 +- package-lock.json | 47 +++++- package.json | 3 + 7 files changed, 597 insertions(+), 9 deletions(-) create mode 100644 __tests__/proxyHelper.spec.js create mode 100644 lib/proxyHelper.js diff --git a/__tests__/oauth.spec.js b/__tests__/oauth.spec.js index 506e7fa..f561f17 100644 --- a/__tests__/oauth.spec.js +++ b/__tests__/oauth.spec.js @@ -1,5 +1,6 @@ const axios = require('axios'); const OAuthInterceptor = require('../lib/oauth'); +const { HttpsProxyAgent } = require('https-proxy-agent'); jest.mock('axios', () => ({ post: jest.fn(), @@ -47,9 +48,8 @@ describe('OAuthInterceptor', () => { expect(params.get('client_id')).toBe(baseConfig.clientId); expect(params.get('client_secret')).toBe(baseConfig.clientSecret); expect(params.get('scope')).toBe(baseConfig.scope); - expect(config).toEqual({ - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - }); + expect(config.headers).toEqual({ 'Content-Type': 'application/x-www-form-urlencoded' }); + expect(config.httpsAgent).toBeDefined(); // Default agent added expect(oauthInterceptor.refreshToken).toBe('refresh-123'); expect(oauthInterceptor.tokenExpiresAt).toBe(baseTime + 120000); @@ -309,4 +309,75 @@ describe('OAuthInterceptor', () => { consoleErrorSpy.mockRestore(); consoleWarnSpy.mockRestore(); }); + + it('uses proxy configuration for token requests', async () => { + const baseTime = 1700000700000; + const nowSpy = jest.spyOn(Date, 'now').mockImplementation(() => baseTime); + const configWithProxy = { + ...baseConfig, + restClientConfig: { + proxy: { + protocol: 'https', + host: '127.0.0.1', + port: 9000, + }, + }, + }; + const oauthInterceptor = new OAuthInterceptor(configWithProxy); + axios.post.mockResolvedValue({ + data: { + access_token: 'token-with-proxy', + expires_in: 120, + }, + }); + + const token = await oauthInterceptor.getAccessToken(); + + expect(token).toBe('token-with-proxy'); + expect(axios.post).toHaveBeenCalledTimes(1); + const [url, params, config] = axios.post.mock.calls[0]; + + expect(url).toBe(baseConfig.tokenEndpoint); + expect(config.headers).toEqual({ 'Content-Type': 'application/x-www-form-urlencoded' }); + expect(config.httpsAgent).toBeInstanceOf(HttpsProxyAgent); + + nowSpy.mockRestore(); + }); + + it('bypasses proxy for token endpoint when in noProxy list', async () => { + const baseTime = 1700000800000; + const nowSpy = jest.spyOn(Date, 'now').mockImplementation(() => baseTime); + const configWithNoProxy = { + ...baseConfig, + restClientConfig: { + proxy: { + protocol: 'https', + host: '127.0.0.1', + port: 9000, + }, + noProxy: 'auth.example.com', + }, + }; + const oauthInterceptor = new OAuthInterceptor(configWithNoProxy); + axios.post.mockResolvedValue({ + data: { + access_token: 'token-no-proxy', + expires_in: 120, + }, + }); + + const token = await oauthInterceptor.getAccessToken(); + + expect(token).toBe('token-no-proxy'); + expect(axios.post).toHaveBeenCalledTimes(1); + const [url, params, config] = axios.post.mock.calls[0]; + + expect(url).toBe(baseConfig.tokenEndpoint); + expect(config.headers).toEqual({ 'Content-Type': 'application/x-www-form-urlencoded' }); + // Should use default agent, not proxy agent + expect(config.httpsAgent).toBeDefined(); + expect(config.httpsAgent.constructor.name).toBe('Agent'); + + nowSpy.mockRestore(); + }); }); diff --git a/__tests__/proxyHelper.spec.js b/__tests__/proxyHelper.spec.js new file mode 100644 index 0000000..02e9ecb --- /dev/null +++ b/__tests__/proxyHelper.spec.js @@ -0,0 +1,277 @@ +const { HttpsProxyAgent } = require('https-proxy-agent'); +const { HttpProxyAgent } = require('http-proxy-agent'); +const { + shouldBypassProxy, + getProxyConfig, + createProxyAgents, + getProxyAgentForUrl, +} = require('../lib/proxyHelper'); + +describe('proxyHelper', () => { + const originalEnv = process.env; + + beforeEach(() => { + jest.resetModules(); + process.env = { ...originalEnv }; + delete process.env.HTTP_PROXY; + delete process.env.HTTPS_PROXY; + delete process.env.NO_PROXY; + delete process.env.http_proxy; + delete process.env.https_proxy; + delete process.env.no_proxy; + }); + + afterAll(() => { + process.env = originalEnv; + }); + + describe('shouldBypassProxy', () => { + it('returns false when noProxy is not provided', () => { + expect(shouldBypassProxy('http://example.com', '')).toBe(false); + expect(shouldBypassProxy('http://example.com', null)).toBe(false); + expect(shouldBypassProxy('http://example.com', undefined)).toBe(false); + }); + + it('returns true for exact hostname match', () => { + expect(shouldBypassProxy('http://example.com', 'example.com')).toBe(true); + expect(shouldBypassProxy('https://example.com:8080', 'example.com')).toBe(true); + }); + + it('returns true for wildcard *', () => { + expect(shouldBypassProxy('http://example.com', '*')).toBe(true); + expect(shouldBypassProxy('https://any.domain.com', '*')).toBe(true); + }); + + it('returns true for subdomain match with leading dot', () => { + expect(shouldBypassProxy('http://sub.example.com', '.example.com')).toBe(true); + expect(shouldBypassProxy('http://deep.sub.example.com', '.example.com')).toBe(true); + expect(shouldBypassProxy('http://example.com', '.example.com')).toBe(false); + }); + + it('returns true for subdomain suffix match', () => { + expect(shouldBypassProxy('http://sub.example.com', 'example.com')).toBe(true); + expect(shouldBypassProxy('http://deep.sub.example.com', 'example.com')).toBe(true); + }); + + it('handles multiple entries separated by commas', () => { + const noProxy = 'localhost,example.com,.test.com'; + expect(shouldBypassProxy('http://localhost', noProxy)).toBe(true); + expect(shouldBypassProxy('http://example.com', noProxy)).toBe(true); + expect(shouldBypassProxy('http://sub.test.com', noProxy)).toBe(true); + expect(shouldBypassProxy('http://other.com', noProxy)).toBe(false); + }); + + it('is case insensitive', () => { + expect(shouldBypassProxy('http://EXAMPLE.COM', 'example.com')).toBe(true); + expect(shouldBypassProxy('http://example.com', 'EXAMPLE.COM')).toBe(true); + }); + + it('handles spaces in noProxy list', () => { + const noProxy = ' localhost , example.com , .test.com '; + expect(shouldBypassProxy('http://localhost', noProxy)).toBe(true); + expect(shouldBypassProxy('http://example.com', noProxy)).toBe(true); + }); + + it('returns false for invalid URLs', () => { + expect(shouldBypassProxy('not-a-url', 'example.com')).toBe(false); + }); + }); + + describe('getProxyConfig', () => { + it('returns null when no proxy is configured', () => { + expect(getProxyConfig('http://example.com', {})).toBeNull(); + }); + + it('returns null when proxy is explicitly disabled', () => { + const config = { proxy: false }; + expect(getProxyConfig('http://example.com', config)).toBeNull(); + }); + + it('returns null when URL matches noProxy from config', () => { + const config = { noProxy: 'example.com,localhost' }; + expect(getProxyConfig('http://example.com', config)).toBeNull(); + expect(getProxyConfig('http://localhost', config)).toBeNull(); + }); + + it('returns null when URL matches NO_PROXY from environment', () => { + process.env.NO_PROXY = 'example.com,localhost'; + expect(getProxyConfig('http://example.com', {})).toBeNull(); + }); + + it('prefers noProxy from config over environment', () => { + process.env.NO_PROXY = 'other.com'; + const config = { noProxy: 'example.com' }; + expect(getProxyConfig('http://example.com', config)).toBeNull(); + }); + + it('returns proxy URL from string config', () => { + const config = { proxy: 'http://proxy.example.com:8080' }; + const result = getProxyConfig('http://target.com', config); + expect(result).toEqual({ + proxyUrl: 'http://proxy.example.com:8080', + fromConfig: true, + }); + }); + + it('returns proxy URL from object config', () => { + const config = { + proxy: { + protocol: 'https', + host: 'proxy.example.com', + port: 8080, + }, + }; + const result = getProxyConfig('http://target.com', config); + expect(result).toEqual({ + proxyUrl: 'https://proxy.example.com:8080', + fromConfig: true, + }); + }); + + it('returns proxy URL with authentication', () => { + const config = { + proxy: { + protocol: 'http', + host: 'proxy.example.com', + port: 8080, + auth: { + username: 'user', + password: 'pass', + }, + }, + }; + const result = getProxyConfig('http://target.com', config); + expect(result).toEqual({ + proxyUrl: 'http://user:pass@proxy.example.com:8080', + fromConfig: true, + }); + }); + + it('uses http as default protocol for object config', () => { + const config = { + proxy: { + host: 'proxy.example.com', + port: 8080, + }, + }; + const result = getProxyConfig('http://target.com', config); + expect(result).toEqual({ + proxyUrl: 'http://proxy.example.com:8080', + fromConfig: true, + }); + }); + + it('returns proxy from HTTPS_PROXY environment variable for https URLs', () => { + process.env.HTTPS_PROXY = 'http://proxy.example.com:8080'; + const result = getProxyConfig('https://target.com', {}); + expect(result).toEqual({ + proxyUrl: 'http://proxy.example.com:8080', + fromConfig: false, + }); + }); + + it('returns proxy from HTTP_PROXY environment variable for http URLs', () => { + process.env.HTTP_PROXY = 'http://proxy.example.com:8080'; + const result = getProxyConfig('http://target.com', {}); + expect(result).toEqual({ + proxyUrl: 'http://proxy.example.com:8080', + fromConfig: false, + }); + }); + + it('respects NO_PROXY when using environment proxy', () => { + process.env.HTTP_PROXY = 'http://proxy.example.com:8080'; + process.env.NO_PROXY = 'target.com'; + const result = getProxyConfig('http://target.com', {}); + expect(result).toBeNull(); + }); + + it('prioritizes explicit config over environment variables', () => { + process.env.HTTPS_PROXY = 'http://env-proxy.com:8080'; + const config = { proxy: 'http://config-proxy.com:9090' }; + const result = getProxyConfig('https://target.com', config); + expect(result).toEqual({ + proxyUrl: 'http://config-proxy.com:9090', + fromConfig: true, + }); + }); + }); + + describe('createProxyAgents', () => { + it('returns default agent when no proxy is configured', () => { + const agents = createProxyAgents('http://example.com', {}); + expect(agents.httpAgent).toBeDefined(); + expect(agents.httpAgent.constructor.name).toBe('Agent'); + }); + + it('creates HttpProxyAgent for HTTP URLs', () => { + const config = { proxy: 'http://proxy.example.com:8080' }; + const agents = createProxyAgents('http://target.com', config); + expect(agents.httpAgent).toBeInstanceOf(HttpProxyAgent); + expect(agents.httpsAgent).toBeUndefined(); + }); + + it('creates HttpsProxyAgent for HTTPS URLs', () => { + const config = { proxy: 'http://proxy.example.com:8080' }; + const agents = createProxyAgents('https://target.com', config); + expect(agents.httpsAgent).toBeInstanceOf(HttpsProxyAgent); + expect(agents.httpAgent).toBeUndefined(); + }); + + it('returns default agent when URL is in noProxy list', () => { + const config = { + proxy: 'http://proxy.example.com:8080', + noProxy: 'target.com', + }; + const agents = createProxyAgents('http://target.com', config); + expect(agents.httpAgent).toBeDefined(); + expect(agents.httpAgent.constructor.name).toBe('Agent'); + }); + + it('returns default HTTPS agent for HTTPS URLs in noProxy list', () => { + const config = { + proxy: 'http://proxy.example.com:8080', + noProxy: 'target.com', + }; + const agents = createProxyAgents('https://target.com', config); + expect(agents.httpsAgent).toBeDefined(); + expect(agents.httpsAgent.constructor.name).toBe('Agent'); + }); + }); + + describe('getProxyAgentForUrl', () => { + it('returns appropriate agent based on URL protocol', () => { + const config = { proxy: 'http://proxy.example.com:8080' }; + + const httpAgents = getProxyAgentForUrl('http://target.com', config); + expect(httpAgents.httpAgent).toBeInstanceOf(HttpProxyAgent); + + const httpsAgents = getProxyAgentForUrl('https://target.com', config); + expect(httpsAgents.httpsAgent).toBeInstanceOf(HttpsProxyAgent); + }); + + it('respects noProxy configuration', () => { + const config = { + proxy: 'http://proxy.example.com:8080', + noProxy: 'localhost,target.com', + }; + const agents = getProxyAgentForUrl('http://target.com', config); + expect(agents.httpAgent).toBeDefined(); + expect(agents.httpAgent.constructor.name).toBe('Agent'); + }); + + it('works with environment variables', () => { + process.env.HTTP_PROXY = 'http://proxy.example.com:8080'; + const agents = getProxyAgentForUrl('http://target.com', {}); + expect(agents.httpAgent).toBeInstanceOf(HttpProxyAgent); + }); + + it('respects NO_PROXY environment variable', () => { + process.env.HTTP_PROXY = 'http://proxy.example.com:8080'; + process.env.NO_PROXY = 'target.com'; + const agents = getProxyAgentForUrl('http://target.com', {}); + expect(agents.httpAgent).toBeDefined(); + expect(agents.httpAgent.constructor.name).toBe('Agent'); + }); + }); +}); diff --git a/lib/oauth.js b/lib/oauth.js index 180d939..6a95872 100644 --- a/lib/oauth.js +++ b/lib/oauth.js @@ -1,4 +1,5 @@ const axios = require('axios'); +const { getProxyAgentForUrl } = require('./proxyHelper'); const TOKEN_REFRESH_THRESHOLD_MS = 60000; const DEFAULT_TOKEN_EXPIRATION_MS = 3600000; // 1 hour in milliseconds @@ -17,6 +18,7 @@ class OAuthInterceptor { * @param {string} [config.clientSecret] - OAuth client secret (optional) * @param {string} [config.scope] - OAuth scope (optional) * @param {boolean} [config.debug] - Enable debug logging + * @param {Object} [config.restClientConfig] - REST client configuration for proxy support */ constructor(config) { this.tokenEndpoint = config.tokenEndpoint; @@ -26,6 +28,7 @@ class OAuthInterceptor { this.clientSecret = config.clientSecret; this.scope = config.scope; this.debug = config.debug || false; + this.restClientConfig = config.restClientConfig || {}; this.accessToken = null; this.refreshToken = null; @@ -145,10 +148,18 @@ class OAuthInterceptor { params.append('scope', this.scope); } + // Get proxy agent for token endpoint + const proxyAgents = getProxyAgentForUrl(this.tokenEndpoint, this.restClientConfig); + + if (this.debug && Object.keys(proxyAgents).length > 0) { + this.logDebug(`Making token request to ${this.tokenEndpoint} with proxy agent`); + } + const response = await axios.post(this.tokenEndpoint, params, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, + ...proxyAgents, }); const { diff --git a/lib/proxyHelper.js b/lib/proxyHelper.js new file mode 100644 index 0000000..888dfaf --- /dev/null +++ b/lib/proxyHelper.js @@ -0,0 +1,184 @@ +const { getProxyForUrl } = require('proxy-from-env'); +const { HttpsProxyAgent } = require('https-proxy-agent'); +const { HttpProxyAgent } = require('http-proxy-agent'); +const http = require('http'); +const https = require('https'); + +/** + * Checks if a URL should bypass proxy based on NO_PROXY patterns + * @param {string} url - The URL to check + * @param {string} noProxy - Comma-separated list of domains/patterns to bypass + * @returns {boolean} - True if proxy should be bypassed + */ +function shouldBypassProxy(url, noProxy) { + if (!noProxy) return false; + + try { + const urlObj = new URL(url); + const hostname = urlObj.hostname.toLowerCase(); + + // Split NO_PROXY entries and clean them + const patterns = noProxy + .split(',') + .map((entry) => entry.trim().toLowerCase()) + .filter(Boolean); + + return patterns.some((pattern) => { + // Special case: * means bypass all + if (pattern === '*') return true; + + // Pattern with leading dot (.example.com) - only matches subdomains + if (pattern.startsWith('.')) { + const cleanPattern = pattern.slice(1); + // Should match sub.example.com but NOT example.com + return hostname.endsWith(`.${cleanPattern}`); + } + + // Pattern without leading dot (example.com) - matches domain and subdomains + // Exact match + if (hostname === pattern) return true; + + // Suffix match (example.com matches sub.example.com) + if (hostname.endsWith(`.${pattern}`)) return true; + + return false; + }); + } catch (error) { + // If URL parsing fails, don't bypass proxy + return false; + } +} + +/** + * Gets proxy configuration for a given URL + * Checks both environment variables and explicit config + * @param {string} url - The target URL + * @param {object} proxyConfig - Explicit proxy configuration from restClientConfig + * @returns {object|null} - Proxy URL and bypass info, or null if no proxy + */ +function getProxyConfig(url, proxyConfig = {}) { + const urlObj = new URL(url); + + // Check NO_PROXY from config or environment + const noProxyFromConfig = proxyConfig.noProxy; + const noProxyFromEnv = process.env.NO_PROXY || process.env.no_proxy || ''; + const noProxy = noProxyFromConfig || noProxyFromEnv; + + if (proxyConfig.debug) { + console.log('[ProxyHelper] getProxyConfig called:'); + console.log(' URL:', url); + console.log(' Hostname:', urlObj.hostname); + console.log(' noProxy from config:', noProxyFromConfig); + console.log(' noProxy from env:', noProxyFromEnv); + console.log(' Final noProxy:', noProxy); + } + + // Check if URL should bypass proxy + const shouldBypass = shouldBypassProxy(url, noProxy); + if (proxyConfig.debug) { + console.log(' Should bypass proxy:', shouldBypass); + } + + if (shouldBypass) { + return null; + } + + // If proxy is explicitly disabled + if (proxyConfig.proxy === false) { + return null; + } + + // Priority 1: Explicit proxy configuration object + if (proxyConfig.proxy && typeof proxyConfig.proxy === 'object') { + const { protocol: proxyProtocol, host, port, auth } = proxyConfig.proxy; + if (host && port) { + let proxyUrl = `${proxyProtocol || 'http'}://${host}:${port}`; + if (auth) { + const { username, password } = auth; + proxyUrl = `${proxyProtocol || 'http'}://${username}:${password}@${host}:${port}`; + } + return { proxyUrl }; + } + } + + // Priority 2: Explicit proxy URL string + if (typeof proxyConfig.proxy === 'string') { + return { proxyUrl: proxyConfig.proxy }; + } + + // Priority 3: Environment variables (with NO_PROXY support via proxy-from-env) + const proxyUrlFromEnv = getProxyForUrl(url); + if (proxyUrlFromEnv) { + return { proxyUrl: proxyUrlFromEnv }; + } + + return null; +} + +/** + * Creates an HTTP/HTTPS agent with proxy configuration for a specific URL + * @param {string} url - The target URL for the request + * @param {object} restClientConfig - The rest client configuration + * @returns {object} - Object with httpAgent and/or httpsAgent + */ +function createProxyAgents(url, restClientConfig = {}) { + const urlObj = new URL(url); + const isHttps = urlObj.protocol === 'https:'; + const proxyConfig = getProxyConfig(url, restClientConfig); + + if (!proxyConfig) { + if (restClientConfig.debug) { + console.log('[ProxyHelper] No proxy for URL (bypassed or not configured):', url); + console.log(' Using default agent to prevent axios from using env proxy'); + } + // Return a default agent to prevent axios from using HTTP_PROXY/HTTPS_PROXY env vars + // This ensures that URLs in noProxy truly bypass the proxy + if (isHttps) { + return { + httpsAgent: new https.Agent(), + }; + } else { + return { + httpAgent: new http.Agent(), + }; + } + } + + const { proxyUrl } = proxyConfig; + + if (restClientConfig.debug) { + console.log('[ProxyHelper] Creating proxy agent:'); + console.log(' URL:', url); + console.log(' Protocol:', urlObj.protocol); + console.log(' Proxy URL:', proxyUrl); + } + + // Create appropriate agent based on target protocol + if (isHttps) { + return { + httpsAgent: new HttpsProxyAgent(proxyUrl), + }; + } else { + return { + httpAgent: new HttpProxyAgent(proxyUrl), + }; + } +} + +/** + * Gets proxy agent for a specific request URL + * This is the main function to be used in axios requests + * @param {string} url - The target URL for the request + * @param {object} restClientConfig - The rest client configuration + * @returns {object} - Object with agent configuration for axios + */ +function getProxyAgentForUrl(url, restClientConfig = {}) { + return createProxyAgents(url, restClientConfig); +} + +module.exports = { + shouldBypassProxy, + getProxyConfig, + createProxyAgents, + getProxyAgentForUrl, +}; diff --git a/lib/rest.js b/lib/rest.js index 5aa4941..40d17ca 100644 --- a/lib/rest.js +++ b/lib/rest.js @@ -4,6 +4,7 @@ const http = require('http'); const https = require('https'); const logger = require('./logger'); const OAuthInterceptor = require('./oauth'); +const { getProxyAgentForUrl } = require('./proxyHelper'); const DEFAULT_MAX_CONNECTION_TIME_MS = 30000; const DEFAULT_RETRY_ATTEMPTS = 6; @@ -16,7 +17,7 @@ const DEFAULT_RETRY_CONFIG = { retryCondition: axiosRetry.isRetryableError, shouldResetTimeout: true, }; -const SKIPPED_REST_CONFIG_KEYS = ['agent', 'retry']; +const SKIPPED_REST_CONFIG_KEYS = ['agent', 'retry', 'proxy', 'noProxy']; class RestClient { constructor(options) { @@ -39,6 +40,7 @@ class RestClient { const oauthInterceptor = new OAuthInterceptor({ ...this.oauthConfig, debug: this.debug, + restClientConfig: this.restClientConfig, }); oauthInterceptor.attach(this.axiosInstance); } catch (error) { @@ -62,12 +64,15 @@ class RestClient { } request(method, url, data, options = {}) { + const proxyAgents = getProxyAgentForUrl(url, this.restClientConfig); + return this.axiosInstance .request({ method, url, data, ...options, + ...proxyAgents, headers: { HOST: new URL(url).host, ...options.headers, diff --git a/package-lock.json b/package-lock.json index a24024f..50214f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,13 +6,16 @@ "packages": { "": { "name": "@reportportal/client-javascript", - "version": "5.4.1", + "version": "5.4.3", "license": "Apache-2.0", "dependencies": { "axios": "^1.12.2", "axios-retry": "^4.5.0", "glob": "^8.1.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", "ini": "^2.0.0", + "proxy-from-env": "^1.1.0", "uniqid": "^5.4.0", "uuid": "^9.0.1" }, @@ -1609,6 +1612,15 @@ "node": ">=0.4.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2307,7 +2319,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -3661,6 +3672,32 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -5038,8 +5075,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/natural-compare": { "version": "1.4.0", @@ -5479,7 +5515,8 @@ "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" }, "node_modules/punycode": { "version": "2.3.0", diff --git a/package.json b/package.json index b2c9ddc..0c366a6 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,10 @@ "axios": "^1.12.2", "axios-retry": "^4.5.0", "glob": "^8.1.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", "ini": "^2.0.0", + "proxy-from-env": "^1.1.0", "uniqid": "^5.4.0", "uuid": "^9.0.1" }, From dadb7c5a2d05522e1afb6a03d6913b21376863c9 Mon Sep 17 00:00:00 2001 From: Ilya_Hancharyk Date: Tue, 28 Oct 2025 13:21:39 +0100 Subject: [PATCH 2/5] EPMRPP-109127 || Fix tests. Update types and readme --- README.md | 165 +++++++++++++++++++++++++++++++++- __tests__/proxyHelper.spec.js | 7 -- index.d.ts | 88 +++++++++++++++++- 3 files changed, 251 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index f783143..6d68215 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ rpClient.checkConnect().then(() => { | headers | Optional | {} | The object with custom headers for internal http client. | | debug | Optional | false | This flag allows seeing the logs of the client. Useful for debugging. | | isLaunchMergeRequired | Optional | false | Allows client to merge launches into one at the end of the run via saving their UUIDs to the temp files at filesystem. At the end of the run launches can be merged using `mergeLaunches` method. Temp file format: `rplaunch-${launch_uuid}.tmp`. | -| restClientConfig | Optional | Not set | `axios` like http client [config](https://github.com/axios/axios#request-config). May contain `agent` property for configure [http(s)](https://nodejs.org/api/https.html#https_https_request_url_options_callback) client, and other client options eg. `timeout`. For debugging and displaying logs you can set `debug: true`. Use the `retry` property (number or [`axios-retry`](https://github.com/softonic/axios-retry#options) config) to customise automatic retries. | +| restClientConfig | Optional | Not set | `axios` like http client [config](https://github.com/axios/axios#request-config). Supports `proxy` and `noProxy` for proxy configuration (see [Proxy configuration](#proxy-configuration)), `agent` property for [http(s)](https://nodejs.org/api/https.html#https_https_request_url_options_callback) client options, `timeout`, `debug: true` for debugging, and `retry` property (number or [`axios-retry`](https://github.com/softonic/axios-retry#options) config) for automatic retries. | | launchUuidPrint | Optional | false | Whether to print the current launch UUID. | | launchUuidPrintOutput | Optional | 'STDOUT' | Launch UUID printing output. Possible values: 'STDOUT', 'STDERR', 'FILE', 'ENVIRONMENT'. Works only if `launchUuidPrint` set to `true`. File format: `rp-launch-uuid-${launch_uuid}.tmp`. Env variable: `RP_LAUNCH_UUID`. | | token | Deprecated | Not set | Use `apiKey` or `oauth` instead. | @@ -166,6 +166,169 @@ const client = new RPClient({ Setting `retry: 0` disables automatic retries. +### Proxy configuration + +The client supports comprehensive proxy configuration for both HTTP and HTTPS requests, including ReportPortal API calls and OAuth token requests. Proxy settings can be configured via `restClientConfig` or environment variables. + +#### Basic proxy configuration + +##### Via configuration object + +```javascript +const RPClient = require('@reportportal/client-javascript'); + +const rpClient = new RPClient({ + apiKey: 'your_api_key', + endpoint: 'http://your-instance.com:8080/api/v1', + launch: 'LAUNCH_NAME', + project: 'PROJECT_NAME', + restClientConfig: { + proxy: { + protocol: 'https', // 'http' or 'https' + host: '127.0.0.1', + port: 8080, + // Optional authentication + auth: { + username: 'proxy-user', + password: 'proxy-password' + } + } + } +}); +``` + +##### Via proxy URL string + +```javascript +const rpClient = new RPClient({ + // ... other options + restClientConfig: { + proxy: 'https://127.0.0.1:8080' + } +}); +``` + +##### Via environment variables + +The client automatically detects and uses proxy environment variables: + +```bash +export HTTPS_PROXY=https://127.0.0.1:8080 +export HTTP_PROXY=http://127.0.0.1:8080 +export NO_PROXY=localhost,127.0.0.1,.local +``` + +#### Bypassing proxy for specific domains (noProxy) + +Use the `noProxy` option to exclude specific domains from being proxied. This is useful when some services are accessible directly while others require a proxy. + +```javascript +const rpClient = new RPClient({ + // ... other options + restClientConfig: { + proxy: { + protocol: 'https', + host: '127.0.0.1', + port: 8080 + }, + // Bypass proxy for these domains + noProxy: 'localhost,127.0.0.1,internal.company.com,.local.domain' + } +}); +``` + +**noProxy format:** +- Exact hostname: `example.com` - matches `example.com` and `sub.example.com` +- Leading dot: `.example.com` - matches only subdomains like `sub.example.com` (not `example.com` itself) +- Wildcard: `*` - bypass proxy for all requests +- Multiple entries: Comma-separated list + +**Priority:** Configuration `noProxy` takes precedence over `NO_PROXY` environment variable. + +#### Proxy with OAuth authentication + +When using OAuth authentication, the proxy configuration is automatically applied to both: +- OAuth token endpoint requests +- ReportPortal API requests + +```javascript +const rpClient = new RPClient({ + endpoint: 'http://your-instance.com:8080/api/v1', + project: 'PROJECT_NAME', + oauth: { + tokenEndpoint: 'https://login.microsoftonline.com/.../oauth2/v2.0/token', + username: 'your-username', + password: 'your-password', + clientId: 'your-client-id' + }, + restClientConfig: { + proxy: { + protocol: 'https', + host: '127.0.0.1', + port: 8080 + }, + // Example: Use proxy for OAuth, bypass for ReportPortal + noProxy: 'your-instance.com' + } +}); +``` + +#### Advanced proxy scenarios + +##### Disable proxy explicitly + +```javascript +restClientConfig: { + proxy: false // Disable proxy even if environment variables are set +} +``` + +##### Debug proxy configuration + +Enable debug mode to see detailed proxy decision logs: + +```javascript +restClientConfig: { + proxy: { /* ... */ }, + noProxy: 'localhost,.local', + debug: true // See proxy-related logs +} +``` + +Debug output example: +``` +[ProxyHelper] getProxyConfig called: + URL: https://login.microsoftonline.com/oauth2/v2.0/token + Hostname: login.microsoftonline.com + noProxy from config: localhost,.local + Should bypass proxy: false +[ProxyHelper] Creating proxy agent: + URL: https://login.microsoftonline.com/oauth2/v2.0/token + Protocol: https: + Proxy URL: https://127.0.0.1:8080 +``` + +#### Proxy configuration options + +| Option | Type | Description | +|---------------------|------------------------------|-------------------------------------------------------------------------------------------------| +| `proxy` | `false \| string \| object` | Proxy configuration. Can be `false` (disable), URL string, or configuration object (see below) | +| `proxy.protocol` | `string` | Proxy protocol: `'http'` or `'https'` | +| `proxy.host` | `string` | Proxy host address | +| `proxy.port` | `number` | Proxy port number | +| `proxy.auth` | `object` | Optional proxy authentication | +| `proxy.auth.username` | `string` | Proxy username | +| `proxy.auth.password` | `string` | Proxy password | +| `noProxy` | `string` | Comma-separated list of domains to bypass proxy | + +#### How proxy handling works + +1. **Per-request proxy decision:** Each request (API or OAuth) determines its proxy configuration based on the target URL +2. **noProxy checking:** URLs matching `noProxy` patterns bypass the proxy and connect directly +3. **Default agents for bypassed URLs:** When a URL bypasses proxy, a default HTTP/HTTPS agent is used to prevent automatic proxy detection +4. **Environment variable support:** `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` are automatically detected and used if no explicit configuration is provided +5. **Priority:** Explicit configuration takes precedence over environment variables + ### checkConnect `checkConnect` - asynchronous method for verifying the correctness of the client connection diff --git a/__tests__/proxyHelper.spec.js b/__tests__/proxyHelper.spec.js index 02e9ecb..0b36d91 100644 --- a/__tests__/proxyHelper.spec.js +++ b/__tests__/proxyHelper.spec.js @@ -109,7 +109,6 @@ describe('proxyHelper', () => { const result = getProxyConfig('http://target.com', config); expect(result).toEqual({ proxyUrl: 'http://proxy.example.com:8080', - fromConfig: true, }); }); @@ -124,7 +123,6 @@ describe('proxyHelper', () => { const result = getProxyConfig('http://target.com', config); expect(result).toEqual({ proxyUrl: 'https://proxy.example.com:8080', - fromConfig: true, }); }); @@ -143,7 +141,6 @@ describe('proxyHelper', () => { const result = getProxyConfig('http://target.com', config); expect(result).toEqual({ proxyUrl: 'http://user:pass@proxy.example.com:8080', - fromConfig: true, }); }); @@ -157,7 +154,6 @@ describe('proxyHelper', () => { const result = getProxyConfig('http://target.com', config); expect(result).toEqual({ proxyUrl: 'http://proxy.example.com:8080', - fromConfig: true, }); }); @@ -166,7 +162,6 @@ describe('proxyHelper', () => { const result = getProxyConfig('https://target.com', {}); expect(result).toEqual({ proxyUrl: 'http://proxy.example.com:8080', - fromConfig: false, }); }); @@ -175,7 +170,6 @@ describe('proxyHelper', () => { const result = getProxyConfig('http://target.com', {}); expect(result).toEqual({ proxyUrl: 'http://proxy.example.com:8080', - fromConfig: false, }); }); @@ -192,7 +186,6 @@ describe('proxyHelper', () => { const result = getProxyConfig('https://target.com', config); expect(result).toEqual({ proxyUrl: 'http://config-proxy.com:9090', - fromConfig: true, }); }); }); diff --git a/index.d.ts b/index.d.ts index 314059a..5a40901 100644 --- a/index.d.ts +++ b/index.d.ts @@ -29,6 +29,75 @@ declare module '@reportportal/client-javascript' { scope?: string; } + /** + * Proxy configuration object. + */ + export interface ProxyConfig { + /** + * Protocol for the proxy (http or https). + */ + protocol?: string; + /** + * Proxy host. + */ + host: string; + /** + * Proxy port. + */ + port: number; + /** + * Optional authentication for the proxy. + */ + auth?: { + username: string; + password: string; + }; + } + + /** + * REST client configuration options. + */ + export interface RestClientConfig { + /** + * Request timeout in milliseconds. + */ + timeout?: number; + /** + * Proxy configuration. Can be: + * - false: Disable proxy + * - string: Proxy URL (e.g., 'http://proxy.example.com:8080') + * - ProxyConfig object: Detailed proxy configuration + */ + proxy?: false | string | ProxyConfig; + /** + * Comma-separated list of domains to bypass proxy. + * Example: 'localhost,127.0.0.1,.example.com' + * This takes precedence over NO_PROXY environment variable. + */ + noProxy?: string; + /** + * Custom HTTP agent options. + */ + agent?: Record; + /** + * Retry configuration. + */ + retry?: number | { + retries?: number; + retryDelay?: (retryCount: number) => number; + retryCondition?: (error: any) => boolean; + shouldResetTimeout?: boolean; + }; + /** + * Enable debug logging. + */ + debug?: boolean; + /** + * Any other axios configuration options. + */ + [key: string]: unknown; + } + /** * Configuration options for initializing the Report Portal client. * @@ -56,6 +125,23 @@ declare module '@reportportal/client-javascript' { * } * }); * ``` + * + * @example With Proxy Configuration + * ```typescript + * const rp = new ReportPortalClient({ + * endpoint: 'https://your.reportportal.server/api/v1', + * project: 'your_project_name', + * apiKey: 'your_api_key', + * restClientConfig: { + * proxy: { + * protocol: 'https', + * host: '127.0.0.1', + * port: 8080, + * }, + * noProxy: 'localhost,.local.domain', + * } + * }); + * ``` */ export interface ReportPortalConfig { apiKey?: string; @@ -67,7 +153,7 @@ declare module '@reportportal/client-javascript' { isLaunchMergeRequired?: boolean; launchUuidPrint?: boolean; launchUuidPrintOutput?: string; - restClientConfig?: Record; + restClientConfig?: RestClientConfig; token?: string; /** * OAuth 2.0 configuration object. When provided, OAuth authentication will be used instead of API key. From 72f59365f406197bcd07db80fe7eacbdfbdcd9f6 Mon Sep 17 00:00:00 2001 From: Ilya_Hancharyk Date: Tue, 28 Oct 2025 13:33:02 +0100 Subject: [PATCH 3/5] EPMRPP-109127 || Update custom agents handling --- __tests__/oauth.spec.js | 7 +++++-- lib/oauth.js | 11 +++++++++-- lib/rest.js | 6 +++++- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/__tests__/oauth.spec.js b/__tests__/oauth.spec.js index f561f17..8ac7091 100644 --- a/__tests__/oauth.spec.js +++ b/__tests__/oauth.spec.js @@ -1,6 +1,6 @@ const axios = require('axios'); -const OAuthInterceptor = require('../lib/oauth'); const { HttpsProxyAgent } = require('https-proxy-agent'); +const OAuthInterceptor = require('../lib/oauth'); jest.mock('axios', () => ({ post: jest.fn(), @@ -152,7 +152,10 @@ describe('OAuthInterceptor', () => { it('logs debug messages only when debug mode is enabled', () => { const consoleSpy = jest.spyOn(console, 'log').mockImplementation(); - const oauthInterceptor = new OAuthInterceptor({ ...baseConfig, debug: true }); + const oauthInterceptor = new OAuthInterceptor({ + ...baseConfig, + restClientConfig: { debug: true }, + }); oauthInterceptor.logDebug('message', { foo: 'bar' }); expect(consoleSpy).toHaveBeenCalledWith('[OAuth] message', { foo: 'bar' }); diff --git a/lib/oauth.js b/lib/oauth.js index 6a95872..f8bc1db 100644 --- a/lib/oauth.js +++ b/lib/oauth.js @@ -27,8 +27,8 @@ class OAuthInterceptor { this.clientId = config.clientId; this.clientSecret = config.clientSecret; this.scope = config.scope; - this.debug = config.debug || false; this.restClientConfig = config.restClientConfig || {}; + this.debug = this.restClientConfig.debug || false; this.accessToken = null; this.refreshToken = null; @@ -149,7 +149,11 @@ class OAuthInterceptor { } // Get proxy agent for token endpoint - const proxyAgents = getProxyAgentForUrl(this.tokenEndpoint, this.restClientConfig); + // Only apply if custom agents are not explicitly provided + const hasCustomAgents = this.restClientConfig.httpsAgent || this.restClientConfig.httpAgent; + const proxyAgents = hasCustomAgents + ? {} + : getProxyAgentForUrl(this.tokenEndpoint, this.restClientConfig); if (this.debug && Object.keys(proxyAgents).length > 0) { this.logDebug(`Making token request to ${this.tokenEndpoint} with proxy agent`); @@ -160,6 +164,9 @@ class OAuthInterceptor { 'Content-Type': 'application/x-www-form-urlencoded', }, ...proxyAgents, + // Custom agents from restClientConfig (if provided) take precedence + ...(this.restClientConfig.httpsAgent && { httpsAgent: this.restClientConfig.httpsAgent }), + ...(this.restClientConfig.httpAgent && { httpAgent: this.restClientConfig.httpAgent }), }); const { diff --git a/lib/rest.js b/lib/rest.js index 40d17ca..8b19c69 100644 --- a/lib/rest.js +++ b/lib/rest.js @@ -64,7 +64,11 @@ class RestClient { } request(method, url, data, options = {}) { - const proxyAgents = getProxyAgentForUrl(url, this.restClientConfig); + // Only apply proxy agents if custom agents are not explicitly provided + // Priority: explicit httpsAgent/httpAgent > proxy config > default + const hasCustomAgents = + 'httpsAgent' in this.restClientConfig || 'httpAgent' in this.restClientConfig; + const proxyAgents = hasCustomAgents ? {} : getProxyAgentForUrl(url, this.restClientConfig); return this.axiosInstance .request({ From ec83f9b9dba158988b33483b00de159beff87810 Mon Sep 17 00:00:00 2001 From: Ilya_Hancharyk Date: Tue, 28 Oct 2025 13:36:21 +0100 Subject: [PATCH 4/5] EPMRPP-109127 || Sanitize credentials while logging --- __tests__/proxyHelper.spec.js | 68 +++++++++++++++++++++++++++++++++++ lib/proxyHelper.js | 23 +++++++++++- 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/__tests__/proxyHelper.spec.js b/__tests__/proxyHelper.spec.js index 0b36d91..cd0b858 100644 --- a/__tests__/proxyHelper.spec.js +++ b/__tests__/proxyHelper.spec.js @@ -267,4 +267,72 @@ describe('proxyHelper', () => { expect(agents.httpAgent.constructor.name).toBe('Agent'); }); }); + + describe('credential sanitization in debug logs', () => { + let consoleLogSpy; + + beforeEach(() => { + consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(); + }); + + afterEach(() => { + consoleLogSpy.mockRestore(); + }); + + it('sanitizes proxy credentials in debug logs when using proxy config object', () => { + const config = { + proxy: { + protocol: 'https', + host: 'proxy.example.com', + port: 8080, + auth: { + username: 'secretuser', + password: 'secretpass', + }, + }, + debug: true, + }; + + getProxyAgentForUrl('http://target.com', config); + + // Check that logs were called + expect(consoleLogSpy).toHaveBeenCalled(); + + // Check that credentials are not exposed in any log + const allLogs = consoleLogSpy.mock.calls.flat().join(' '); + expect(allLogs).not.toContain('secretuser'); + expect(allLogs).not.toContain('secretpass'); + // [REDACTED] gets URL-encoded to %5BREDACTED%5D + expect(allLogs).toMatch(/\[REDACTED\]|%5BREDACTED%5D/); + }); + + it('sanitizes proxy credentials in debug logs when using proxy URL string', () => { + const config = { + proxy: 'https://myuser:mypassword@proxy.example.com:8080', + debug: true, + }; + + getProxyAgentForUrl('http://target.com', config); + + // Check that credentials are not exposed in any log + const allLogs = consoleLogSpy.mock.calls.flat().join(' '); + expect(allLogs).not.toContain('myuser'); + expect(allLogs).not.toContain('mypassword'); + // [REDACTED] gets URL-encoded to %5BREDACTED%5D + expect(allLogs).toMatch(/\[REDACTED\]|%5BREDACTED%5D/); + }); + + it('logs proxy URL normally when no credentials are present', () => { + const config = { + proxy: 'https://proxy.example.com:8080', + debug: true, + }; + + getProxyAgentForUrl('http://target.com', config); + + const allLogs = consoleLogSpy.mock.calls.flat().join(' '); + expect(allLogs).toContain('https://proxy.example.com:8080'); + expect(allLogs).not.toContain('[REDACTED]'); + }); + }); }); diff --git a/lib/proxyHelper.js b/lib/proxyHelper.js index 888dfaf..e3ab904 100644 --- a/lib/proxyHelper.js +++ b/lib/proxyHelper.js @@ -4,6 +4,27 @@ const { HttpProxyAgent } = require('http-proxy-agent'); const http = require('http'); const https = require('https'); +/** + * Sanitizes a URL by removing credentials (username/password) for safe logging + * @param {string} urlString - The URL to sanitize + * @returns {string} - Sanitized URL with credentials replaced by [REDACTED] + */ +function sanitizeUrlForLogging(urlString) { + try { + const urlObj = new URL(urlString); + if (urlObj.username || urlObj.password) { + // Replace credentials with [REDACTED] + urlObj.username = '[REDACTED]'; + urlObj.password = ''; + return urlObj.toString(); + } + return urlString; + } catch (error) { + // If URL parsing fails, return as-is (likely not a URL) + return urlString; + } +} + /** * Checks if a URL should bypass proxy based on NO_PROXY patterns * @param {string} url - The URL to check @@ -150,7 +171,7 @@ function createProxyAgents(url, restClientConfig = {}) { console.log('[ProxyHelper] Creating proxy agent:'); console.log(' URL:', url); console.log(' Protocol:', urlObj.protocol); - console.log(' Proxy URL:', proxyUrl); + console.log(' Proxy URL:', sanitizeUrlForLogging(proxyUrl)); } // Create appropriate agent based on target protocol From 6b2afb112f4ad0acfb59bd363fc39ce7e4fa8326 Mon Sep 17 00:00:00 2001 From: Ilya_Hancharyk Date: Tue, 28 Oct 2025 13:45:00 +0100 Subject: [PATCH 5/5] EPMRPP-109127 || Remove unused vars --- __tests__/oauth.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/__tests__/oauth.spec.js b/__tests__/oauth.spec.js index 8ac7091..156b7ff 100644 --- a/__tests__/oauth.spec.js +++ b/__tests__/oauth.spec.js @@ -338,7 +338,7 @@ describe('OAuthInterceptor', () => { expect(token).toBe('token-with-proxy'); expect(axios.post).toHaveBeenCalledTimes(1); - const [url, params, config] = axios.post.mock.calls[0]; + const [url, , config] = axios.post.mock.calls[0]; expect(url).toBe(baseConfig.tokenEndpoint); expect(config.headers).toEqual({ 'Content-Type': 'application/x-www-form-urlencoded' }); @@ -373,7 +373,7 @@ describe('OAuthInterceptor', () => { expect(token).toBe('token-no-proxy'); expect(axios.post).toHaveBeenCalledTimes(1); - const [url, params, config] = axios.post.mock.calls[0]; + const [url, , config] = axios.post.mock.calls[0]; expect(url).toBe(baseConfig.tokenEndpoint); expect(config.headers).toEqual({ 'Content-Type': 'application/x-www-form-urlencoded' });