From 1c61377800bf0fb9e7472ba711839d6d1f4fc762 Mon Sep 17 00:00:00 2001 From: ghe Date: Thu, 30 Jun 2022 15:21:43 +0100 Subject: [PATCH] feat: support REST APIs --- README.md | 11 +- src/lib/request/request.ts | 6 +- src/lib/request/requestManager.ts | 10 ++ test/lib/request/request.test.ts | 4 +- test/lib/request/rest-request.test.ts | 225 ++++++++++++++++++++++++++ 5 files changed, 251 insertions(+), 5 deletions(-) create mode 100644 test/lib/request/rest-request.test.ts diff --git a/README.md b/README.md index 6a01720..155146f 100644 --- a/README.md +++ b/README.md @@ -167,6 +167,15 @@ const requestManager = new requestsManager({burstSize: 20, period: 100, maxRetry const requestManager = new requestsManager({snykToken:'21346-1234-1234-1234') ``` + +#### Customize to use REST api endpoint + +Each request can be opted in to use the new REST Snyk API, which defaults to 'https://api.snyk.io/rest/' and is automatically calculated from the `SNYK_API` or `endpoint` configuration by reusing the same host. +``` +const res = await requestManager.request({verb: "GET", url: '/url', useRESTApi: true}) +``` + + #### Customize snyk token and queue|intervals|retries ``` @@ -176,4 +185,4 @@ const requestManager = new requestsManager({snykToken:'21346-1234-1234-1234', bu ### Notes -Axios is temporarily pinned to 0.21.4 as minor versions above contain breaking change. \ No newline at end of file +Axios is temporarily pinned to 0.21.4 as minor versions above contain breaking change. diff --git a/src/lib/request/request.ts b/src/lib/request/request.ts index 489b20b..4429f69 100644 --- a/src/lib/request/request.ts +++ b/src/lib/request/request.ts @@ -6,13 +6,14 @@ import * as Error from '../customErrors/apiError'; import 'global-agent/bootstrap'; const DEFAULT_API = 'https://snyk.io/api/v1'; - +const DEFAULT_REST_API = 'https://api.snyk.io/rest/'; interface SnykRequest { verb: string; url: string; body?: string; headers?: Record; requestId?: string; + useRESTApi?: boolean; } const getTopParentModuleName = (parent: NodeModule | null): string => { @@ -31,6 +32,7 @@ const makeSnykRequest = async ( request: SnykRequest, snykToken = '', apiUrl = DEFAULT_API, + apiUrlREST = DEFAULT_REST_API, userAgentPrefix = '', ): Promise> => { const topParentModuleName = getTopParentModuleName(module.parent as any); @@ -45,7 +47,7 @@ const makeSnykRequest = async ( }; const apiClient = axios.create({ - baseURL: apiUrl, + baseURL: request.useRESTApi ? apiUrlREST : apiUrl, responseType: 'json', headers: { ...requestHeaders, ...request.headers }, }); diff --git a/src/lib/request/requestManager.ts b/src/lib/request/requestManager.ts index 982295b..fd1c591 100644 --- a/src/lib/request/requestManager.ts +++ b/src/lib/request/requestManager.ts @@ -4,6 +4,7 @@ const Configstore = require('@snyk/configstore'); import { LeakyBucketQueue } from 'leaky-bucket-queue'; import { SnykRequest, makeSnykRequest, DEFAULT_API } from './request'; import { v4 as uuidv4 } from 'uuid'; +import { URL } from 'url'; import * as requestsManagerError from '../customErrors/requestManagerErrors'; interface QueuedRequest { @@ -34,6 +35,12 @@ interface RequestsManagerParams { userAgentPrefix?: string; } +function getRESTAPI(endpoint: string): string { + const apiData = new URL(endpoint); + // e.g 'https://api.snyk.io/rest/' + return new URL(`${apiData.protocol}//api.${apiData.host}/rest`).toString(); +} + const getConfig = (): { endpoint: string; token: string } => { const snykApiEndpoint: string = process.env.SNYK_API || @@ -53,6 +60,7 @@ class RequestsManager { token: string; }; // loaded user config from configstore _apiUrl: string; + _apiUrlREST: string; _retryCounter: Map; _MAX_RETRY_COUNT: number; _snykToken: string; @@ -71,6 +79,7 @@ class RequestsManager { this._MAX_RETRY_COUNT = params?.maxRetryCount || 5; this._snykToken = params?.snykToken ?? this._userConfig.token; this._apiUrl = this._userConfig.endpoint; + this._apiUrlREST = getRESTAPI(this._userConfig.endpoint); this._userAgentPrefix = params?.userAgentPrefix; } @@ -91,6 +100,7 @@ class RequestsManager { request.snykRequest, this._snykToken, this._apiUrl, + this._apiUrlREST, this._userAgentPrefix, ); this._emit({ diff --git a/test/lib/request/request.test.ts b/test/lib/request/request.test.ts index 517926b..2796d12 100644 --- a/test/lib/request/request.test.ts +++ b/test/lib/request/request.test.ts @@ -79,7 +79,7 @@ describe('Test Snyk Utils make request properly', () => { .toString(), ); - expect(_.isEqual(response.data, fixturesJSON)).toBeTruthy(); + expect(response.data).toEqual(fixturesJSON); }); it('Test POST command on /', async () => { const bodyToSend = { @@ -93,7 +93,7 @@ describe('Test Snyk Utils make request properly', () => { }, 'token123', ); - expect(_.isEqual(response.data, bodyToSend)).toBeTruthy(); + expect(response.data).toEqual(bodyToSend); }); }); diff --git a/test/lib/request/rest-request.test.ts b/test/lib/request/rest-request.test.ts new file mode 100644 index 0000000..6902882 --- /dev/null +++ b/test/lib/request/rest-request.test.ts @@ -0,0 +1,225 @@ +import { makeSnykRequest } from '../../../src/lib/request/request'; +import * as fs from 'fs'; +import * as nock from 'nock'; +import * as _ from 'lodash'; +import * as path from 'path'; +import { + NotFoundError, + ApiError, + ApiAuthenticationError, + GenericError, +} from '../../../src/lib/customErrors/apiError'; + +const fixturesFolderPath = path.resolve(__dirname, '../..') + '/fixtures/'; +beforeEach(() => { + return nock('https://api.snyk.io') + .persist() + .get(/\/xyz/) + .reply(404, '404') + .get(/\/customtoken/) + .reply(200, function() { + return this.req.headers.authorization; + }) + .post(/\/xyz/) + .reply(404, '404') + .get(/\/apierror/) + .reply(500, '500') + .post(/\/apierror/) + .reply(500, '500') + .get(/\/genericerror/) + .reply(512, '512') + .post(/\/genericerror/) + .reply(512, '512') + .get(/\/apiautherror/) + .reply(401, '401') + .post(/\/apiautherror/) + .reply(401, '401') + .post(/^(?!.*xyz).*$/) + .reply(200, (uri, requestBody) => { + switch (uri) { + case '/rest/': + return requestBody; + break; + default: + } + }) + .get(/^(?!.*xyz).*$/) + .reply(200, (uri) => { + switch (uri) { + case '/rest/': + return fs.readFileSync( + fixturesFolderPath + 'apiResponses/general-doc.json', + ); + break; + default: + } + }); +}); + +const OLD_ENV = process.env; +beforeEach(() => { + jest.resetModules(); // this is important - it clears the cache + process.env = { ...OLD_ENV }; + delete process.env.SNYK_TOKEN; +}); + +afterEach(() => { + process.env = OLD_ENV; +}); + +describe('Test Snyk Utils make request properly', () => { + it('Test GET command on /', async () => { + const response = await makeSnykRequest( + { verb: 'GET', url: '/', useRESTApi: true }, + 'token123', + ); + const fixturesJSON = JSON.parse( + fs + .readFileSync(fixturesFolderPath + 'apiResponses/general-doc.json') + .toString(), + ); + expect(response.data).toEqual(fixturesJSON); + }); + it('Test POST command on /', async () => { + const bodyToSend = { + testbody: {}, + }; + const response = await makeSnykRequest( + { + verb: 'POST', + url: '/', + body: JSON.stringify(bodyToSend), + useRESTApi: true, + }, + 'token123', + ); + expect(response.data).toEqual(bodyToSend); + }); +}); + +describe('Test Snyk Utils error handling/classification', () => { + it('Test NotFoundError on GET command', async () => { + try { + await makeSnykRequest( + { verb: 'GET', url: '/xyz', body: '', useRESTApi: true }, + 'token123', + ); + } catch (err) { + expect(err.data).toEqual(404); + expect(err).toBeInstanceOf(NotFoundError); + } + }); + + it('Test NotFoundError on POST command', async () => { + try { + const bodyToSend = { + testbody: {}, + }; + await makeSnykRequest( + { + verb: 'POST', + url: '/xyz', + body: JSON.stringify(bodyToSend), + useRESTApi: true, + }, + 'token123', + ); + } catch (err) { + expect(err.data).toEqual(404); + expect(err).toBeInstanceOf(NotFoundError); + } + }); + + it('Test ApiError on GET command', async () => { + try { + await makeSnykRequest( + { verb: 'GET', url: '/apierror', useRESTApi: true }, + 'token123', + ); + } catch (err) { + expect(err.data).toEqual(500); + expect(err).toBeInstanceOf(ApiError); + } + }); + it('Test ApiError on POST command', async () => { + try { + const bodyToSend = { + testbody: {}, + }; + await makeSnykRequest( + { + verb: 'POST', + url: '/apierror', + body: JSON.stringify(bodyToSend), + useRESTApi: true, + }, + 'token123', + ); + } catch (err) { + expect(err.data).toEqual(500); + expect(err).toBeInstanceOf(ApiError); + } + }); + + it('Test ApiAuthenticationError on GET command', async () => { + try { + await makeSnykRequest( + { verb: 'GET', url: '/apiautherror', useRESTApi: true }, + 'token123', + ); + } catch (err) { + expect(err.data).toEqual(401); + expect(err).toBeInstanceOf(ApiAuthenticationError); + } + }); + it('Test ApiAuthenticationError on POST command', async () => { + try { + const bodyToSend = { + testbody: {}, + }; + await makeSnykRequest( + { + verb: 'POST', + url: '/apiautherror', + body: JSON.stringify(bodyToSend), + useRESTApi: true, + }, + 'token123', + ); + } catch (err) { + expect(err.data).toEqual(401); + expect(err).toBeInstanceOf(ApiAuthenticationError); + } + }); + + it('Test GenericError on GET command', async () => { + try { + await makeSnykRequest( + { verb: 'GET', url: '/genericerror', useRESTApi: true }, + 'token123', + ); + } catch (err) { + expect(err.data).toEqual(512); + expect(err).toBeInstanceOf(GenericError); + } + }); + it('Test GenericError on POST command', async () => { + try { + const bodyToSend = { + testbody: {}, + }; + await makeSnykRequest( + { + verb: 'POST', + url: '/genericerror', + body: JSON.stringify(bodyToSend), + useRESTApi: true, + }, + 'token123', + ); + } catch (err) { + expect(err.data).toEqual(512); + expect(err).toBeInstanceOf(GenericError); + } + }); +});