Skip to content

Commit

Permalink
SCANNPM-2 Use axios instance to re-use fetching logic
Browse files Browse the repository at this point in the history
  • Loading branch information
7PH committed Apr 22, 2024
1 parent f8b76dd commit abe0633
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 37 deletions.
5 changes: 4 additions & 1 deletion src/http-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AxiosRequestConfig, 'httpAgent' | 'httpsAgent'> {
const agents: Pick<AxiosRequestConfig, 'httpAgent' | 'httpsAgent'> = {};
const proxyUrl = getProxyUrl(properties);

if (proxyUrl) {
agents.httpsAgent = new HttpsProxyAgent({ proxy: proxyUrl.toString() });
Expand Down
22 changes: 6 additions & 16 deletions src/java.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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);

Expand All @@ -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';
Expand Down Expand Up @@ -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'];
Expand Down Expand Up @@ -195,17 +189,14 @@ async function validateChecksum(filePath: string, expectedChecksum: string) {

export async function fetchServerVersion(
sonarHostUrl: string,
parameters: ScannerProperties,
properties: ScannerProperties,
): Promise<SemVer> {
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) {
Expand All @@ -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) {
Expand Down
34 changes: 26 additions & 8 deletions src/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
15 changes: 9 additions & 6 deletions src/scan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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');
Expand Down
16 changes: 10 additions & 6 deletions test/unit/http-agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({});
Expand Down
75 changes: 75 additions & 0 deletions test/unit/request.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});

0 comments on commit abe0633

Please sign in to comment.