Skip to content

Commit 1c61377

Browse files
committed
feat: support REST APIs
1 parent fd8d0da commit 1c61377

File tree

5 files changed

+251
-5
lines changed

5 files changed

+251
-5
lines changed

README.md

+10-1
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,15 @@ const requestManager = new requestsManager({burstSize: 20, period: 100, maxRetry
167167
const requestManager = new requestsManager({snykToken:'21346-1234-1234-1234')
168168
```
169169

170+
171+
#### Customize to use REST api endpoint
172+
173+
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.
174+
```
175+
const res = await requestManager.request({verb: "GET", url: '/url', useRESTApi: true})
176+
```
177+
178+
170179
#### Customize snyk token and queue|intervals|retries
171180

172181
```
@@ -176,4 +185,4 @@ const requestManager = new requestsManager({snykToken:'21346-1234-1234-1234', bu
176185

177186

178187
### Notes
179-
Axios is temporarily pinned to 0.21.4 as minor versions above contain breaking change.
188+
Axios is temporarily pinned to 0.21.4 as minor versions above contain breaking change.

src/lib/request/request.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@ import * as Error from '../customErrors/apiError';
66
import 'global-agent/bootstrap';
77

88
const DEFAULT_API = 'https://snyk.io/api/v1';
9-
9+
const DEFAULT_REST_API = 'https://api.snyk.io/rest/';
1010
interface SnykRequest {
1111
verb: string;
1212
url: string;
1313
body?: string;
1414
headers?: Record<string, any>;
1515
requestId?: string;
16+
useRESTApi?: boolean;
1617
}
1718

1819
const getTopParentModuleName = (parent: NodeModule | null): string => {
@@ -31,6 +32,7 @@ const makeSnykRequest = async (
3132
request: SnykRequest,
3233
snykToken = '',
3334
apiUrl = DEFAULT_API,
35+
apiUrlREST = DEFAULT_REST_API,
3436
userAgentPrefix = '',
3537
): Promise<AxiosResponse<any>> => {
3638
const topParentModuleName = getTopParentModuleName(module.parent as any);
@@ -45,7 +47,7 @@ const makeSnykRequest = async (
4547
};
4648

4749
const apiClient = axios.create({
48-
baseURL: apiUrl,
50+
baseURL: request.useRESTApi ? apiUrlREST : apiUrl,
4951
responseType: 'json',
5052
headers: { ...requestHeaders, ...request.headers },
5153
});

src/lib/request/requestManager.ts

+10
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const Configstore = require('@snyk/configstore');
44
import { LeakyBucketQueue } from 'leaky-bucket-queue';
55
import { SnykRequest, makeSnykRequest, DEFAULT_API } from './request';
66
import { v4 as uuidv4 } from 'uuid';
7+
import { URL } from 'url';
78
import * as requestsManagerError from '../customErrors/requestManagerErrors';
89

910
interface QueuedRequest {
@@ -34,6 +35,12 @@ interface RequestsManagerParams {
3435
userAgentPrefix?: string;
3536
}
3637

38+
function getRESTAPI(endpoint: string): string {
39+
const apiData = new URL(endpoint);
40+
// e.g 'https://api.snyk.io/rest/'
41+
return new URL(`${apiData.protocol}//api.${apiData.host}/rest`).toString();
42+
}
43+
3744
const getConfig = (): { endpoint: string; token: string } => {
3845
const snykApiEndpoint: string =
3946
process.env.SNYK_API ||
@@ -53,6 +60,7 @@ class RequestsManager {
5360
token: string;
5461
}; // loaded user config from configstore
5562
_apiUrl: string;
63+
_apiUrlREST: string;
5664
_retryCounter: Map<string, number>;
5765
_MAX_RETRY_COUNT: number;
5866
_snykToken: string;
@@ -71,6 +79,7 @@ class RequestsManager {
7179
this._MAX_RETRY_COUNT = params?.maxRetryCount || 5;
7280
this._snykToken = params?.snykToken ?? this._userConfig.token;
7381
this._apiUrl = this._userConfig.endpoint;
82+
this._apiUrlREST = getRESTAPI(this._userConfig.endpoint);
7483
this._userAgentPrefix = params?.userAgentPrefix;
7584
}
7685

@@ -91,6 +100,7 @@ class RequestsManager {
91100
request.snykRequest,
92101
this._snykToken,
93102
this._apiUrl,
103+
this._apiUrlREST,
94104
this._userAgentPrefix,
95105
);
96106
this._emit({

test/lib/request/request.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ describe('Test Snyk Utils make request properly', () => {
7979
.toString(),
8080
);
8181

82-
expect(_.isEqual(response.data, fixturesJSON)).toBeTruthy();
82+
expect(response.data).toEqual(fixturesJSON);
8383
});
8484
it('Test POST command on /', async () => {
8585
const bodyToSend = {
@@ -93,7 +93,7 @@ describe('Test Snyk Utils make request properly', () => {
9393
},
9494
'token123',
9595
);
96-
expect(_.isEqual(response.data, bodyToSend)).toBeTruthy();
96+
expect(response.data).toEqual(bodyToSend);
9797
});
9898
});
9999

test/lib/request/rest-request.test.ts

+225
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
import { makeSnykRequest } from '../../../src/lib/request/request';
2+
import * as fs from 'fs';
3+
import * as nock from 'nock';
4+
import * as _ from 'lodash';
5+
import * as path from 'path';
6+
import {
7+
NotFoundError,
8+
ApiError,
9+
ApiAuthenticationError,
10+
GenericError,
11+
} from '../../../src/lib/customErrors/apiError';
12+
13+
const fixturesFolderPath = path.resolve(__dirname, '../..') + '/fixtures/';
14+
beforeEach(() => {
15+
return nock('https://api.snyk.io')
16+
.persist()
17+
.get(/\/xyz/)
18+
.reply(404, '404')
19+
.get(/\/customtoken/)
20+
.reply(200, function() {
21+
return this.req.headers.authorization;
22+
})
23+
.post(/\/xyz/)
24+
.reply(404, '404')
25+
.get(/\/apierror/)
26+
.reply(500, '500')
27+
.post(/\/apierror/)
28+
.reply(500, '500')
29+
.get(/\/genericerror/)
30+
.reply(512, '512')
31+
.post(/\/genericerror/)
32+
.reply(512, '512')
33+
.get(/\/apiautherror/)
34+
.reply(401, '401')
35+
.post(/\/apiautherror/)
36+
.reply(401, '401')
37+
.post(/^(?!.*xyz).*$/)
38+
.reply(200, (uri, requestBody) => {
39+
switch (uri) {
40+
case '/rest/':
41+
return requestBody;
42+
break;
43+
default:
44+
}
45+
})
46+
.get(/^(?!.*xyz).*$/)
47+
.reply(200, (uri) => {
48+
switch (uri) {
49+
case '/rest/':
50+
return fs.readFileSync(
51+
fixturesFolderPath + 'apiResponses/general-doc.json',
52+
);
53+
break;
54+
default:
55+
}
56+
});
57+
});
58+
59+
const OLD_ENV = process.env;
60+
beforeEach(() => {
61+
jest.resetModules(); // this is important - it clears the cache
62+
process.env = { ...OLD_ENV };
63+
delete process.env.SNYK_TOKEN;
64+
});
65+
66+
afterEach(() => {
67+
process.env = OLD_ENV;
68+
});
69+
70+
describe('Test Snyk Utils make request properly', () => {
71+
it('Test GET command on /', async () => {
72+
const response = await makeSnykRequest(
73+
{ verb: 'GET', url: '/', useRESTApi: true },
74+
'token123',
75+
);
76+
const fixturesJSON = JSON.parse(
77+
fs
78+
.readFileSync(fixturesFolderPath + 'apiResponses/general-doc.json')
79+
.toString(),
80+
);
81+
expect(response.data).toEqual(fixturesJSON);
82+
});
83+
it('Test POST command on /', async () => {
84+
const bodyToSend = {
85+
testbody: {},
86+
};
87+
const response = await makeSnykRequest(
88+
{
89+
verb: 'POST',
90+
url: '/',
91+
body: JSON.stringify(bodyToSend),
92+
useRESTApi: true,
93+
},
94+
'token123',
95+
);
96+
expect(response.data).toEqual(bodyToSend);
97+
});
98+
});
99+
100+
describe('Test Snyk Utils error handling/classification', () => {
101+
it('Test NotFoundError on GET command', async () => {
102+
try {
103+
await makeSnykRequest(
104+
{ verb: 'GET', url: '/xyz', body: '', useRESTApi: true },
105+
'token123',
106+
);
107+
} catch (err) {
108+
expect(err.data).toEqual(404);
109+
expect(err).toBeInstanceOf(NotFoundError);
110+
}
111+
});
112+
113+
it('Test NotFoundError on POST command', async () => {
114+
try {
115+
const bodyToSend = {
116+
testbody: {},
117+
};
118+
await makeSnykRequest(
119+
{
120+
verb: 'POST',
121+
url: '/xyz',
122+
body: JSON.stringify(bodyToSend),
123+
useRESTApi: true,
124+
},
125+
'token123',
126+
);
127+
} catch (err) {
128+
expect(err.data).toEqual(404);
129+
expect(err).toBeInstanceOf(NotFoundError);
130+
}
131+
});
132+
133+
it('Test ApiError on GET command', async () => {
134+
try {
135+
await makeSnykRequest(
136+
{ verb: 'GET', url: '/apierror', useRESTApi: true },
137+
'token123',
138+
);
139+
} catch (err) {
140+
expect(err.data).toEqual(500);
141+
expect(err).toBeInstanceOf(ApiError);
142+
}
143+
});
144+
it('Test ApiError on POST command', async () => {
145+
try {
146+
const bodyToSend = {
147+
testbody: {},
148+
};
149+
await makeSnykRequest(
150+
{
151+
verb: 'POST',
152+
url: '/apierror',
153+
body: JSON.stringify(bodyToSend),
154+
useRESTApi: true,
155+
},
156+
'token123',
157+
);
158+
} catch (err) {
159+
expect(err.data).toEqual(500);
160+
expect(err).toBeInstanceOf(ApiError);
161+
}
162+
});
163+
164+
it('Test ApiAuthenticationError on GET command', async () => {
165+
try {
166+
await makeSnykRequest(
167+
{ verb: 'GET', url: '/apiautherror', useRESTApi: true },
168+
'token123',
169+
);
170+
} catch (err) {
171+
expect(err.data).toEqual(401);
172+
expect(err).toBeInstanceOf(ApiAuthenticationError);
173+
}
174+
});
175+
it('Test ApiAuthenticationError on POST command', async () => {
176+
try {
177+
const bodyToSend = {
178+
testbody: {},
179+
};
180+
await makeSnykRequest(
181+
{
182+
verb: 'POST',
183+
url: '/apiautherror',
184+
body: JSON.stringify(bodyToSend),
185+
useRESTApi: true,
186+
},
187+
'token123',
188+
);
189+
} catch (err) {
190+
expect(err.data).toEqual(401);
191+
expect(err).toBeInstanceOf(ApiAuthenticationError);
192+
}
193+
});
194+
195+
it('Test GenericError on GET command', async () => {
196+
try {
197+
await makeSnykRequest(
198+
{ verb: 'GET', url: '/genericerror', useRESTApi: true },
199+
'token123',
200+
);
201+
} catch (err) {
202+
expect(err.data).toEqual(512);
203+
expect(err).toBeInstanceOf(GenericError);
204+
}
205+
});
206+
it('Test GenericError on POST command', async () => {
207+
try {
208+
const bodyToSend = {
209+
testbody: {},
210+
};
211+
await makeSnykRequest(
212+
{
213+
verb: 'POST',
214+
url: '/genericerror',
215+
body: JSON.stringify(bodyToSend),
216+
useRESTApi: true,
217+
},
218+
'token123',
219+
);
220+
} catch (err) {
221+
expect(err.data).toEqual(512);
222+
expect(err).toBeInstanceOf(GenericError);
223+
}
224+
});
225+
});

0 commit comments

Comments
 (0)