UNPKG

21.3 kBJavaScriptView Raw
1
2// test tools
3var chai = require('chai');
4var cap = require('chai-as-promised');
5chai.use(cap);
6var expect = chai.expect;
7var bluebird = require('bluebird');
8var then = require('promise');
9var spawn = require('child_process').spawn;
10var stream = require('stream');
11var resumer = require('resumer');
12
13var TestServer = require('./server');
14
15// test subjects
16var fetch = require('../index.js');
17var Headers = require('../lib/headers.js');
18var Response = require('../lib/response.js');
19var Request = require('../lib/request.js');
20// test with native promise on node 0.11, and bluebird for node 0.10
21fetch.Promise = fetch.Promise || bluebird;
22
23var url, opts, local, base;
24
25describe('node-fetch', function() {
26
27 before(function(done) {
28 local = new TestServer();
29 base = 'http://' + local.hostname + ':' + local.port;
30 local.start(done);
31 });
32
33 after(function(done) {
34 local.stop(done);
35 });
36
37 it('should return a promise', function() {
38 url = 'http://example.com/';
39 var p = fetch(url);
40 expect(p).to.be.an.instanceof(fetch.Promise);
41 expect(p).to.have.property('then');
42 });
43
44 it('should allow custom promise', function() {
45 url = 'http://example.com/';
46 var old = fetch.Promise;
47 fetch.Promise = then;
48 expect(fetch(url)).to.be.an.instanceof(then);
49 expect(fetch(url)).to.not.be.an.instanceof(bluebird);
50 fetch.Promise = old;
51 });
52
53 it('should throw error when no promise implementation are found', function() {
54 url = 'http://example.com/';
55 var old = fetch.Promise;
56 fetch.Promise = undefined;
57 expect(function() {
58 fetch(url)
59 }).to.throw(Error);
60 fetch.Promise = old;
61 });
62
63 it('should expose Headers, Response and Request constructors', function() {
64 expect(fetch.Headers).to.equal(Headers);
65 expect(fetch.Response).to.equal(Response);
66 expect(fetch.Request).to.equal(Request);
67 });
68
69 it('should reject with error if url is protocol relative', function() {
70 url = '//example.com/';
71 return expect(fetch(url)).to.eventually.be.rejectedWith(Error);
72 });
73
74 it('should reject with error if url is relative path', function() {
75 url = '/some/path';
76 return expect(fetch(url)).to.eventually.be.rejectedWith(Error);
77 });
78
79 it('should reject with error if protocol is unsupported', function() {
80 url = 'ftp://example.com/';
81 return expect(fetch(url)).to.eventually.be.rejectedWith(Error);
82 });
83
84 it('should reject with error on network failure', function() {
85 url = 'http://localhost:50000/';
86 return expect(fetch(url)).to.eventually.be.rejectedWith(Error);
87 });
88
89 it('should resolve into response', function() {
90 url = base + '/hello';
91 return fetch(url).then(function(res) {
92 expect(res).to.be.an.instanceof(Response);
93 expect(res.headers).to.be.an.instanceof(Headers);
94 expect(res.body).to.be.an.instanceof(stream.Transform);
95 expect(res.bodyUsed).to.be.false;
96
97 expect(res.url).to.equal(url);
98 expect(res.ok).to.be.true;
99 expect(res.status).to.equal(200);
100 expect(res.statusText).to.equal('OK');
101 });
102 });
103
104 it('should accept plain text response', function() {
105 url = base + '/plain';
106 return fetch(url).then(function(res) {
107 expect(res.headers.get('content-type')).to.equal('text/plain');
108 return res.text().then(function(result) {
109 expect(res.bodyUsed).to.be.true;
110 expect(result).to.be.a('string');
111 expect(result).to.equal('text');
112 });
113 });
114 });
115
116 it('should accept html response (like plain text)', function() {
117 url = base + '/html';
118 return fetch(url).then(function(res) {
119 expect(res.headers.get('content-type')).to.equal('text/html');
120 return res.text().then(function(result) {
121 expect(res.bodyUsed).to.be.true;
122 expect(result).to.be.a('string');
123 expect(result).to.equal('<html></html>');
124 });
125 });
126 });
127
128 it('should accept json response', function() {
129 url = base + '/json';
130 return fetch(url).then(function(res) {
131 expect(res.headers.get('content-type')).to.equal('application/json');
132 return res.json().then(function(result) {
133 expect(res.bodyUsed).to.be.true;
134 expect(result).to.be.an('object');
135 expect(result).to.deep.equal({ name: 'value' });
136 });
137 });
138 });
139
140 it('should send request with custom headers', function() {
141 url = base + '/inspect';
142 opts = {
143 headers: { 'x-custom-header': 'abc' }
144 };
145 return fetch(url, opts).then(function(res) {
146 return res.json();
147 }).then(function(res) {
148 expect(res.headers['x-custom-header']).to.equal('abc');
149 });
150 });
151
152 it('should accept headers instance', function() {
153 url = base + '/inspect';
154 opts = {
155 headers: new Headers({ 'x-custom-header': 'abc' })
156 };
157 return fetch(url, opts).then(function(res) {
158 return res.json();
159 }).then(function(res) {
160 expect(res.headers['x-custom-header']).to.equal('abc');
161 });
162 });
163
164 it('should accept custom host header', function() {
165 url = base + '/inspect';
166 opts = {
167 headers: {
168 host: 'example.com'
169 }
170 };
171 return fetch(url, opts).then(function(res) {
172 return res.json();
173 }).then(function(res) {
174 expect(res.headers['host']).to.equal('example.com');
175 });
176 });
177
178 it('should follow redirect code 301', function() {
179 url = base + '/redirect/301';
180 return fetch(url).then(function(res) {
181 expect(res.url).to.equal(base + '/inspect');
182 expect(res.status).to.equal(200);
183 expect(res.ok).to.be.true;
184 });
185 });
186
187 it('should follow redirect code 302', function() {
188 url = base + '/redirect/302';
189 return fetch(url).then(function(res) {
190 expect(res.url).to.equal(base + '/inspect');
191 expect(res.status).to.equal(200);
192 });
193 });
194
195 it('should follow redirect code 303', function() {
196 url = base + '/redirect/303';
197 return fetch(url).then(function(res) {
198 expect(res.url).to.equal(base + '/inspect');
199 expect(res.status).to.equal(200);
200 });
201 });
202
203 it('should follow redirect code 307', function() {
204 url = base + '/redirect/307';
205 return fetch(url).then(function(res) {
206 expect(res.url).to.equal(base + '/inspect');
207 expect(res.status).to.equal(200);
208 });
209 });
210
211 it('should follow redirect code 308', function() {
212 url = base + '/redirect/308';
213 return fetch(url).then(function(res) {
214 expect(res.url).to.equal(base + '/inspect');
215 expect(res.status).to.equal(200);
216 });
217 });
218
219 it('should follow redirect chain', function() {
220 url = base + '/redirect/chain';
221 return fetch(url).then(function(res) {
222 expect(res.url).to.equal(base + '/inspect');
223 expect(res.status).to.equal(200);
224 });
225 });
226
227 it('should obey maximum redirect', function() {
228 url = base + '/redirect/chain';
229 opts = {
230 follow: 1
231 }
232 return expect(fetch(url, opts)).to.eventually.be.rejectedWith(Error);
233 });
234
235 it('should allow not following redirect', function() {
236 url = base + '/redirect/301';
237 opts = {
238 follow: 0
239 }
240 return expect(fetch(url, opts)).to.eventually.be.rejectedWith(Error);
241 });
242
243 it('should follow redirect code 301 and keep existing headers', function() {
244 url = base + '/redirect/301';
245 opts = {
246 headers: new Headers({ 'x-custom-header': 'abc' })
247 };
248 return fetch(url, opts).then(function(res) {
249 expect(res.url).to.equal(base + '/inspect');
250 return res.json();
251 }).then(function(res) {
252 expect(res.headers['x-custom-header']).to.equal('abc');
253 });
254 });
255
256 it('should reject broken redirect', function() {
257 url = base + '/error/redirect';
258 return expect(fetch(url)).to.eventually.be.rejectedWith(Error);
259 });
260
261 it('should handle client-error response', function() {
262 url = base + '/error/400';
263 return fetch(url).then(function(res) {
264 expect(res.headers.get('content-type')).to.equal('text/plain');
265 expect(res.status).to.equal(400);
266 expect(res.statusText).to.equal('Bad Request');
267 expect(res.ok).to.be.false;
268 return res.text().then(function(result) {
269 expect(res.bodyUsed).to.be.true;
270 expect(result).to.be.a('string');
271 expect(result).to.equal('client error');
272 });
273 });
274 });
275
276 it('should handle server-error response', function() {
277 url = base + '/error/500';
278 return fetch(url).then(function(res) {
279 expect(res.headers.get('content-type')).to.equal('text/plain');
280 expect(res.status).to.equal(500);
281 expect(res.statusText).to.equal('Internal Server Error');
282 expect(res.ok).to.be.false;
283 return res.text().then(function(result) {
284 expect(res.bodyUsed).to.be.true;
285 expect(result).to.be.a('string');
286 expect(result).to.equal('server error');
287 });
288 });
289 });
290
291 it('should handle network-error response', function() {
292 url = base + '/error/reset';
293 return expect(fetch(url)).to.eventually.be.rejectedWith(Error);
294 });
295
296 it('should reject invalid json response', function() {
297 url = base + '/error/json';
298 return fetch(url).then(function(res) {
299 expect(res.headers.get('content-type')).to.equal('application/json');
300 return expect(res.json()).to.eventually.be.rejectedWith(Error);
301 });
302 });
303
304 it('should handle empty response', function() {
305 url = base + '/empty';
306 return fetch(url).then(function(res) {
307 expect(res.status).to.equal(204);
308 expect(res.statusText).to.equal('No Content');
309 expect(res.ok).to.be.true;
310 return res.text().then(function(result) {
311 expect(result).to.be.a('string');
312 expect(result).to.be.empty;
313 });
314 });
315 });
316
317 it('should decompress gzip response', function() {
318 url = base + '/gzip';
319 return fetch(url).then(function(res) {
320 expect(res.headers.get('content-type')).to.equal('text/plain');
321 return res.text().then(function(result) {
322 expect(result).to.be.a('string');
323 expect(result).to.equal('hello world');
324 });
325 });
326 });
327
328 it('should decompress deflate response', function() {
329 url = base + '/deflate';
330 return fetch(url).then(function(res) {
331 expect(res.headers.get('content-type')).to.equal('text/plain');
332 return res.text().then(function(result) {
333 expect(result).to.be.a('string');
334 expect(result).to.equal('hello world');
335 });
336 });
337 });
338
339 it('should skip decompression if unsupported', function() {
340 url = base + '/sdch';
341 return fetch(url).then(function(res) {
342 expect(res.headers.get('content-type')).to.equal('text/plain');
343 return res.text().then(function(result) {
344 expect(result).to.be.a('string');
345 expect(result).to.equal('fake sdch string');
346 });
347 });
348 });
349
350 it('should allow disabling auto decompression', function() {
351 url = base + '/gzip';
352 opts = {
353 compress: false
354 };
355 return fetch(url, opts).then(function(res) {
356 expect(res.headers.get('content-type')).to.equal('text/plain');
357 return res.text().then(function(result) {
358 expect(result).to.be.a('string');
359 expect(result).to.not.equal('hello world');
360 });
361 });
362 });
363
364 it('should allow custom timeout', function() {
365 this.timeout(500);
366 url = base + '/timeout';
367 opts = {
368 timeout: 100
369 };
370 return expect(fetch(url, opts)).to.eventually.be.rejectedWith(Error);
371 });
372
373 it('should allow custom timeout on response body', function() {
374 this.timeout(500);
375 url = base + '/slow';
376 opts = {
377 timeout: 100
378 };
379 return fetch(url, opts).then(function(res) {
380 expect(res.ok).to.be.true;
381 return expect(res.text()).to.eventually.be.rejectedWith(Error);
382 });
383 });
384
385 it('should clear internal timeout on fetch response', function (done) {
386 this.timeout(1000);
387 spawn('node', ['-e', 'require("./")("' + base + '/hello", { timeout: 5000 })'])
388 .on('exit', function () {
389 done();
390 });
391 });
392
393 it('should clear internal timeout on fetch redirect', function (done) {
394 this.timeout(1000);
395 spawn('node', ['-e', 'require("./")("' + base + '/redirect/301", { timeout: 5000 })'])
396 .on('exit', function () {
397 done();
398 });
399 });
400
401 it('should clear internal timeout on fetch error', function (done) {
402 this.timeout(1000);
403 spawn('node', ['-e', 'require("./")("' + base + '/error/reset", { timeout: 5000 })'])
404 .on('exit', function () {
405 done();
406 });
407 });
408
409 it('should allow POST request', function() {
410 url = base + '/inspect';
411 opts = {
412 method: 'POST'
413 };
414 return fetch(url, opts).then(function(res) {
415 return res.json();
416 }).then(function(res) {
417 expect(res.method).to.equal('POST');
418 });
419 });
420
421 it('should allow POST request with string body', function() {
422 url = base + '/inspect';
423 opts = {
424 method: 'POST'
425 , body: 'a=1'
426 };
427 return fetch(url, opts).then(function(res) {
428 return res.json();
429 }).then(function(res) {
430 expect(res.method).to.equal('POST');
431 expect(res.body).to.equal('a=1');
432 });
433 });
434
435 it('should allow POST request with readable stream as body', function() {
436 url = base + '/inspect';
437 opts = {
438 method: 'POST'
439 , body: resumer().queue('a=1').end()
440 };
441 return fetch(url, opts).then(function(res) {
442 return res.json();
443 }).then(function(res) {
444 expect(res.method).to.equal('POST');
445 expect(res.body).to.equal('a=1');
446 });
447 });
448
449 it('should allow PUT request', function() {
450 url = base + '/inspect';
451 opts = {
452 method: 'PUT'
453 , body: 'a=1'
454 };
455 return fetch(url, opts).then(function(res) {
456 return res.json();
457 }).then(function(res) {
458 expect(res.method).to.equal('PUT');
459 expect(res.body).to.equal('a=1');
460 });
461 });
462
463 it('should allow DELETE request', function() {
464 url = base + '/inspect';
465 opts = {
466 method: 'DELETE'
467 };
468 return fetch(url, opts).then(function(res) {
469 return res.json();
470 }).then(function(res) {
471 expect(res.method).to.equal('DELETE');
472 });
473 });
474
475 it('should allow PATCH request', function() {
476 url = base + '/inspect';
477 opts = {
478 method: 'PATCH'
479 , body: 'a=1'
480 };
481 return fetch(url, opts).then(function(res) {
482 return res.json();
483 }).then(function(res) {
484 expect(res.method).to.equal('PATCH');
485 expect(res.body).to.equal('a=1');
486 });
487 });
488
489 it('should allow HEAD request', function() {
490 url = base + '/hello';
491 opts = {
492 method: 'HEAD'
493 };
494 return fetch(url, opts).then(function(res) {
495 expect(res.status).to.equal(200);
496 expect(res.statusText).to.equal('OK');
497 expect(res.headers.get('content-type')).to.equal('text/plain');
498 expect(res.body).to.be.an.instanceof(stream.Transform);
499 });
500 });
501
502 it('should reject decoding body twice', function() {
503 url = base + '/plain';
504 return fetch(url).then(function(res) {
505 expect(res.headers.get('content-type')).to.equal('text/plain');
506 return res.text().then(function(result) {
507 expect(res.bodyUsed).to.be.true;
508 return expect(res.text()).to.eventually.be.rejectedWith(Error);
509 });
510 });
511 });
512
513 it('should support maximum response size, multiple chunk', function() {
514 url = base + '/size/chunk';
515 opts = {
516 size: 5
517 };
518 return fetch(url, opts).then(function(res) {
519 expect(res.status).to.equal(200);
520 expect(res.headers.get('content-type')).to.equal('text/plain');
521 return expect(res.text()).to.eventually.be.rejectedWith(Error);
522 });
523 });
524
525 it('should support maximum response size, single chunk', function() {
526 url = base + '/size/long';
527 opts = {
528 size: 5
529 };
530 return fetch(url, opts).then(function(res) {
531 expect(res.status).to.equal(200);
532 expect(res.headers.get('content-type')).to.equal('text/plain');
533 return expect(res.text()).to.eventually.be.rejectedWith(Error);
534 });
535 });
536
537 it('should support encoding decode, xml dtd detect', function() {
538 url = base + '/encoding/euc-jp';
539 return fetch(url).then(function(res) {
540 expect(res.status).to.equal(200);
541 return res.text().then(function(result) {
542 expect(result).to.equal('<?xml version="1.0" encoding="EUC-JP"?><title>日本語</title>');
543 });
544 });
545 });
546
547 it('should support encoding decode, content-type detect', function() {
548 url = base + '/encoding/shift-jis';
549 return fetch(url).then(function(res) {
550 expect(res.status).to.equal(200);
551 return res.text().then(function(result) {
552 expect(result).to.equal('<div>日本語</div>');
553 });
554 });
555 });
556
557 it('should support encoding decode, html5 detect', function() {
558 url = base + '/encoding/gbk';
559 return fetch(url).then(function(res) {
560 expect(res.status).to.equal(200);
561 return res.text().then(function(result) {
562 expect(result).to.equal('<meta charset="gbk"><div>中文</div>');
563 });
564 });
565 });
566
567 it('should support encoding decode, html4 detect', function() {
568 url = base + '/encoding/gb2312';
569 return fetch(url).then(function(res) {
570 expect(res.status).to.equal(200);
571 return res.text().then(function(result) {
572 expect(result).to.equal('<meta http-equiv="Content-Type" content="text/html; charset=gb2312"><div>中文</div>');
573 });
574 });
575 });
576
577 it('should default to utf8 encoding', function() {
578 url = base + '/encoding/utf8';
579 return fetch(url).then(function(res) {
580 expect(res.status).to.equal(200);
581 expect(res.headers.get('content-type')).to.be.null;
582 return res.text().then(function(result) {
583 expect(result).to.equal('中文');
584 });
585 });
586 });
587
588 it('should support uncommon content-type order, charset in front', function() {
589 url = base + '/encoding/order1';
590 return fetch(url).then(function(res) {
591 expect(res.status).to.equal(200);
592 return res.text().then(function(result) {
593 expect(result).to.equal('中文');
594 });
595 });
596 });
597
598 it('should support uncommon content-type order, end with qs', function() {
599 url = base + '/encoding/order2';
600 return fetch(url).then(function(res) {
601 expect(res.status).to.equal(200);
602 return res.text().then(function(result) {
603 expect(result).to.equal('中文');
604 });
605 });
606 });
607
608 it('should allow piping response body as stream', function(done) {
609 url = base + '/hello';
610 fetch(url).then(function(res) {
611 expect(res.body).to.be.an.instanceof(stream.Transform);
612 res.body.on('data', function(chunk) {
613 if (chunk === null) {
614 return;
615 }
616 expect(chunk.toString()).to.equal('world');
617 });
618 res.body.on('end', function() {
619 done();
620 });
621 });
622 });
623
624 it('should allow get all responses of a header', function() {
625 url = base + '/cookie';
626 return fetch(url).then(function(res) {
627 expect(res.headers.get('set-cookie')).to.equal('a=1');
628 expect(res.headers.get('Set-Cookie')).to.equal('a=1');
629 expect(res.headers.getAll('set-cookie')).to.deep.equal(['a=1', 'b=1']);
630 expect(res.headers.getAll('Set-Cookie')).to.deep.equal(['a=1', 'b=1']);
631 });
632 });
633
634 it('should allow deleting header', function() {
635 url = base + '/cookie';
636 return fetch(url).then(function(res) {
637 res.headers.delete('set-cookie');
638 expect(res.headers.get('set-cookie')).to.be.null;
639 expect(res.headers.getAll('set-cookie')).to.be.empty;
640 });
641 });
642
643 it('should ignore unsupported attributes while reading headers', function() {
644 var FakeHeader = function() {};
645 // prototypes are ignored
646 FakeHeader.prototype.z = 'fake';
647
648 var res = new FakeHeader;
649 // valid
650 res.a = 'string';
651 res.b = ['1','2'];
652 res.c = '';
653 res.d = [];
654 // common mistakes, normalized
655 res.e = 1;
656 res.f = [1, 2];
657 // invalid, ignored
658 res.g = { a:1 };
659 res.h = undefined;
660 res.i = null;
661 res.j = NaN;
662 res.k = true;
663 res.l = false;
664
665 var h1 = new Headers(res);
666
667 expect(h1._headers['a']).to.include('string');
668 expect(h1._headers['b']).to.include('1');
669 expect(h1._headers['b']).to.include('2');
670 expect(h1._headers['c']).to.include('');
671 expect(h1._headers['d']).to.be.undefined;
672
673 expect(h1._headers['e']).to.include('1');
674 expect(h1._headers['f']).to.include('1');
675 expect(h1._headers['f']).to.include('2');
676
677 expect(h1._headers['g']).to.be.undefined;
678 expect(h1._headers['h']).to.be.undefined;
679 expect(h1._headers['i']).to.be.undefined;
680 expect(h1._headers['j']).to.be.undefined;
681 expect(h1._headers['k']).to.be.undefined;
682 expect(h1._headers['l']).to.be.undefined;
683
684 expect(h1._headers['z']).to.be.undefined;
685 });
686
687 it('should wrap headers', function() {
688 var h1 = new Headers({
689 a: '1'
690 });
691
692 var h2 = new Headers(h1);
693 h2.set('b', '1');
694
695 var h3 = new Headers(h2);
696 h3.append('a', '2');
697
698 expect(h1._headers['a']).to.include('1');
699 expect(h1._headers['a']).to.not.include('2');
700 expect(h1._headers['b']).to.not.include('1');
701
702 expect(h2._headers['a']).to.include('1');
703 expect(h2._headers['a']).to.not.include('2');
704 expect(h2._headers['b']).to.include('1');
705
706 expect(h3._headers['a']).to.include('1');
707 expect(h3._headers['a']).to.include('2');
708 expect(h3._headers['b']).to.include('1');
709 });
710
711 it('should support fetch with Request instance', function() {
712 url = base + '/hello';
713 var req = new Request(url);
714 return fetch(req).then(function(res) {
715 expect(res.url).to.equal(url);
716 expect(res.ok).to.be.true;
717 expect(res.status).to.equal(200);
718 });
719 });
720
721 it('should support wrapping Request instance', function() {
722 url = base + '/hello';
723 var r1 = new Request(url, {
724 method: 'POST'
725 , follow: 1
726 });
727 var r2 = new Request(r1, {
728 follow: 2
729 })
730 expect(r2.url).to.equal(url);
731 expect(r2.method).to.equal('POST');
732 expect(r1.follow).to.equal(1);
733 expect(r2.follow).to.equal(2);
734 });
735
736 it('should support overwrite Request instance', function() {
737 url = base + '/inspect';
738 var req = new Request(url, {
739 method: 'POST'
740 , headers: {
741 a: '1'
742 }
743 });
744 return fetch(req, {
745 method: 'GET'
746 , headers: {
747 a: '2'
748 }
749 }).then(function(res) {
750 return res.json();
751 }).then(function(body) {
752 expect(body.method).to.equal('GET');
753 expect(body.headers.a).to.equal('2');
754 });
755 });
756
757 it('should support https request', function() {
758 this.timeout(5000);
759 url = 'https://github.com/';
760 opts = {
761 method: 'HEAD'
762 };
763 return fetch(url, opts).then(function(res) {
764 expect(res.status).to.equal(200);
765 expect(res.ok).to.be.true;
766 });
767 });
768
769});