diff --git a/src/http-agent.ts b/src/http-agent.ts index b23b6e75..84050621 100644 --- a/src/http-agent.ts +++ b/src/http-agent.ts @@ -19,11 +19,14 @@ */ import { AxiosRequestConfig } from 'axios'; import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; +import { getProxyUrl } from './proxy'; +import { ScannerProperties } from './types'; export function getHttpAgents( - proxyUrl?: URL, + properties: ScannerProperties, ): Pick { const agents: Pick = {}; + const proxyUrl = getProxyUrl(properties); if (proxyUrl) { agents.httpsAgent = new HttpsProxyAgent({ proxy: proxyUrl.toString() }); diff --git a/src/java.ts b/src/java.ts index c08a4a3e..c549e489 100644 --- a/src/java.ts +++ b/src/java.ts @@ -32,12 +32,10 @@ import { SONARQUBE_JRE_PROVISIONING_MIN_VERSION, UNARCHIVE_SUFFIX, } from './constants'; -import { getHttpAgents } from './http-agent'; +import { extractArchive, getCachedFileLocation } from './file'; import { log, LogLevel } from './logging'; -import { getProxyUrl } from './proxy'; import { fetch } from './request'; import { JREFullData, PlatformInfo, ScannerProperties, ScannerProperty } from './types'; -import { extractArchive, getCachedFileLocation } from './file'; const finished = promisify(stream.finished); @@ -62,12 +60,11 @@ export async function serverSupportsJREProvisioning( async function fetchLatestSupportedJRE(properties: ScannerProperties, platformInfo: PlatformInfo) { const serverUrl = properties[ScannerProperty.SonarHostUrl]; - const token = properties[ScannerProperty.SonarToken]; const jreInfoUrl = `${serverUrl}/api/v2/analysis/jres?os=${platformInfo.os}&arch=${platformInfo.arch}`; log(LogLevel.DEBUG, `Downloading JRE from: ${jreInfoUrl}`); - const { data } = await fetch(token, { url: jreInfoUrl }); + const { data } = await fetch({ url: jreInfoUrl }); log(LogLevel.DEBUG, 'file info: ', data); @@ -91,8 +88,6 @@ export async function handleJREProvisioning( latestJREData.filename + UNARCHIVE_SUFFIX, ); - const proxyUrl = getProxyUrl(properties); - if (cachedJRE) { log(LogLevel.INFO, 'Using Cached JRE'); properties[ScannerProperty.SonarScannerWasJRECacheHit] = 'true'; @@ -123,11 +118,10 @@ export async function handleJREProvisioning( const url = serverUrl + API_V2_JRE_ENDPOINT + `/${latestJREData.filename}`; log(LogLevel.DEBUG, `Downloading ${url} to ${archivePath}`); - const response = await fetch(token, { + const response = await fetch({ url, method: 'GET', responseType: 'stream', - ...getHttpAgents(proxyUrl), }); const totalLength = response.headers['content-length']; @@ -195,17 +189,14 @@ async function validateChecksum(filePath: string, expectedChecksum: string) { export async function fetchServerVersion( sonarHostUrl: string, - parameters: ScannerProperties, + properties: ScannerProperties, ): Promise { - const token = parameters[ScannerProperty.SonarToken]; - const proxyUrl = getProxyUrl(parameters); let version: SemVer | null = null; try { // Try and fetch the new version endpoint first log(LogLevel.DEBUG, `Fetching API V2 ${API_V2_VERSION_ENDPOINT}`); - const response = await fetch(token, { + const response = await fetch({ url: sonarHostUrl + API_V2_VERSION_ENDPOINT, - ...getHttpAgents(proxyUrl), }); version = semver.coerce(response.data); } catch (error: unknown) { @@ -215,9 +206,8 @@ export async function fetchServerVersion( LogLevel.DEBUG, `Unable to fetch API V2 ${API_V2_VERSION_ENDPOINT}: ${error}. Falling back on ${API_OLD_VERSION_ENDPOINT}`, ); - const response = await fetch(token, { + const response = await fetch({ url: sonarHostUrl + API_OLD_VERSION_ENDPOINT, - ...getHttpAgents(proxyUrl), }); version = semver.coerce(response.data); } catch (error: unknown) { diff --git a/src/request.ts b/src/request.ts index e523ec14..50262f23 100644 --- a/src/request.ts +++ b/src/request.ts @@ -17,13 +17,31 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import axios, { AxiosRequestConfig } from 'axios'; +import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'; +import { getHttpAgents } from './http-agent'; +import { ScannerProperties, ScannerProperty } from './types'; -export function fetch(token: string, config: AxiosRequestConfig) { - return axios({ - headers: { - Authorization: `Bearer ${token}`, - }, - ...config, - }); +// The axios instance is private to this module +let _axiosInstance: AxiosInstance | null = null; + +export function initializeAxios(properties: ScannerProperties) { + const token = properties[ScannerProperty.SonarToken]; + const agents = getHttpAgents(properties); + + if (!_axiosInstance) { + _axiosInstance = axios.create({ + headers: { + Authorization: `Bearer ${token}`, + }, + ...agents, + }); + } +} + +export function fetch(config: AxiosRequestConfig) { + if (!_axiosInstance) { + throw new Error('Axios instance is not initialized'); + } + + return _axiosInstance.request(config); } diff --git a/src/scan.ts b/src/scan.ts index 1c57f896..d7c5b41e 100644 --- a/src/scan.ts +++ b/src/scan.ts @@ -18,20 +18,18 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +import { version } from '../package.json'; import { handleJREProvisioning, serverSupportsJREProvisioning } from './java'; -import { log, LogLevel, setLogLevel } from './logging'; +import { LogLevel, log, setLogLevel } from './logging'; import { getPlatformInfo } from './platform'; import { getProperties } from './properties'; -import { ScannerProperty, JreMetaData, ScanOptions } from './types'; -import { version } from '../package.json'; +import { initializeAxios } from './request'; +import { JreMetaData, ScanOptions, ScannerProperty } from './types'; export async function scan(scanOptions: ScanOptions, cliArgs?: string[]) { const startTimestampMs = Date.now(); const properties = getProperties(scanOptions, startTimestampMs, cliArgs); - const serverUrl = properties[ScannerProperty.SonarHostUrl]; - const explicitJREPathOverride = properties[ScannerProperty.SonarScannerJavaExePath]; - if (properties[ScannerProperty.SonarVerbose] === 'true') { setLogLevel(LogLevel.DEBUG); log(LogLevel.DEBUG, 'Setting the log level to DEBUG due to verbose mode'); @@ -42,6 +40,11 @@ export async function scan(scanOptions: ScanOptions, cliArgs?: string[]) { log(LogLevel.DEBUG, `Overriding the log level to ${properties[ScannerProperty.SonarLogLevel]}`); } + initializeAxios(properties); + + const serverUrl = properties[ScannerProperty.SonarHostUrl]; + const explicitJREPathOverride = properties[ScannerProperty.SonarScannerJavaExePath]; + log(LogLevel.INFO, 'Version: ', version); log(LogLevel.DEBUG, 'Finding platform info'); diff --git a/test/unit/http-agent.test.ts b/test/unit/http-agent.test.ts index 6a279fb4..184e3986 100644 --- a/test/unit/http-agent.test.ts +++ b/test/unit/http-agent.test.ts @@ -20,20 +20,24 @@ import { HttpProxyAgent, HttpsProxyAgent } from 'hpagent'; import { getHttpAgents } from '../../src/http-agent'; +import { ScannerProperty } from '../../src/types'; describe('http-agent', () => { it('should define proxy url correctly', () => { - const proxyUrl = new URL('http://proxy.com'); - - const agents = getHttpAgents(proxyUrl); + const agents = getHttpAgents({ + [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io', + [ScannerProperty.SonarScannerProxyHost]: 'proxy.com', + }); expect(agents.httpAgent).toBeInstanceOf(HttpProxyAgent); - expect(agents.httpAgent?.proxy.toString()).toBe(proxyUrl.toString()); + expect(agents.httpAgent?.proxy.toString()).toBe('https://proxy.com/'); expect(agents.httpsAgent).toBeInstanceOf(HttpsProxyAgent); - expect(agents.httpsAgent?.proxy.toString()).toBe(proxyUrl.toString()); + expect(agents.httpsAgent?.proxy.toString()).toBe('https://proxy.com/'); }); it('should not define agents when no proxy is provided', () => { - const agents = getHttpAgents(); + const agents = getHttpAgents({ + [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io', + }); expect(agents.httpAgent).toBeUndefined(); expect(agents.httpsAgent).toBeUndefined(); expect(agents).toEqual({}); diff --git a/test/unit/request.test.ts b/test/unit/request.test.ts new file mode 100644 index 00000000..9869172e --- /dev/null +++ b/test/unit/request.test.ts @@ -0,0 +1,75 @@ +/* + * sonar-scanner-npm + * Copyright (C) 2022-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import axios from 'axios'; +import { fetch, initializeAxios } from '../../src/request'; +import { ScannerProperties, ScannerProperty } from '../../src/types'; + +jest.mock('axios', () => ({ + create: jest.fn(), +})); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe('request', () => { + it('should initialize axios', () => { + jest.spyOn(axios, 'create'); + + const properties: ScannerProperties = { + [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io', + [ScannerProperty.SonarToken]: 'testToken', + }; + + initializeAxios(properties); + + expect(axios.create).toHaveBeenCalledWith({ + headers: { + Authorization: `Bearer testToken`, + }, + }); + }); + + it('should throw error if axios is not initialized', () => { + expect(() => fetch({})).toThrow('Axios instance is not initialized'); + }); + + it('should call axios request if axios is initialized', () => { + const mockedRequest = jest.fn(); + jest.spyOn(axios, 'create').mockImplementation( + () => + ({ + request: mockedRequest, + }) as any, + ); + + const properties: ScannerProperties = { + [ScannerProperty.SonarHostUrl]: 'https://sonarcloud.io', + [ScannerProperty.SonarToken]: 'testToken', + }; + + initializeAxios(properties); + + const config = { url: 'https://sonarcloud.io/api/issues/search' }; + + fetch(config); + expect(mockedRequest).toHaveBeenCalledWith(config); + }); +});