Skip to content

Commit

Permalink
feat: support REST APIs
Browse files Browse the repository at this point in the history
  • Loading branch information
lili2311 committed Jun 30, 2022
1 parent fd8d0da commit 1c61377
Show file tree
Hide file tree
Showing 5 changed files with 251 additions and 5 deletions.
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

```
Expand All @@ -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.
Axios is temporarily pinned to 0.21.4 as minor versions above contain breaking change.
6 changes: 4 additions & 2 deletions src/lib/request/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any>;
requestId?: string;
useRESTApi?: boolean;
}

const getTopParentModuleName = (parent: NodeModule | null): string => {
Expand All @@ -31,6 +32,7 @@ const makeSnykRequest = async (
request: SnykRequest,
snykToken = '',
apiUrl = DEFAULT_API,
apiUrlREST = DEFAULT_REST_API,
userAgentPrefix = '',
): Promise<AxiosResponse<any>> => {
const topParentModuleName = getTopParentModuleName(module.parent as any);
Expand All @@ -45,7 +47,7 @@ const makeSnykRequest = async (
};

const apiClient = axios.create({
baseURL: apiUrl,
baseURL: request.useRESTApi ? apiUrlREST : apiUrl,
responseType: 'json',
headers: { ...requestHeaders, ...request.headers },
});
Expand Down
10 changes: 10 additions & 0 deletions src/lib/request/requestManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 ||
Expand All @@ -53,6 +60,7 @@ class RequestsManager {
token: string;
}; // loaded user config from configstore
_apiUrl: string;
_apiUrlREST: string;
_retryCounter: Map<string, number>;
_MAX_RETRY_COUNT: number;
_snykToken: string;
Expand All @@ -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;
}

Expand All @@ -91,6 +100,7 @@ class RequestsManager {
request.snykRequest,
this._snykToken,
this._apiUrl,
this._apiUrlREST,
this._userAgentPrefix,
);
this._emit({
Expand Down
4 changes: 2 additions & 2 deletions test/lib/request/request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -93,7 +93,7 @@ describe('Test Snyk Utils make request properly', () => {
},
'token123',
);
expect(_.isEqual(response.data, bodyToSend)).toBeTruthy();
expect(response.data).toEqual(bodyToSend);
});
});

Expand Down
225 changes: 225 additions & 0 deletions test/lib/request/rest-request.test.ts
Original file line number Diff line number Diff line change
@@ -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);
}
});
});

0 comments on commit 1c61377

Please sign in to comment.