UNPKG

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