UNPKG

53 kBJavaScriptView Raw
1/* eslint-env mocha */
2
3var httpism = require('../index')
4var express = require('express')
5var bodyParser = require('body-parser')
6var chai = require('chai')
7chai.use(require('chai-as-promised'))
8var assert = chai.assert
9var expect = chai.expect
10var http = require('http')
11var https = require('https')
12var fs = require('fs-promise')
13var qs = require('qs')
14var middleware = require('../middleware')
15var basicAuth = require('basic-auth-connect')
16var cookieParser = require('cookie-parser')
17var toughCookie = require('tough-cookie')
18var httpProxy = require('http-proxy')
19var net = require('net')
20var FormData = require('form-data')
21var multiparty = require('multiparty')
22var obfuscateUrlPassword = require('../obfuscateUrlPassword')
23var urlTemplate = require('url-template')
24var pathUtils = require('path')
25var cache = require('../middleware/cache')
26
27describe('httpism', function () {
28 var server
29 var app
30 var port = 12345
31 var baseurl = 'http://localhost:' + port
32
33 beforeEach(function () {
34 app = express()
35 server = app.listen(port)
36 })
37
38 afterEach(function () {
39 server.close()
40 })
41
42 describe('json', function () {
43 beforeEach(function () {
44 app.use(bodyParser.json())
45 })
46
47 function itCanMakeRequests (method) {
48 it('can make ' + method + ' requests', function () {
49 app[method.toLowerCase()]('/', function (req, res) {
50 res.send({
51 method: req.method,
52 path: req.path,
53 accept: req.headers.accept
54 })
55 })
56
57 return httpism[method.toLowerCase()](baseurl).then(function (body) {
58 expect(body).to.eql({
59 method: method,
60 path: '/',
61 accept: 'application/json'
62 })
63 })
64 })
65 }
66
67 it('can make HEAD requests', function () {
68 app.head('/', function (req, res) {
69 res.header('x-method', req.method)
70 res.header('x-path', req.path)
71 res.end()
72 })
73
74 return httpism.head(baseurl).then(function (response) {
75 expect(response.headers['x-method']).to.equal('HEAD')
76 expect(response.headers['x-path']).to.equal('/')
77 })
78 })
79
80 function itCanMakeRequestsWithBody (method) {
81 it('can make ' + method + ' requests with body', function () {
82 app[method.toLowerCase()]('/', function (req, res) {
83 res.send({
84 method: req.method,
85 path: req.path,
86 accept: req.headers.accept,
87 body: req.body
88 })
89 })
90
91 return httpism[method.toLowerCase()](baseurl, {
92 joke: 'a chicken...'
93 }).then(function (body) {
94 expect(body).to.eql({
95 method: method,
96 path: '/',
97 accept: 'application/json',
98 body: {
99 joke: 'a chicken...'
100 }
101 })
102 })
103 })
104 }
105
106 itCanMakeRequests('GET')
107 itCanMakeRequests('DELETE')
108 itCanMakeRequestsWithBody('POST')
109 itCanMakeRequestsWithBody('PUT')
110 itCanMakeRequestsWithBody('PATCH')
111 itCanMakeRequestsWithBody('OPTIONS')
112
113 describe('content type request header', function () {
114 beforeEach(function () {
115 app.post('/', function (req, res) {
116 res.header('received-content-type', req.headers['content-type'])
117 res.header('content-type', 'text/plain')
118 req.pipe(res)
119 })
120 })
121
122 it('can upload JSON as application/custom', function () {
123 return httpism.post(baseurl, { json: 'json' }, { response: true, headers: { 'content-type': 'application/custom' } }).then(function (response) {
124 expect(JSON.parse(response.body)).to.eql({
125 json: 'json'
126 })
127 expect(response.headers['received-content-type']).to.eql('application/custom')
128 })
129 })
130
131 it('can upload form as application/custom', function () {
132 return httpism.post(baseurl, { json: 'json' }, {
133 response: true,
134 form: true,
135 headers: {
136 'content-type': 'application/custom'
137 }
138 }).then(function (response) {
139 expect(qs.parse(response.body)).to.eql({
140 json: 'json'
141 })
142 expect(response.headers['received-content-type']).to.eql('application/custom')
143 })
144 })
145
146 it('can upload string as application/custom', function () {
147 return httpism.post(baseurl, 'a string', {
148 response: true,
149 headers: {
150 'content-type': 'application/custom'
151 }
152 }).then(function (response) {
153 expect(response.body).to.eql('a string')
154 expect(response.headers['received-content-type']).to.eql('application/custom')
155 })
156 })
157 })
158
159 describe('content-length header', function () {
160 var unicodeText = '♫♫♫♫♪ ☺'
161
162 beforeEach(function () {
163 return app.post('/', function (req, res) {
164 res.send({
165 'content-length': req.headers['content-length'],
166 'transfer-encoding': req.headers['transfer-encoding']
167 })
168 })
169 })
170
171 it('sends content-length, and not transfer-encoding: chunked, with JSON', function () {
172 return httpism.post(baseurl, { json: unicodeText }).then(function (body) {
173 expect(body).to.eql({
174 'content-length': Buffer.byteLength(JSON.stringify({
175 json: unicodeText
176 })).toString()
177 })
178 })
179 })
180
181 it('sends content-length, and not transfer-encoding: chunked, with plain text', function () {
182 return httpism.post(baseurl, unicodeText).then(function (body) {
183 expect(body).to.eql({
184 'content-length': Buffer.byteLength(unicodeText).toString()
185 })
186 })
187 })
188
189 it('sends content-length, and not transfer-encoding: chunked, with form data', function () {
190 return httpism.post(baseurl, { formData: unicodeText }, {
191 form: true
192 }).then(function (response) {
193 expect(response).to.eql({
194 'content-length': Buffer.byteLength(qs.stringify({
195 formData: unicodeText
196 })).toString()
197 })
198 })
199 })
200 })
201
202 describe('accept request header', function () {
203 beforeEach(function () {
204 app.get('/', function (req, res) {
205 res.header('content-type', 'text/plain')
206 res.send(req.headers.accept)
207 })
208 })
209
210 it('sends Accept: application/json by default', function () {
211 return httpism.get(baseurl).then(function (body) {
212 expect(body).to.eql('application/json')
213 })
214 })
215
216 it('can send a custom Accept header', function () {
217 return httpism.get(baseurl, {
218 headers: {
219 accept: 'application/custom'
220 }
221 }).then(function (body) {
222 expect(body).to.eql('application/custom')
223 })
224 })
225
226 it('can send a custom Accept header even mixed case', function () {
227 return httpism.get(baseurl, {
228 headers: {
229 Accept: 'application/custom'
230 }
231 }).then(function (body) {
232 expect(body).to.eql('application/custom')
233 })
234 })
235 })
236
237 describe('request headers', function () {
238 it('can specify headers for the request', function () {
239 app.get('/', function (req, res) {
240 res.send({
241 'x-header': req.headers['x-header']
242 })
243 })
244
245 return httpism.get(baseurl, {
246 headers: {
247 'x-header': 'haha'
248 }
249 }).then(function (body) {
250 expect(body['x-header']).to.equal('haha')
251 })
252 })
253 })
254
255 describe('text', function () {
256 function itReturnsAStringForContentType (mimeType) {
257 it('returns a string if the content-type is ' + mimeType, function () {
258 app.get('/', function (req, res) {
259 res.header('content-type', mimeType)
260 res.send('content as string')
261 })
262
263 return httpism.get(baseurl).then(function (body) {
264 expect(body).to.equal('content as string')
265 })
266 })
267 }
268
269 itReturnsAStringForContentType('text/plain')
270 itReturnsAStringForContentType('text/html')
271 itReturnsAStringForContentType('text/css')
272 itReturnsAStringForContentType('text/javascript')
273 itReturnsAStringForContentType('application/javascript')
274
275 it('will upload a string as text/plain', function () {
276 app.post('/text', function (req, res) {
277 res.header('received-content-type', req.headers['content-type'])
278 res.header('content-type', 'text/plain')
279 req.pipe(res)
280 })
281
282 return httpism.post(baseurl + '/text', 'content as string', {response: true}).then(function (response) {
283 expect(response.headers['received-content-type']).to.equal('text/plain')
284 expect(response.body).to.equal('content as string')
285 })
286 })
287 })
288
289 describe('params', function () {
290 beforeEach(function () {
291 app.get('*', function (req, res) {
292 res.send(req.url)
293 })
294 })
295
296 it('can set params', function () {
297 return httpism.get(baseurl + '/:a', {
298 params: {
299 a: 'aa',
300 b: 'bb'
301 }
302 }).then(function (body) {
303 expect(body).to.eql('/aa?b=bb')
304 })
305 })
306
307 it('can set path params', function () {
308 return httpism.get(baseurl + '/:a*/:b', {
309 params: {
310 a: 'a/long/path',
311 b: 'bb'
312 }
313 }).then(function (body) {
314 expect(body).to.eql('/a/long/path/bb')
315 })
316 })
317
318 it('uses escapes', function () {
319 return httpism.get(baseurl + '/:a/:b', {
320 params: {
321 a: 'a/a',
322 b: 'b/b',
323 c: 'c/c'
324 }
325 }).then(function (body) {
326 expect(body).to.eql('/a%2Fa/b%2Fb?c=c%2Fc')
327 })
328 })
329
330 it('can use other template engines', function () {
331 return httpism.get(baseurl + '/{a}{?b,c}', {
332 params: {
333 a: 'x',
334 b: 'y',
335 c: 'z'
336 },
337 expandUrl: function (url, params) {
338 var template = urlTemplate.parse(url)
339 return template.expand(params)
340 }
341 }).then(function (body) {
342 expect(body).to.eql('/x?b=y&c=z')
343 })
344 })
345 })
346
347 describe('.client()', function () {
348 it('can make a new client that adds headers', function () {
349 app.get('/', function (req, res) {
350 res.send({
351 joke: req.headers.joke
352 })
353 })
354
355 var client = httpism.client(function (request, next) {
356 request.headers.joke = 'a chicken...'
357 return next(request)
358 })
359
360 return client.get(baseurl).then(function (body) {
361 expect(body).to.eql({
362 joke: 'a chicken...'
363 })
364 })
365 })
366
367 it('can make a new client that adds headers by passing them to options', function () {
368 app.get('/', function (req, res) {
369 res.send({
370 x: req.headers.x,
371 y: req.headers.y
372 })
373 })
374
375 var client = httpism.client('/', { headers: { x: '123' } }).client('/', { headers: { y: '456' } })
376
377 return client.get(baseurl).then(function (body) {
378 expect(body).to.eql({
379 x: '123',
380 y: '456'
381 })
382 })
383 })
384
385 it('makes requests with additional headers', function () {
386 app.get('/', function (req, res) {
387 res.send({
388 x: req.headers.x,
389 y: req.headers.y
390 })
391 })
392
393 var client = httpism.client({ headers: { x: '123' } })
394
395 return client.get(baseurl, { headers: { y: '456' } }).then(function (body) {
396 expect(body).to.eql({
397 x: '123',
398 y: '456'
399 })
400 })
401 })
402
403 describe('cache example', function () {
404 var filename = pathUtils.join(__dirname, 'cachefile.txt')
405
406 beforeEach(function () {
407 return fs.writeFile(filename, '{"from": "cache"}')
408 })
409
410 afterEach(function () {
411 return fs.unlink(filename)
412 })
413
414 it('can insert a new middleware just before the http request, dealing with streams', function () {
415 app.get('/', function (req, res) {
416 res.send({from: 'server'})
417 })
418
419 var cache = function (req, next) {
420 return next().then(function (response) {
421 response.body = fs.createReadStream(filename)
422 return response
423 })
424 }
425
426 cache.httpismMiddleware = {
427 before: 'http'
428 }
429
430 var http = httpism.client(cache)
431
432 return http.get(baseurl).then(function (body) {
433 expect(body).to.eql({from: 'cache'})
434 })
435 })
436 })
437
438 describe('middleware', function () {
439 describe('defining middleware', function () {
440 var client
441
442 beforeEach(function () {
443 app.get('/', function (req, res) {
444 res.send(req.query)
445 })
446
447 client = httpism.client(baseurl)
448 })
449
450 it('can modify the request', function () {
451 client.use(function (req, next) {
452 req.url += '?param=hi'
453 return next()
454 })
455
456 return client.get('/').then(function (response) {
457 expect(response).to.eql({param: 'hi'})
458 })
459 })
460
461 it('can send an entirely new request', function () {
462 client.use(function (req, next) {
463 return next({
464 url: req.url + '?param=hi',
465 options: {},
466 headers: {}
467 })
468 })
469
470 return client.get('/').then(function (response) {
471 expect(response).to.eql({param: 'hi'})
472 })
473 })
474
475 it('can return an entirely new response', function () {
476 client.use(function (req, next) {
477 return next().then(function (response) {
478 return {
479 body: 'body'
480 }
481 })
482 })
483
484 return client.get('/').then(function (response) {
485 expect(response).to.eql('body')
486 })
487 })
488 })
489
490 describe('inserting middleware', function () {
491 var pipeline, a, b
492
493 function middlwareNamed (name) {
494 function middleware () {
495 }
496
497 middleware.httpismMiddleware = {
498 name: name
499 }
500
501 return middleware
502 }
503
504 beforeEach(function () {
505 pipeline = httpism.raw.client()
506 pipeline.middleware = [
507 a = middlwareNamed('a'),
508 b = middlwareNamed('b')
509 ]
510 })
511
512 describe('before', function () {
513 it('can insert middleware before another', function () {
514 var m = function () {}
515 m.httpismMiddleware = {
516 before: 'b'
517 }
518
519 var client = pipeline.client(m)
520
521 expect(client.middleware).to.eql([
522 a,
523 m,
524 b
525 ])
526 })
527
528 it('can insert middleware into same client before another', function () {
529 var m = function () {}
530 m.httpismMiddleware = {
531 before: 'b'
532 }
533
534 pipeline.use(m)
535
536 expect(pipeline.middleware).to.eql([
537 a,
538 m,
539 b
540 ])
541 })
542
543 it('inserts before the named middleware if at least one is found', function () {
544 var m = function () {}
545 m.httpismMiddleware = {
546 before: ['b', 'c']
547 }
548
549 var client = pipeline.client(m)
550 expect(client.middleware).to.eql([
551 a,
552 m,
553 b
554 ])
555 })
556
557 it('inserts before all the named middleware if all are found', function () {
558 var m = function () {}
559 m.httpismMiddleware = {
560 before: ['b', 'b']
561 }
562 var client = pipeline.client(m)
563 expect(client.middleware).to.eql([
564 a,
565 m,
566 b
567 ])
568 })
569 })
570
571 describe('after', function () {
572 it('can insert middleware after another', function () {
573 var m = function () {}
574 m.httpismMiddleware = {
575 after: 'a'
576 }
577 var client = pipeline.client(m)
578 expect(client.middleware).to.eql([
579 a,
580 m,
581 b
582 ])
583 })
584
585 it('inserts after the named middleware if at lesat one is found', function () {
586 var m = function () {}
587 m.httpismMiddleware = {
588 after: ['a', 'c']
589 }
590 var client = pipeline.client(m)
591 expect(client.middleware).to.eql([
592 a,
593 m,
594 b
595 ])
596 })
597
598 it('inserts after all the named middleware if all are found', function () {
599 var m = function () {}
600 m.httpismMiddleware = {
601 after: ['a', 'b']
602 }
603 var client = pipeline.client(m)
604 expect(client.middleware).to.eql([
605 a,
606 b,
607 m
608 ])
609 })
610 })
611
612 it('can remove middleware', function () {
613 pipeline.remove('b')
614
615 expect(pipeline.middleware).to.eql([
616 a
617 ])
618 })
619
620 it('throws if before middleware name cannot be found', function () {
621 var m = function () {}
622 m.httpismMiddleware = {
623 before: 'notfound'
624 }
625 expect(function () { httpism.client(m) }).to.throw('no such middleware: notfound')
626 })
627
628 it('throws if none of the before middleware names can be found', function () {
629 var m = function () {}
630 m.httpismMiddleware = {
631 before: ['notfound']
632 }
633 expect(function () { httpism.client(m) }).to.throw('no such middleware: notfound')
634 })
635
636 it('throws if after middleware name cannot be found', function () {
637 var m = function () {}
638 m.httpismMiddleware = {
639 after: 'notfound'
640 }
641 expect(function () { httpism.client(m) }).to.throw('no such middleware: notfound')
642 })
643
644 it('throws if none of the after middleware names can be found', function () {
645 var m = function () {}
646 m.httpismMiddleware = {
647 after: ['notfound']
648 }
649 expect(function () { httpism.client(m) }).to.throw('no such middleware: notfound')
650 })
651 })
652 })
653
654 describe('2.x compatibility', function () {
655 it('can be created using `.api()`', function () {
656 app.get('/', function (req, res) {
657 res.send({
658 joke: req.headers.joke
659 })
660 })
661
662 var client = httpism.api(function (request, next) {
663 request.headers.joke = 'a chicken...'
664 return next(request)
665 })
666
667 return client.get(baseurl).then(function (body) {
668 expect(body).to.eql({
669 joke: 'a chicken...'
670 })
671 })
672 })
673 })
674
675 describe('query strings (deprecated)', function () {
676 beforeEach(function () {
677 app.get('/', function (req, res) {
678 res.send(req.query)
679 })
680 })
681
682 it('can set query string', function () {
683 return httpism.get(baseurl, {
684 querystring: {
685 a: 'a',
686 b: 'b'
687 }
688 }).then(function (body) {
689 expect(body).to.eql({
690 a: 'a',
691 b: 'b'
692 })
693 })
694 })
695
696 it('can override query string in url', function () {
697 return httpism.get(baseurl + '/?a=a&c=c', {
698 querystring: {
699 a: 'newa',
700 b: 'b'
701 }
702 }).then(function (body) {
703 expect(body).to.eql({
704 a: 'newa',
705 b: 'b',
706 c: 'c'
707 })
708 })
709 })
710 })
711 })
712
713 describe('exceptions', function () {
714 beforeEach(function () {
715 app.get('/400', function (req, res) {
716 res.status(400).send({
717 message: 'oh dear'
718 })
719 })
720 })
721
722 it('throws exceptions on 400-500 status codes, by default', function () {
723 return httpism.client(baseurl).get('/400').then(function () {
724 assert.fail('expected an exception to be thrown')
725 }).catch(function (e) {
726 expect(e.message).to.equal('GET ' + baseurl + '/400 => 400 Bad Request')
727 expect(e.statusCode).to.equal(400)
728 expect(e.body.message).to.equal('oh dear')
729 })
730 })
731
732 it("doesn't include the password in the error message", function () {
733 return httpism.client('http://user:pass@localhost:' + port + '/').get('/400').then(function () {
734 assert.fail('expected an exception to be thrown')
735 }).catch(function (e) {
736 expect(e.message).to.equal('GET http://user:********@localhost:' + port + '/400 => 400 Bad Request')
737 expect(e.statusCode).to.equal(400)
738 expect(e.body.message).to.equal('oh dear')
739 })
740 })
741
742 it("doesn't throw exceptions on 400-500 status codes, when specified", function () {
743 return httpism.client(baseurl).get('/400', { exceptions: false }).then(function (body) {
744 expect(body.message).to.equal('oh dear')
745 })
746 })
747
748 describe('error predicate', function () {
749 it('throws exceptions when predicate returns true', function () {
750 function isError (response) {
751 return response.statusCode === 400
752 }
753
754 return httpism.client(baseurl).get('/400', { exceptions: isError }).then(function () {
755 assert.fail('expected an exception to be thrown')
756 }).catch(function (e) {
757 expect(e.message).to.equal('GET ' + baseurl + '/400 => 400 Bad Request')
758 expect(e.statusCode).to.equal(400)
759 expect(e.body.message).to.equal('oh dear')
760 })
761 })
762
763 it("doesn't throw exceptions when predicate returns false", function () {
764 function isError (response) {
765 return response.statusCode !== 400
766 }
767
768 return httpism.client(baseurl).get('/400', { exceptions: isError }).then(function (body) {
769 expect(body.message).to.equal('oh dear')
770 })
771 })
772 })
773
774 it('throws if it cannot connect', function () {
775 return expect(httpism.get('http://localhost:50000/')).to.eventually.be.rejectedWith('ECONNREFUSED')
776 })
777 })
778
779 describe('options', function () {
780 var client
781
782 beforeEach(function () {
783 client = httpism.client(function (request, next) {
784 request.body = request.options
785 return next(request)
786 }, { a: 'a' })
787
788 app.post('/', function (req, res) {
789 res.send(req.body)
790 })
791 })
792
793 it('clients have options, which can be overwritten on each request', function () {
794 var root = client.client(baseurl)
795 return root.post('', undefined, { b: 'b' }).then(function (body) {
796 expect(body).to.eql({
797 a: 'a',
798 b: 'b'
799 })
800
801 return root.client().post('', undefined, { c: 'c' }).then(function (body) {
802 expect(body).to.eql({
803 a: 'a',
804 c: 'c'
805 })
806
807 return root.post('', undefined).then(function (body) {
808 expect(body).eql({
809 a: 'a'
810 })
811 })
812 })
813 })
814 })
815 })
816
817 describe('responses', function () {
818 beforeEach(function () {
819 app.get('/', function (req, res) {
820 res.set({'x-custom-header': 'header value'})
821 res.status(234)
822 res.send({data: 'data'})
823 })
824 })
825
826 describe('response: true', function () {
827 var response
828
829 beforeEach(function () {
830 return httpism.get(baseurl, {response: true}).then(function (_response) {
831 response = _response
832 })
833 })
834
835 it('contains the url', function () {
836 expect(response.url).to.equal(baseurl)
837 })
838
839 it('contains the status code', function () {
840 expect(response.statusCode).to.equal(234)
841 })
842
843 it('contains the headers', function () {
844 expect(response.headers['x-custom-header']).to.equal('header value')
845 })
846
847 it('contains the body', function () {
848 expect(response.body).to.eql({data: 'data'})
849 })
850 })
851
852 describe('response: false (default)', function () {
853 var body
854
855 beforeEach(function () {
856 return httpism.get(baseurl).then(function (_body) {
857 body = _body
858 })
859 })
860
861 describe('2.x compatibility', function () {
862 it('contains the url', function () {
863 expect(body.url).to.equal(baseurl)
864 })
865
866 it('contains the status code', function () {
867 expect(body.statusCode).to.equal(234)
868 })
869
870 it('contains the headers', function () {
871 expect(body.headers['x-custom-header']).to.equal('header value')
872 })
873
874 it('contains the body', function () {
875 expect(body.body).to.eql({data: 'data'})
876 })
877 })
878
879 it('returns the body', function () {
880 expect(body).to.eql({data: 'data'})
881 })
882 })
883 })
884
885 describe('redirects', function () {
886 beforeEach(function () {
887 app.get('/redirecttoredirect', function (req, res) {
888 res.redirect('/redirect')
889 })
890
891 app.get('/redirect', function (req, res) {
892 res.location('/path/')
893 res.status(302).send({
894 path: req.path
895 })
896 })
897 app.get('/', function (req, res) {
898 res.send({
899 path: req.path
900 })
901 })
902 app.get('/path/', function (req, res) {
903 res.send({
904 path: req.path
905 })
906 })
907 app.get('/path/file', function (req, res) {
908 res.send({
909 path: req.path
910 })
911 })
912 })
913
914 it('follows redirects by default', function () {
915 return httpism.get(baseurl + '/redirect', {response: true}).then(function (response) {
916 expect(response.body).to.eql({
917 path: '/path/'
918 })
919 expect(response.url).to.eql(baseurl + '/path/')
920 })
921 })
922
923 function itFollowsRedirects (statusCode) {
924 it('follows ' + statusCode + ' redirects', function () {
925 app.get('/' + statusCode, function (req, res) {
926 res.location('/path/')
927 res.status(statusCode).send()
928 })
929
930 return httpism.get(baseurl + '/' + statusCode, {response: true}).then(function (response) {
931 expect(response.body).to.eql({
932 path: '/path/'
933 })
934 expect(response.url).to.eql(baseurl + '/path/')
935 })
936 })
937 }
938
939 describe('redirects', function () {
940 itFollowsRedirects(300)
941 itFollowsRedirects(301)
942 itFollowsRedirects(302)
943 itFollowsRedirects(303)
944 itFollowsRedirects(307)
945 })
946
947 it('follows a more than one redirect', function () {
948 return httpism.get(baseurl + '/redirecttoredirect', {response: true}).then(function (response) {
949 expect(response.body).to.eql({
950 path: '/path/'
951 })
952 expect(response.url).to.eql(baseurl + '/path/')
953 })
954 })
955
956 it("doesn't follow redirects when specified", function () {
957 return httpism.get(baseurl + '/redirect', {
958 redirect: false,
959 response: true
960 }).then(function (response) {
961 expect(response.body).to.eql({
962 path: '/redirect'
963 })
964 expect(response.url).to.eql(baseurl + '/redirect')
965 expect(response.headers.location).to.equal('/path/')
966 expect(response.statusCode).to.equal(302)
967 })
968 })
969 })
970
971 describe('cookies', function () {
972 beforeEach(function () {
973 app.use(cookieParser())
974 app.get('/setcookie', function (req, res) {
975 res.cookie('mycookie', 'value')
976 res.send({})
977 })
978 app.get('/getcookie', function (req, res) {
979 res.send(req.cookies)
980 })
981 })
982
983 it('can store cookies and send cookies', function () {
984 var cookies = new toughCookie.CookieJar()
985 return httpism.get(baseurl + '/setcookie', {
986 cookies: cookies
987 }).then(function () {
988 return httpism.get(baseurl + '/getcookie', {
989 cookies: cookies
990 }).then(function (body) {
991 expect(body).to.eql({
992 mycookie: 'value'
993 })
994 })
995 })
996 })
997
998 it('can store cookies and send cookies', function () {
999 var client = httpism.client(baseurl, {
1000 cookies: true
1001 })
1002 return client.get(baseurl + '/setcookie').then(function () {
1003 return client.get(baseurl + '/getcookie').then(function (body) {
1004 expect(body).to.eql({
1005 mycookie: 'value'
1006 })
1007 })
1008 })
1009 })
1010 })
1011
1012 describe('https', function () {
1013 var httpsServer
1014 var httpsPort = 23456
1015 var httpsBaseurl = 'https://localhost:' + httpsPort + '/'
1016
1017 beforeEach(function () {
1018 var credentials = {
1019 key: fs.readFileSync(pathUtils.join(__dirname, 'server.key'), 'utf-8'),
1020 cert: fs.readFileSync(pathUtils.join(__dirname, 'server.crt'), 'utf-8')
1021 }
1022 httpsServer = https.createServer(credentials, app)
1023 httpsServer.listen(httpsPort)
1024 })
1025
1026 afterEach(function () {
1027 httpsServer.close()
1028 })
1029
1030 it('can make HTTPS requests', function () {
1031 app.get('/', function (req, res) {
1032 res.send({
1033 protocol: req.protocol
1034 })
1035 })
1036
1037 return httpism.get(httpsBaseurl, { https: { rejectUnauthorized: false } }).then(function (body) {
1038 expect(body.protocol).to.equal('https')
1039 })
1040 })
1041 })
1042
1043 describe('forms', function () {
1044 it('can upload application/x-www-form-urlencoded', function () {
1045 app.post('/form', function (req, res) {
1046 res.header('content-type', 'text/plain')
1047 res.header('received-content-type', req.headers['content-type'])
1048 req.pipe(res)
1049 })
1050
1051 return httpism.post(baseurl + '/form', {
1052 name: 'Betty Boop',
1053 address: 'one & two'
1054 }, {
1055 form: true,
1056 response: true
1057 }).then(function (response) {
1058 expect(response.body).to.equal('name=Betty%20Boop&address=one%20%26%20two')
1059 expect(response.headers['received-content-type']).to.equal('application/x-www-form-urlencoded')
1060 })
1061 })
1062
1063 it('can download application/x-www-form-urlencoded', function () {
1064 app.get('/form', function (req, res) {
1065 res.header('content-type', 'application/x-www-form-urlencoded')
1066 res.send(qs.stringify({
1067 name: 'Betty Boop',
1068 address: 'one & two'
1069 }))
1070 })
1071
1072 return httpism.get(baseurl + '/form', {response: true}).then(function (response) {
1073 expect(response.body).to.eql({
1074 name: 'Betty Boop',
1075 address: 'one & two'
1076 })
1077 expect(response.headers['content-type']).to.equal('application/x-www-form-urlencoded; charset=utf-8')
1078 })
1079 })
1080
1081 describe('multipart forms', function () {
1082 var filename = pathUtils.join(__dirname, 'afile.jpg')
1083
1084 beforeEach(function () {
1085 return fs.writeFile(filename, 'an image')
1086 })
1087
1088 afterEach(function () {
1089 return fs.unlink(filename)
1090 })
1091
1092 it('can send multipart forms with `form-data`', function () {
1093 app.post('/form', function (req, res) {
1094 var form = new multiparty.Form()
1095
1096 form.parse(req, function (err, fields, files) {
1097 if (err) {
1098 console.log(err)
1099 res.status(500).send({message: err.message})
1100 }
1101 var response = {}
1102
1103 Object.keys(fields).forEach(function (field) {
1104 response[field] = fields[field][0]
1105 })
1106 Promise.all(Object.keys(files).map(function (field) {
1107 var file = files[field][0]
1108 return middleware.streamToString(fs.createReadStream(file.path)).then(function (contents) {
1109 response[field] = {
1110 contents: contents,
1111 headers: file.headers
1112 }
1113 })
1114 })).then(function () {
1115 res.send(response)
1116 })
1117 })
1118 })
1119
1120 var form = new FormData()
1121
1122 form.append('name', 'Betty Boop')
1123 form.append('address', 'one & two')
1124 form.append('photo', fs.createReadStream(filename))
1125
1126 return httpism.post(baseurl + '/form', form).then(function (body) {
1127 expect(body).to.eql({
1128 name: 'Betty Boop',
1129 address: 'one & two',
1130 photo: {
1131 contents: 'an image',
1132 headers: {
1133 'content-disposition': 'form-data; name="photo"; filename="afile.jpg"',
1134 'content-type': 'image/jpeg'
1135 }
1136 }
1137 })
1138 })
1139 })
1140 })
1141 })
1142
1143 describe('basic authentication', function () {
1144 beforeEach(function () {
1145 app.use(basicAuth(function (user, pass) {
1146 return user === 'good user' && pass === 'good password!'
1147 }))
1148 return app.get('/secret', function (req, res) {
1149 res.send('this is secret')
1150 })
1151 })
1152
1153 it('can authenticate using username password', function () {
1154 return httpism.get(baseurl + '/secret', {
1155 basicAuth: {
1156 username: 'good user',
1157 password: 'good password!'
1158 }
1159 }).then(function (body) {
1160 expect(body).to.equal('this is secret')
1161 })
1162 })
1163
1164 it('can authenticate using username password encoded in URL', function () {
1165 var u = encodeURIComponent
1166 return httpism.get('http://' + u('good user') + ':' + u('good password!') + '@localhost:' + port + '/secret').then(function (body) {
1167 expect(body).to.equal('this is secret')
1168 })
1169 })
1170
1171 it('can authenticate using username with colons :', function () {
1172 return httpism.get(baseurl + '/secret', {
1173 basicAuth: {
1174 username: 'good: :user',
1175 password: 'good password!'
1176 }
1177 }).then(function (body) {
1178 expect(body).to.equal('this is secret')
1179 })
1180 })
1181
1182 it("doesn't crash if username or password are undefined", function () {
1183 return httpism.get(baseurl + '/secret', {
1184 basicAuth: {
1185 },
1186 exceptions: false,
1187 response: true
1188 }).then(function (response) {
1189 expect(response.statusCode).to.equal(401)
1190 })
1191 })
1192
1193 it('fails to authenticate when password is incorrect', function () {
1194 return httpism.get(baseurl + '/secret', {
1195 basicAuth: {
1196 username: 'good user',
1197 password: 'bad password!'
1198 },
1199 exceptions: false
1200 }).then(function (response) {
1201 expect(response.statusCode).to.equal(401)
1202 })
1203 })
1204 })
1205 })
1206
1207 describe('streams', function () {
1208 var filename = pathUtils.join(__dirname, 'afile.txt')
1209
1210 beforeEach(function () {
1211 return fs.writeFile(filename, 'some content').then(function () {
1212 app.post('/file', function (req, res) {
1213 res.header('content-type', 'text/plain')
1214 res.header('received-content-type', req.headers['content-type'])
1215 req.unshift('received: ')
1216 req.pipe(res)
1217 })
1218
1219 app.get('/file', function (req, res) {
1220 var stream
1221 stream = fs.createReadStream(filename)
1222 res.header('content-type', 'application/blah')
1223 stream.pipe(res)
1224 })
1225 })
1226 })
1227
1228 afterEach(function () {
1229 return fs.unlink(filename)
1230 })
1231
1232 function itCanUploadAStreamWithContentType (contentType) {
1233 it('can upload a stream with Content-Type: ' + contentType, function () {
1234 var stream = fs.createReadStream(filename)
1235
1236 return httpism.post(baseurl + '/file', stream, {
1237 headers: {
1238 'content-type': contentType
1239 },
1240 response: true
1241 }).then(function (response) {
1242 expect(response.headers['received-content-type']).to.equal(contentType)
1243 expect(response.body).to.equal('received: some content')
1244 })
1245 })
1246 }
1247
1248 itCanUploadAStreamWithContentType('application/blah')
1249 itCanUploadAStreamWithContentType('application/json')
1250 itCanUploadAStreamWithContentType('text/plain')
1251 itCanUploadAStreamWithContentType('application/x-www-form-urlencoded')
1252
1253 it('it guesses the Content-Type of the stream when created from a file', function () {
1254 var stream = fs.createReadStream(filename)
1255
1256 return httpism.post(baseurl + '/file', stream, {response: true}).then(function (response) {
1257 expect(response.headers['received-content-type']).to.equal('text/plain')
1258 expect(response.body).to.equal('received: some content')
1259 })
1260 })
1261
1262 it('can download a stream', function () {
1263 return httpism.get(baseurl + '/file', {response: true}).then(function (response) {
1264 expect(response.headers['content-type']).to.equal('application/blah')
1265 return middleware.streamToString(response.body).then(function (response) {
1266 expect(response).to.equal('some content')
1267 })
1268 })
1269 })
1270
1271 describe('forcing response parsing', function () {
1272 function describeForcingResponse (type, options) {
1273 var contentType = options !== undefined && Object.prototype.hasOwnProperty.call(options, 'contentType') && options.contentType !== undefined ? options.contentType : undefined
1274 var content = options !== undefined && Object.prototype.hasOwnProperty.call(options, 'content') && options.content !== undefined ? options.content : undefined
1275 var sendContent = options !== undefined && Object.prototype.hasOwnProperty.call(options, 'sendContent') && options.sendContent !== undefined ? options.sendContent : undefined
1276
1277 describe(type, function () {
1278 it('can download a stream of content-type ' + contentType, function () {
1279 app.get('/content', function (req, res) {
1280 var stream = fs.createReadStream(filename)
1281 res.header('content-type', contentType)
1282 stream.pipe(res)
1283 })
1284
1285 return httpism.get(baseurl + '/content', {
1286 responseBody: 'stream',
1287 response: true
1288 }).then(function (response) {
1289 expect(response.headers['content-type']).to.equal(contentType)
1290 return middleware.streamToString(response.body).then(function (response) {
1291 expect(response).to.equal('some content')
1292 })
1293 })
1294 })
1295
1296 it('can force parse ' + type + ' when content-type is application/blah', function () {
1297 app.get('/content', function (req, res) {
1298 res.header('content-type', 'application/blah')
1299 res.send(sendContent || content)
1300 })
1301
1302 return httpism.get(baseurl + '/content', {
1303 responseBody: type,
1304 response: true
1305 }).then(function (response) {
1306 expect(response.headers['content-type']).to.equal('application/blah; charset=utf-8')
1307 expect(response.body).to.eql(content)
1308 })
1309 })
1310 })
1311 }
1312
1313 describeForcingResponse('text', {
1314 contentType: 'text/plain; charset=utf-8',
1315 content: 'some text content'
1316 })
1317 describeForcingResponse('json', {
1318 contentType: 'application/json',
1319 content: {
1320 json: true
1321 }
1322 })
1323 describeForcingResponse('form', {
1324 contentType: 'application/x-www-form-urlencoded',
1325 content: {
1326 json: 'true'
1327 },
1328 sendContent: qs.stringify({
1329 json: 'true'
1330 })
1331 })
1332 })
1333 })
1334
1335 describe('proxy', function () {
1336 var proxyServer
1337 var proxyPort = 12346
1338 var proxy
1339 var urlProxied
1340 var proxied
1341 var proxyAuth = false
1342 var proxyUrl = 'http://localhost:' + proxyPort + '/'
1343 var secureProxyUrl = 'http://bob:secret@localhost:' + proxyPort + '/'
1344
1345 function proxyRequest (req, res) {
1346 urlProxied = req.url
1347 proxied = true
1348 proxy.web(req, res, { target: req.url })
1349 }
1350
1351 function checkProxyAuthentication (req, res, next) {
1352 var expectedAuthorisation = 'Basic ' + Buffer.from('bob:secret').toString('base64')
1353
1354 if (expectedAuthorisation === req.headers['proxy-authorization']) {
1355 next(req, res)
1356 } else {
1357 res.statusCode = 407
1358 res.end('bad proxy authentication')
1359 }
1360 }
1361
1362 beforeEach(function () {
1363 urlProxied = undefined
1364 proxied = false
1365 proxy = httpProxy.createProxyServer()
1366
1367 proxyServer = http.createServer(function (req, res) {
1368 if (proxyAuth) {
1369 return checkProxyAuthentication(req, res, proxyRequest)
1370 } else {
1371 return proxyRequest(req, res)
1372 }
1373 })
1374 proxyServer.listen(proxyPort)
1375
1376 proxyServer.on('connect', function (req, socket) {
1377 proxied = true
1378 var addr = req.url.split(':')
1379 // creating TCP connection to remote server
1380 var conn = net.connect(addr[1] || 443, addr[0], function () {
1381 // tell the client that the connection is established
1382 socket.write('HTTP/' + req.httpVersion + ' 200 OK\r\n\r\n', 'UTF-8', function () {
1383 // creating pipes in both ends
1384 conn.pipe(socket)
1385 socket.pipe(conn)
1386 })
1387 })
1388
1389 conn.on('error', function (e) {
1390 console.log('Server connection error: ' + e, addr)
1391 socket.end()
1392 })
1393 })
1394 })
1395
1396 afterEach(function () {
1397 proxyServer.close()
1398 })
1399
1400 var httpsServer
1401 var httpsPort = 23456
1402 var httpsBaseurl = 'https://localhost:' + httpsPort + '/'
1403
1404 beforeEach(function () {
1405 var credentials = {
1406 key: fs.readFileSync(pathUtils.join(__dirname, 'server.key'), 'utf-8'),
1407 cert: fs.readFileSync(pathUtils.join(__dirname, 'server.crt'), 'utf-8')
1408 }
1409 httpsServer = https.createServer(credentials, app)
1410 httpsServer.listen(httpsPort)
1411 })
1412
1413 afterEach(function () {
1414 httpsServer.close()
1415 })
1416
1417 context('unsecured proxy', function () {
1418 it('can use a proxy', function () {
1419 app.get('/', function (req, res) {
1420 res.send({
1421 blah: 'blah'
1422 })
1423 })
1424
1425 return httpism.get(baseurl, {proxy: proxyUrl}).then(function (body) {
1426 expect(body).to.eql({blah: 'blah'})
1427 expect(urlProxied).to.equal(baseurl)
1428 })
1429 })
1430
1431 it('can make HTTPS requests', function () {
1432 app.get('/', function (req, res) {
1433 res.send({
1434 protocol: req.protocol
1435 })
1436 })
1437
1438 return httpism.get(httpsBaseurl, { proxy: proxyUrl, https: { rejectUnauthorized: false } }).then(function (body) {
1439 expect(body.protocol).to.equal('https')
1440 })
1441 })
1442 })
1443
1444 context('proxy environment variables', function () {
1445 beforeEach(function () {
1446 app.get('/', function (req, res) {
1447 res.send({
1448 blah: 'blah'
1449 })
1450 })
1451 })
1452
1453 beforeEach(function () {
1454 delete process.env.NO_PROXY
1455 delete process.env.no_proxy
1456 delete process.env.HTTP_PROXY
1457 delete process.env.http_proxy
1458 delete process.env.HTTPS_PROXY
1459 delete process.env.https_proxy
1460 })
1461
1462 function assertProxied (url) {
1463 return httpism.get(url, { https: { rejectUnauthorized: false } }).then(function () {
1464 expect(proxied).to.equal(true)
1465 })
1466 }
1467
1468 function assertNotProxied (url) {
1469 return httpism.get(url, { https: { rejectUnauthorized: false } }).then(function () {
1470 expect(proxied).to.equal(false)
1471 })
1472 }
1473
1474 it('uses http_proxy for HTTP requests', function () {
1475 process.env.http_proxy = proxyUrl
1476
1477 return assertProxied(baseurl)
1478 })
1479
1480 it('uses HTTP_PROXY for HTTP requests', function () {
1481 process.env.HTTP_PROXY = proxyUrl
1482
1483 return assertProxied(baseurl)
1484 })
1485
1486 it('uses https_proxy for HTTPS requests', function () {
1487 process.env.https_proxy = proxyUrl
1488
1489 return assertProxied(httpsBaseurl)
1490 })
1491
1492 it('uses HTTPS_PROXY for HTTPS requests', function () {
1493 process.env.HTTPS_PROXY = proxyUrl
1494
1495 return assertProxied(httpsBaseurl)
1496 })
1497
1498 it('use skips hosts defined in no_proxy', function () {
1499 process.env.HTTP_PROXY = proxyUrl
1500 process.env.no_proxy = 'localhost'
1501
1502 return assertNotProxied(httpsBaseurl)
1503 })
1504
1505 it('use skips hosts defined in NO_PROXY', function () {
1506 process.env.HTTP_PROXY = proxyUrl
1507 process.env.NO_PROXY = 'localhost'
1508
1509 return assertNotProxied(baseurl)
1510 })
1511 })
1512
1513 context('secured proxy', function () {
1514 it('can use a proxy', function () {
1515 app.get('/', function (req, res) {
1516 res.send({
1517 blah: 'blah'
1518 })
1519 })
1520
1521 return httpism.get(baseurl, {proxy: secureProxyUrl}).then(function (body) {
1522 expect(body).to.eql({blah: 'blah'})
1523 expect(urlProxied).to.equal(baseurl)
1524 })
1525 })
1526
1527 it('can make HTTPS requests', function () {
1528 app.get('/', function (req, res) {
1529 res.send({
1530 protocol: req.protocol
1531 })
1532 })
1533
1534 return httpism.get(httpsBaseurl, { proxy: secureProxyUrl, https: { rejectUnauthorized: false } }).then(function (body) {
1535 expect(body.protocol).to.equal('https')
1536 })
1537 })
1538 })
1539 })
1540
1541 describe('raw', function () {
1542 it('can be used to create new middleware pipelines', function () {
1543 app.get('/', function (req, res) {
1544 res.status(400).send({
1545 blah: 'blah'
1546 })
1547 })
1548
1549 var client = httpism.raw.client(baseurl, function (request, next) {
1550 return next().then(function (res) {
1551 return middleware.streamToString(res.body).then(function (response) {
1552 res.body = response
1553 return res
1554 })
1555 })
1556 })
1557
1558 return client.get(baseurl, {response: true}).then(function (response) {
1559 expect(response.statusCode).to.equal(400)
1560 expect(JSON.parse(response.body)).to.eql({
1561 blah: 'blah'
1562 })
1563 })
1564 })
1565 })
1566
1567 describe('json reviver', function () {
1568 it('controls how the JSON response is deserialised', function () {
1569 app.get('/', function (req, res) {
1570 res.status(200).send({ blah: 1234 })
1571 })
1572
1573 var client = httpism.client(baseurl, {
1574 jsonReviver: function (key, value) {
1575 if (key === '') { return value }
1576 return key + value + '!'
1577 }
1578 })
1579
1580 return client.get(baseurl, {response: true}).then(function (response) {
1581 expect(response.statusCode).to.equal(200)
1582 expect(response.body).to.eql({
1583 blah: 'blah1234!'
1584 })
1585 })
1586 })
1587 })
1588
1589 describe('obfuscating passwords', function () {
1590 it('can obfuscate passwords from http URLs', function () {
1591 var obfuscated = obfuscateUrlPassword('https://user:password@example.com/a/:path/user:password/user:password.thing')
1592 expect(obfuscated).to.equal('https://user:********@example.com/a/:path/user:password/user:password.thing')
1593 })
1594
1595 it("doesn't do anything to relative paths", function () {
1596 var obfuscated = obfuscateUrlPassword('/a/:path/user:password/user:password.thing')
1597 expect(obfuscated).to.equal('/a/:path/user:password/user:password.thing')
1598 })
1599
1600 it("doesn't do anything to hosts with ports", function () {
1601 var obfuscated = obfuscateUrlPassword('http://localhost:4000/a/:path/user:password/user:password.thing')
1602 expect(obfuscated).to.equal('http://localhost:4000/a/:path/user:password/user:password.thing')
1603 })
1604 })
1605
1606 describe('output', function () {
1607 var filename = pathUtils.join(__dirname, 'streamfile.txt')
1608
1609 afterEach(function () {
1610 return fs.unlink(filename)
1611 })
1612
1613 it('can write to an output stream and wait for end', function () {
1614 app.get('/', function (req, res) {
1615 res.send('contents')
1616 })
1617
1618 return httpism.get(baseurl, {output: fs.createWriteStream(filename)}).then(function () {
1619 expect(fs.readFileSync(filename, 'utf-8')).to.equal('contents')
1620 })
1621 })
1622 })
1623
1624 describe('timeouts', function () {
1625 it('can set the timeout', function () {
1626 app.get('/', function (req, res) {
1627 // don't respond
1628 })
1629
1630 var startTime = Date.now()
1631 return expect(httpism.get(baseurl, {timeout: 20})).to.eventually.be.rejectedWith('timeout').then(function () {
1632 expect(Date.now() - startTime).to.be.within(20, 50)
1633 })
1634 })
1635 })
1636
1637 describe('cache', function () {
1638 var version
1639 var cachePath
1640
1641 beforeEach(function () {
1642 version = 1
1643 cachePath = pathUtils.join(__dirname, 'cache')
1644
1645 app.get('/', function (req, res) {
1646 res.set('x-version', version)
1647 res.send({version: version})
1648 })
1649
1650 app.get('/binary', function (req, res) {
1651 res.set('x-version', version)
1652 res.send(Buffer.from([1, 3, 3, 7, version]))
1653 })
1654
1655 return clearCache()
1656 })
1657
1658 function clearCache () {
1659 return fs.remove(cachePath)
1660 }
1661
1662 it('caches responses', function () {
1663 var http = httpism.client(cache({url: cachePath}), {response: true})
1664 return http.get(baseurl).then(function (response) {
1665 expect(response.headers['x-version']).to.eql('1')
1666 expect(response.body.version).to.equal(1)
1667 }).then(function () {
1668 version++
1669 return http.get(baseurl)
1670 }).then(function (response) {
1671 expect(response.headers['x-version']).to.eql('1')
1672 expect(response.body.version).to.equal(1)
1673 }).then(function () {
1674 return clearCache()
1675 }).then(function () {
1676 return http.get(baseurl)
1677 }).then(function (response) {
1678 expect(response.headers['x-version']).to.eql('2')
1679 expect(response.body.version).to.equal(2)
1680 })
1681 })
1682
1683 function streamToBuffer (stream) {
1684 return new Promise(function (resolve, reject) {
1685 var buffers = []
1686 stream.on('data', function (buffer) {
1687 buffers.push(buffer)
1688 })
1689 stream.on('error', reject)
1690 stream.on('end', function () {
1691 resolve(Buffer.concat(buffers))
1692 })
1693 })
1694 }
1695
1696 it('caches binary responses', function () {
1697 var http = httpism.client(cache({url: cachePath}), {response: true})
1698 return http.get(baseurl + '/binary').then(function (response) {
1699 expect(response.headers['x-version']).to.eql('1')
1700 return streamToBuffer(response.body)
1701 }).then(function (buffer) {
1702 expect(Array.from(buffer.values())).to.eql([1, 3, 3, 7, 1])
1703 }).then(function () {
1704 version++
1705 return http.get(baseurl + '/binary')
1706 }).then(function (response) {
1707 expect(response.headers['x-version']).to.eql('1')
1708 return streamToBuffer(response.body)
1709 }).then(function (buffer) {
1710 expect(Array.from(buffer.values())).to.eql([1, 3, 3, 7, 1])
1711 }).then(function () {
1712 return clearCache()
1713 }).then(function () {
1714 return http.get(baseurl + '/binary')
1715 }).then(function (response) {
1716 expect(response.headers['x-version']).to.eql('2')
1717 return streamToBuffer(response.body)
1718 }).then(function (buffer) {
1719 expect(Array.from(buffer.values())).to.eql([1, 3, 3, 7, 2])
1720 })
1721 })
1722 })
1723})