UNPKG

17.6 kBJavaScriptView Raw
1'use strict';
2
3const assert = require('chai').assert;
4const nock = require('nock');
5const sinon = require('sinon');
6
7const HttpTransport = require('..');
8const Transport = require('../lib/transport/transport');
9const toJson = require('../lib/middleware/asJson');
10const setContextProperty = require('../lib/middleware/setContextProperty');
11const log = require('../lib/middleware/logger');
12const packageInfo = require('../package');
13const toError = require('./toError');
14const sandbox = sinon.sandbox.create();
15
16const url = 'http://www.example.com/';
17const host = 'http://www.example.com';
18const api = nock(host);
19const path = '/';
20
21const simpleResponseBody = { blobbus: 'Illegitimi non carborundum' };
22const requestBody = {
23 foo: 'bar'
24};
25const responseBody = requestBody;
26const defaultHeaders = { 'Content-Type': 'application/json' };
27
28function toUpperCase() {
29 return async (ctx, next) => {
30 await next();
31 ctx.res.body.blobbus = ctx.res.body.blobbus.toUpperCase();
32 };
33}
34
35function nockRetries(retry, opts) {
36 const httpMethod = opts?.httpMethod || 'get';
37 const successCode = opts?.successCode || 200;
38
39 nock.cleanAll();
40 api[httpMethod](path)
41 .times(retry)
42 .reply(500);
43 api[httpMethod](path).reply(successCode, simpleResponseBody, defaultHeaders);
44}
45
46function nockTimeouts(number, opts) {
47 const httpMethod = opts?.httpMethod || 'get';
48 const successCode = opts?.successCode || 200;
49
50 nock.cleanAll();
51 api[httpMethod](path)
52 .times(number)
53 .delay(10000)
54 .reply(200);
55 api[httpMethod](path).reply(successCode, simpleResponseBody, defaultHeaders);
56}
57
58describe('HttpTransportClient', () => {
59 beforeEach(() => {
60 nock.disableNetConnect();
61 nock.cleanAll();
62 api.get(path).reply(200, simpleResponseBody, defaultHeaders);
63 });
64
65 afterEach(() => {
66 sandbox.restore();
67 });
68
69 describe('.get', () => {
70 it('returns a response', async () => {
71 const res = await HttpTransport.createClient()
72 .get(url)
73 .asResponse();
74
75 assert.deepEqual(res.body, simpleResponseBody);
76 });
77
78 it('handles an empty response body when parsing as JSON', async () => {
79 nock.cleanAll();
80 api.get(path).reply(200, undefined, defaultHeaders);
81
82 const res = await HttpTransport.createClient()
83 .get(url)
84 .asResponse();
85
86 assert.deepEqual(res.body, '');
87 });
88
89 it('sets a default User-agent for every request', async () => {
90 nock.cleanAll();
91
92 nock(host, {
93 reqheaders: {
94 'User-Agent': `${packageInfo.name}/${packageInfo.version}`
95 }
96 })
97 .get(path)
98 .times(2)
99 .reply(200, responseBody);
100
101 const client = HttpTransport.createClient();
102 await client.get(url).asResponse();
103 });
104
105 it('overrides the default User-agent for every request', async () => {
106 nock.cleanAll();
107
108 nock(host, {
109 reqheaders: {
110 'User-Agent': 'some-new-user-agent'
111 }
112 })
113 .get(path)
114 .times(2)
115 .reply(200, responseBody);
116
117 const client = HttpTransport.createBuilder()
118 .userAgent('some-new-user-agent')
119 .createClient();
120
121 await client.get(url).asResponse();
122 });
123 });
124
125 describe('default', () => {
126 it('sets default retry values in the context', async () => {
127 const transport = new Transport();
128 sandbox.stub(transport, 'execute').returns(Promise.resolve());
129
130 const client = HttpTransport.createBuilder(transport)
131 .retries(50)
132 .retryDelay(2000)
133 .createClient();
134
135 await client
136 .get(url)
137 .asResponse();
138
139 const ctx = transport.execute.getCall(0).args[0];
140 assert.equal(ctx.retries, 50);
141 assert.equal(ctx.retryDelay, 2000);
142 });
143 });
144
145 describe('.retries', () => {
146 it('retries a given number of times for failed requests', async () => {
147 nockRetries(2);
148
149 const client = HttpTransport.createBuilder()
150 .use(toError())
151 .createClient();
152
153 const res = await client
154 .get(url)
155 .retry(2)
156 .asResponse();
157
158 assert.equal(res.statusCode, 200);
159 });
160
161 it('retries a given number of times for requests that timed out', async () => {
162 nockTimeouts(2);
163
164 const client = HttpTransport.createBuilder()
165 .use(toError())
166 .createClient();
167
168 const res = await client
169 .get(url)
170 .timeout(500)
171 .retry(2)
172 .asResponse();
173
174 assert.equal(res.statusCode, 200);
175 });
176
177 it('waits a minimum of 100ms between retries by default', async () => {
178 nockRetries(1);
179 const startTime = Date.now();
180
181 const client = HttpTransport.createBuilder()
182 .use(toError())
183 .createClient();
184
185 const res = await client
186 .get(url)
187 .retry(2)
188 .asResponse();
189
190 const timeTaken = Date.now() - startTime;
191 assert(timeTaken > 100);
192 assert.equal(res.statusCode, 200);
193 });
194
195 it('disables retryDelay if retries if set to zero', async () => {
196 nock.cleanAll();
197 api.get(path).reply(500);
198
199 const client = HttpTransport.createBuilder()
200 .use(toError())
201 .createClient();
202
203 try {
204 await client
205 .get(url)
206 .retry(0)
207 .retryDelay(10000)
208 .asResponse();
209 } catch (e) {
210 return assert.equal(e.message, 'something bad happened.');
211 }
212
213 assert.fail('Should have thrown');
214 });
215
216 it('overrides the minimum wait time between retries', async () => {
217 nockRetries(1);
218 const retryDelay = 200;
219 const startTime = Date.now();
220
221 const client = HttpTransport.createBuilder()
222 .use(toError())
223 .createClient();
224
225 const res = await client
226 .get(url)
227 .retry(1)
228 .retryDelay(retryDelay)
229 .asResponse();
230
231 const timeTaken = Date.now() - startTime;
232 assert(timeTaken > retryDelay);
233 assert.equal(res.statusCode, 200);
234 });
235
236 it('does not retry 4XX errors', async () => {
237 nock.cleanAll();
238 api
239 .get(path)
240 .once()
241 .reply(400);
242
243 const client = HttpTransport.createBuilder()
244 .use(toError())
245 .createClient();
246
247 try {
248 await client
249 .get(url)
250 .retry(1)
251 .asResponse();
252 } catch (err) {
253 return assert.equal(err.statusCode, 400);
254 }
255 assert.fail('Should have thrown');
256 });
257 });
258
259 describe('.post', () => {
260 it('makes a POST request', async () => {
261 api.post(path, requestBody).reply(201, responseBody, { 'content-type': 'application/json' });
262
263 const body = await HttpTransport.createClient()
264 .post(url, requestBody)
265 .asBody();
266
267 assert.deepEqual(body, responseBody);
268 });
269
270 it('returns an error when the API returns a 5XX statusCode code', async () => {
271 api.post(path, requestBody).reply(500);
272
273 try {
274 await HttpTransport.createClient()
275 .use(toError())
276 .post(url, requestBody)
277 .asResponse();
278 } catch (err) {
279 return assert.equal(err.statusCode, 500);
280 }
281
282 assert.fail('Should have thrown');
283 });
284 });
285
286 describe('.put', () => {
287 it('makes a PUT request with a JSON body', async () => {
288 api.put(path, requestBody).reply(201, responseBody, { 'content-type': 'application/json' });
289
290 const body = await HttpTransport.createClient()
291 .put(url, requestBody)
292 .asBody();
293
294 assert.deepEqual(body, responseBody);
295 });
296
297 it('returns an error when the API returns a 5XX statusCode code', async () => {
298 api.put(path, requestBody).reply(500);
299
300 try {
301 await HttpTransport.createClient()
302 .use(toError())
303 .put(url, requestBody)
304 .asResponse();
305 } catch (err) {
306 return assert.equal(err.statusCode, 500);
307 }
308
309 assert.fail('Should have thrown');
310 });
311 });
312
313 describe('.delete', () => {
314 it('makes a DELETE request', () => {
315 api.delete(path).reply(204);
316 return HttpTransport.createClient().delete(url);
317 });
318
319 it('returns an error when the API returns a 5XX statusCode code', async () => {
320 api.delete(path).reply(500);
321
322 try {
323 await HttpTransport.createClient()
324 .use(toError())
325 .delete(url)
326 .asResponse();
327 } catch (err) {
328 return assert.equal(err.statusCode, 500);
329 }
330
331 assert.fail('Should have thrown');
332 });
333 });
334
335 describe('.patch', () => {
336 it('makes a PATCH request', async () => {
337 api.patch(path).reply(204, simpleResponseBody);
338 await HttpTransport.createClient()
339 .patch(url)
340 .asResponse();
341 });
342
343 it('returns an error when the API returns a 5XX statusCode code', async () => {
344 api.patch(path, requestBody).reply(500);
345
346 try {
347 await HttpTransport.createClient()
348 .use(toError())
349 .patch(url, requestBody)
350 .asResponse();
351 } catch (err) {
352 return assert.equal(err.statusCode, 500);
353 }
354 assert.fail('Should have thrown');
355 });
356 });
357
358 describe('.head', () => {
359 it('makes a HEAD request', async () => {
360 api.head(path).reply(200, simpleResponseBody);
361
362 const res = await HttpTransport.createClient()
363 .head(url)
364 .asResponse();
365
366 assert.strictEqual(res.statusCode, 200);
367 });
368
369 it('returns an error when the API returns a 5XX statusCode code', async () => {
370 api.head(path).reply(500);
371
372 try {
373 await HttpTransport.createClient()
374 .use(toError())
375 .head(url)
376 .asResponse();
377 } catch (err) {
378 return assert.strictEqual(err.statusCode, 500);
379 }
380 assert.fail('Should have thrown');
381 });
382 });
383
384 describe('.headers', () => {
385 it('sends a custom headers', async () => {
386 nock.cleanAll();
387
388 const HeaderValue = `${packageInfo.name}/${packageInfo.version}`;
389 nock(host, {
390 reqheaders: {
391 'User-Agent': HeaderValue,
392 foo: 'bar'
393 }
394 })
395 .get(path)
396 .reply(200, responseBody);
397
398 const res = await HttpTransport.createClient()
399 .get(url)
400 .headers({
401 'User-Agent': HeaderValue,
402 foo: 'bar'
403 })
404 .asResponse();
405
406 assert.equal(res.statusCode, 200);
407 });
408
409 it('ignores an empty header object', async () => {
410 nock.cleanAll();
411 api.get(path).reply(200, simpleResponseBody, { 'content-type': 'application/json' });
412
413 const res = await HttpTransport.createClient()
414 .headers({})
415 .get(url)
416 .asResponse();
417
418 assert.deepEqual(res.body, simpleResponseBody);
419 });
420 });
421
422 describe('query strings', () => {
423 it('supports adding a query string', async () => {
424 api.get('/?a=1').reply(200, simpleResponseBody, { 'content-type': 'application/json' });
425
426 const body = await HttpTransport.createClient()
427 .get(url)
428 .query('a', 1)
429 .asBody();
430
431 assert.deepEqual(body, simpleResponseBody);
432 });
433
434 it('supports multiple query strings', async () => {
435 nock.cleanAll();
436 api.get('/?a=1&b=2&c=3').reply(200, simpleResponseBody, { 'content-type': 'application/json' });
437
438 const body = await HttpTransport.createClient()
439 .get(url)
440 .query({
441 a: 1,
442 b: 2,
443 c: 3
444 })
445 .asBody();
446
447 assert.deepEqual(body, simpleResponseBody);
448 });
449
450 it('ignores empty query objects', async () => {
451 const res = await HttpTransport.createClient()
452 .query({})
453 .get(url)
454 .asResponse();
455
456 assert.deepEqual(res.body, simpleResponseBody);
457 });
458 });
459
460 describe('.timeout', () => {
461 it('sets the a timeout', async () => {
462 nock.cleanAll();
463 api
464 .get('/')
465 .delay(1000)
466 .reply(200, simpleResponseBody);
467
468 try {
469 await HttpTransport.createClient()
470 .get(url)
471 .timeout(20)
472 .asBody();
473 } catch (err) {
474 return assert.equal(err.message, 'Request failed for GET http://www.example.com/: ESOCKETTIMEDOUT');
475 }
476 assert.fail('Should have thrown');
477 });
478 });
479
480 describe('plugins', () => {
481 it('supports a per request plugin', async () => {
482 nock.cleanAll();
483 api
484 .get(path)
485 .times(2)
486 .reply(200, simpleResponseBody);
487
488 const client = HttpTransport.createClient();
489
490 const upperCaseResponse = await client
491 .use(toUpperCase())
492 .get(url)
493 .asBody();
494
495 const lowerCaseResponse = await client
496 .get(url)
497 .asBody();
498
499 assert.equal(upperCaseResponse.blobbus, 'ILLEGITIMI NON CARBORUNDUM');
500 assert.equal(lowerCaseResponse.blobbus, 'Illegitimi non carborundum');
501 });
502
503 it('executes global and per request plugins', async () => {
504 nock.cleanAll();
505 api.get(path).reply(200, simpleResponseBody);
506
507 function appendTagGlobally() {
508 return async (ctx, next) => {
509 await next();
510 ctx.res.body = 'global ' + ctx.res.body;
511 };
512 }
513
514 function appendTagPerRequestTag() {
515 return async (ctx, next) => {
516 await next();
517 ctx.res.body = 'request';
518 };
519 }
520
521 const client = HttpTransport.createBuilder()
522 .use(appendTagGlobally())
523 .createClient();
524
525 const body = await client
526 .use(appendTagPerRequestTag())
527 .get(url)
528 .asBody();
529
530 assert.equal(body, 'global request');
531 });
532
533 it('throws if a global plugin is not a function', () => {
534 assert.throws(
535 () => {
536 HttpTransport.createBuilder().use('bad plugin');
537 },
538 TypeError,
539 'Plugin is not a function'
540 );
541 });
542
543 it('throws if a per request plugin is not a function', () => {
544 assert.throws(
545 () => {
546 const client = HttpTransport.createClient();
547 client.use('bad plugin').get(url);
548 },
549 TypeError,
550 'Plugin is not a function'
551 );
552 });
553
554 describe('setContextProperty', () => {
555 it('sets an option in the context', async () => {
556 nock.cleanAll();
557 api.get(path).reply(200, responseBody);
558
559 const client = HttpTransport.createBuilder()
560 .use(toJson())
561 .createClient();
562
563 const res = await client
564 .use(setContextProperty({
565 time: false
566 },
567 'opts'
568 ))
569 .get(url)
570 .asResponse();
571
572 assert.isUndefined(res.elapsedTime);
573 });
574
575 it('sets an explict key on the context', async () => {
576 nock.cleanAll();
577 api
578 .get(path)
579 .delay(1000)
580 .reply(200, responseBody);
581
582 const client = HttpTransport.createBuilder()
583 .use(toJson())
584 .createClient();
585
586 try {
587 await client
588 .use(setContextProperty(20, 'req._timeout'))
589 .get(url)
590 .asResponse();
591 } catch (err) {
592 return assert.equal(err.message, 'Request failed for GET http://www.example.com/: ESOCKETTIMEDOUT');
593 }
594 assert.fail('Should have thrown');
595 });
596 });
597
598 describe('toJson', () => {
599 it('returns body of a JSON response', async () => {
600 nock.cleanAll();
601 api.get(path).reply(200, responseBody, defaultHeaders);
602
603 const client = HttpTransport.createBuilder()
604 .use(toJson())
605 .createClient();
606
607 const body = await client
608 .get(url)
609 .asBody();
610
611 assert.equal(body.foo, 'bar');
612 });
613 });
614
615 describe('logging', () => {
616 it('logs each request at info level when a logger is passed in', async () => {
617 api.get(path).reply(200);
618
619 const stubbedLogger = {
620 info: sandbox.stub(),
621 warn: sandbox.stub()
622 };
623
624 const client = HttpTransport.createBuilder()
625 .use(log(stubbedLogger))
626 .createClient();
627
628 await client
629 .get(url)
630 .asBody();
631
632 const message = stubbedLogger.info.getCall(0).args[0];
633 assert.match(message, /GET http:\/\/www.example.com\/ 200 \d+ ms/);
634 });
635
636 it('uses default logger', async () => {
637 sandbox.stub(console, 'info');
638
639 const client = HttpTransport.createBuilder()
640 .use(log())
641 .createClient();
642
643 await client
644 .get(url)
645 .asBody();
646
647 /*eslint no-console: ["error", { allow: ["info"] }] */
648 const message = console.info.getCall(0).args[0];
649 assert.match(message, /GET http:\/\/www.example.com\/ 200 \d+ ms/);
650 });
651
652 it('logs retry attempts as warnings when they return a critical error', async () => {
653 nock.cleanAll();
654 sandbox.stub(console, 'info');
655 sandbox.stub(console, 'warn');
656 nockRetries(2);
657
658 const client = HttpTransport.createBuilder()
659 .use(toError())
660 .use(log())
661 .createClient();
662
663 await client
664 .retry(2)
665 .get(url)
666 .asBody();
667
668 /*eslint no-console: ["error", { allow: ["info", "warn"] }] */
669 sinon.assert.calledOnce(console.warn);
670 const intial = console.info.getCall(0).args[0];
671 const attempt1 = console.warn.getCall(0).args[0];
672 assert.match(intial, /GET http:\/\/www.example.com\/ 500 \d+ ms/);
673 assert.match(attempt1, /Attempt 1 GET http:\/\/www.example.com\/ 500 \d+ ms/);
674 });
675 });
676 });
677});