UNPKG

41.8 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');
12var FormData = require('form-data');
13var http = require('http');
14var fs = require('fs');
15
16var TestServer = require('./server');
17
18// test subjects
19var fetch = require('../index.js');
20var Headers = require('../lib/headers.js');
21var Response = require('../lib/response.js');
22var Request = require('../lib/request.js');
23var Body = require('../lib/body.js');
24var FetchError = require('../lib/fetch-error.js');
25// test with native promise on node 0.11, and bluebird for node 0.10
26fetch.Promise = fetch.Promise || bluebird;
27
28var url, opts, local, base;
29
30describe('node-fetch', function() {
31
32 before(function(done) {
33 local = new TestServer();
34 base = 'http://' + local.hostname + ':' + local.port;
35 local.start(done);
36 });
37
38 after(function(done) {
39 local.stop(done);
40 });
41
42 it('should return a promise', function() {
43 url = 'http://example.com/';
44 var p = fetch(url);
45 expect(p).to.be.an.instanceof(fetch.Promise);
46 expect(p).to.have.property('then');
47 });
48
49 it('should allow custom promise', function() {
50 url = 'http://example.com/';
51 var old = fetch.Promise;
52 fetch.Promise = then;
53 expect(fetch(url)).to.be.an.instanceof(then);
54 expect(fetch(url)).to.not.be.an.instanceof(bluebird);
55 fetch.Promise = old;
56 });
57
58 it('should throw error when no promise implementation are found', function() {
59 url = 'http://example.com/';
60 var old = fetch.Promise;
61 fetch.Promise = undefined;
62 expect(function() {
63 fetch(url)
64 }).to.throw(Error);
65 fetch.Promise = old;
66 });
67
68 it('should expose Headers, Response and Request constructors', function() {
69 expect(fetch.Headers).to.equal(Headers);
70 expect(fetch.Response).to.equal(Response);
71 expect(fetch.Request).to.equal(Request);
72 });
73
74 it('should reject with error if url is protocol relative', function() {
75 url = '//example.com/';
76 return expect(fetch(url)).to.eventually.be.rejectedWith(Error);
77 });
78
79 it('should reject with error if url is relative path', function() {
80 url = '/some/path';
81 return expect(fetch(url)).to.eventually.be.rejectedWith(Error);
82 });
83
84 it('should reject with error if protocol is unsupported', function() {
85 url = 'ftp://example.com/';
86 return expect(fetch(url)).to.eventually.be.rejectedWith(Error);
87 });
88
89 it('should reject with error on network failure', function() {
90 url = 'http://localhost:50000/';
91 return expect(fetch(url)).to.eventually.be.rejected
92 .and.be.an.instanceOf(FetchError)
93 .and.include({ type: 'system', code: 'ECONNREFUSED', errno: 'ECONNREFUSED' });
94 });
95
96 it('should resolve into response', function() {
97 url = base + '/hello';
98 return fetch(url).then(function(res) {
99 expect(res).to.be.an.instanceof(Response);
100 expect(res.headers).to.be.an.instanceof(Headers);
101 expect(res.body).to.be.an.instanceof(stream.Transform);
102 expect(res.bodyUsed).to.be.false;
103
104 expect(res.url).to.equal(url);
105 expect(res.ok).to.be.true;
106 expect(res.status).to.equal(200);
107 expect(res.statusText).to.equal('OK');
108 });
109 });
110
111 it('should accept plain text response', function() {
112 url = base + '/plain';
113 return fetch(url).then(function(res) {
114 expect(res.headers.get('content-type')).to.equal('text/plain');
115 return res.text().then(function(result) {
116 expect(res.bodyUsed).to.be.true;
117 expect(result).to.be.a('string');
118 expect(result).to.equal('text');
119 });
120 });
121 });
122
123 it('should accept html response (like plain text)', function() {
124 url = base + '/html';
125 return fetch(url).then(function(res) {
126 expect(res.headers.get('content-type')).to.equal('text/html');
127 return res.text().then(function(result) {
128 expect(res.bodyUsed).to.be.true;
129 expect(result).to.be.a('string');
130 expect(result).to.equal('<html></html>');
131 });
132 });
133 });
134
135 it('should accept json response', function() {
136 url = base + '/json';
137 return fetch(url).then(function(res) {
138 expect(res.headers.get('content-type')).to.equal('application/json');
139 return res.json().then(function(result) {
140 expect(res.bodyUsed).to.be.true;
141 expect(result).to.be.an('object');
142 expect(result).to.deep.equal({ name: 'value' });
143 });
144 });
145 });
146
147 it('should send request with custom headers', function() {
148 url = base + '/inspect';
149 opts = {
150 headers: { 'x-custom-header': 'abc' }
151 };
152 return fetch(url, opts).then(function(res) {
153 return res.json();
154 }).then(function(res) {
155 expect(res.headers['x-custom-header']).to.equal('abc');
156 });
157 });
158
159 it('should accept headers instance', function() {
160 url = base + '/inspect';
161 opts = {
162 headers: new Headers({ 'x-custom-header': 'abc' })
163 };
164 return fetch(url, opts).then(function(res) {
165 return res.json();
166 }).then(function(res) {
167 expect(res.headers['x-custom-header']).to.equal('abc');
168 });
169 });
170
171 it('should accept custom host header', function() {
172 url = base + '/inspect';
173 opts = {
174 headers: {
175 host: 'example.com'
176 }
177 };
178 return fetch(url, opts).then(function(res) {
179 return res.json();
180 }).then(function(res) {
181 expect(res.headers['host']).to.equal('example.com');
182 });
183 });
184
185 it('should follow redirect code 301', function() {
186 url = base + '/redirect/301';
187 return fetch(url).then(function(res) {
188 expect(res.url).to.equal(base + '/inspect');
189 expect(res.status).to.equal(200);
190 expect(res.ok).to.be.true;
191 });
192 });
193
194 it('should follow redirect code 302', function() {
195 url = base + '/redirect/302';
196 return fetch(url).then(function(res) {
197 expect(res.url).to.equal(base + '/inspect');
198 expect(res.status).to.equal(200);
199 });
200 });
201
202 it('should follow redirect code 303', function() {
203 url = base + '/redirect/303';
204 return fetch(url).then(function(res) {
205 expect(res.url).to.equal(base + '/inspect');
206 expect(res.status).to.equal(200);
207 });
208 });
209
210 it('should follow redirect code 307', function() {
211 url = base + '/redirect/307';
212 return fetch(url).then(function(res) {
213 expect(res.url).to.equal(base + '/inspect');
214 expect(res.status).to.equal(200);
215 });
216 });
217
218 it('should follow redirect code 308', function() {
219 url = base + '/redirect/308';
220 return fetch(url).then(function(res) {
221 expect(res.url).to.equal(base + '/inspect');
222 expect(res.status).to.equal(200);
223 });
224 });
225
226 it('should follow redirect chain', function() {
227 url = base + '/redirect/chain';
228 return fetch(url).then(function(res) {
229 expect(res.url).to.equal(base + '/inspect');
230 expect(res.status).to.equal(200);
231 });
232 });
233
234 it('should follow POST request redirect code 301 with GET', function() {
235 url = base + '/redirect/301';
236 opts = {
237 method: 'POST'
238 , body: 'a=1'
239 };
240 return fetch(url, opts).then(function(res) {
241 expect(res.url).to.equal(base + '/inspect');
242 expect(res.status).to.equal(200);
243 return res.json().then(function(result) {
244 expect(result.method).to.equal('GET');
245 expect(result.body).to.equal('');
246 });
247 });
248 });
249
250 it('should follow POST request redirect code 302 with GET', function() {
251 url = base + '/redirect/302';
252 opts = {
253 method: 'POST'
254 , body: 'a=1'
255 };
256 return fetch(url, opts).then(function(res) {
257 expect(res.url).to.equal(base + '/inspect');
258 expect(res.status).to.equal(200);
259 return res.json().then(function(result) {
260 expect(result.method).to.equal('GET');
261 expect(result.body).to.equal('');
262 });
263 });
264 });
265
266 it('should follow redirect code 303 with GET', function() {
267 url = base + '/redirect/303';
268 opts = {
269 method: 'PUT'
270 , body: 'a=1'
271 };
272 return fetch(url, opts).then(function(res) {
273 expect(res.url).to.equal(base + '/inspect');
274 expect(res.status).to.equal(200);
275 return res.json().then(function(result) {
276 expect(result.method).to.equal('GET');
277 expect(result.body).to.equal('');
278 });
279 });
280 });
281
282 it('should obey maximum redirect, reject case', function() {
283 url = base + '/redirect/chain';
284 opts = {
285 follow: 1
286 }
287 return expect(fetch(url, opts)).to.eventually.be.rejected
288 .and.be.an.instanceOf(FetchError)
289 .and.have.property('type', 'max-redirect');
290 });
291
292 it('should obey redirect chain, resolve case', function() {
293 url = base + '/redirect/chain';
294 opts = {
295 follow: 2
296 }
297 return fetch(url, opts).then(function(res) {
298 expect(res.url).to.equal(base + '/inspect');
299 expect(res.status).to.equal(200);
300 });
301 });
302
303 it('should allow not following redirect', function() {
304 url = base + '/redirect/301';
305 opts = {
306 follow: 0
307 }
308 return expect(fetch(url, opts)).to.eventually.be.rejected
309 .and.be.an.instanceOf(FetchError)
310 .and.have.property('type', 'max-redirect');
311 });
312
313 it('should support redirect mode, manual flag', function() {
314 url = base + '/redirect/301';
315 opts = {
316 redirect: 'manual'
317 };
318 return fetch(url, opts).then(function(res) {
319 expect(res.url).to.equal(url);
320 expect(res.status).to.equal(301);
321 expect(res.headers.get('location')).to.equal(base + '/inspect');
322 });
323 });
324
325 it('should support redirect mode, error flag', function() {
326 url = base + '/redirect/301';
327 opts = {
328 redirect: 'error'
329 };
330 return expect(fetch(url, opts)).to.eventually.be.rejected
331 .and.be.an.instanceOf(FetchError)
332 .and.have.property('type', 'no-redirect');
333 });
334
335 it('should support redirect mode, manual flag when there is no redirect', function() {
336 url = base + '/hello';
337 opts = {
338 redirect: 'manual'
339 };
340 return fetch(url, opts).then(function(res) {
341 expect(res.url).to.equal(url);
342 expect(res.status).to.equal(200);
343 expect(res.headers.get('location')).to.be.null;
344 });
345 });
346
347 it('should follow redirect code 301 and keep existing headers', function() {
348 url = base + '/redirect/301';
349 opts = {
350 headers: new Headers({ 'x-custom-header': 'abc' })
351 };
352 return fetch(url, opts).then(function(res) {
353 expect(res.url).to.equal(base + '/inspect');
354 return res.json();
355 }).then(function(res) {
356 expect(res.headers['x-custom-header']).to.equal('abc');
357 });
358 });
359
360 it('should reject broken redirect', function() {
361 url = base + '/error/redirect';
362 return expect(fetch(url)).to.eventually.be.rejected
363 .and.be.an.instanceOf(FetchError)
364 .and.have.property('type', 'invalid-redirect');
365 });
366
367 it('should not reject broken redirect under manual redirect', function() {
368 url = base + '/error/redirect';
369 opts = {
370 redirect: 'manual'
371 };
372 return fetch(url, opts).then(function(res) {
373 expect(res.url).to.equal(url);
374 expect(res.status).to.equal(301);
375 expect(res.headers.get('location')).to.be.null;
376 });
377 });
378
379 it('should handle client-error response', function() {
380 url = base + '/error/400';
381 return fetch(url).then(function(res) {
382 expect(res.headers.get('content-type')).to.equal('text/plain');
383 expect(res.status).to.equal(400);
384 expect(res.statusText).to.equal('Bad Request');
385 expect(res.ok).to.be.false;
386 return res.text().then(function(result) {
387 expect(res.bodyUsed).to.be.true;
388 expect(result).to.be.a('string');
389 expect(result).to.equal('client error');
390 });
391 });
392 });
393
394 it('should handle server-error response', function() {
395 url = base + '/error/500';
396 return fetch(url).then(function(res) {
397 expect(res.headers.get('content-type')).to.equal('text/plain');
398 expect(res.status).to.equal(500);
399 expect(res.statusText).to.equal('Internal Server Error');
400 expect(res.ok).to.be.false;
401 return res.text().then(function(result) {
402 expect(res.bodyUsed).to.be.true;
403 expect(result).to.be.a('string');
404 expect(result).to.equal('server error');
405 });
406 });
407 });
408
409 it('should handle network-error response', function() {
410 url = base + '/error/reset';
411 return expect(fetch(url)).to.eventually.be.rejected
412 .and.be.an.instanceOf(FetchError)
413 .and.have.property('code', 'ECONNRESET');
414 });
415
416 it('should handle DNS-error response', function() {
417 url = 'http://domain.invalid';
418 return expect(fetch(url)).to.eventually.be.rejected
419 .and.be.an.instanceOf(FetchError)
420 .and.have.property('code', 'ENOTFOUND');
421 });
422
423 it('should reject invalid json response', function() {
424 url = base + '/error/json';
425 return fetch(url).then(function(res) {
426 expect(res.headers.get('content-type')).to.equal('application/json');
427 return expect(res.json()).to.eventually.be.rejectedWith(Error);
428 });
429 });
430
431 it('should handle no content response', function() {
432 url = base + '/no-content';
433 return fetch(url).then(function(res) {
434 expect(res.status).to.equal(204);
435 expect(res.statusText).to.equal('No Content');
436 expect(res.ok).to.be.true;
437 return res.text().then(function(result) {
438 expect(result).to.be.a('string');
439 expect(result).to.be.empty;
440 });
441 });
442 });
443
444 it('should handle no content response with gzip encoding', function() {
445 url = base + '/no-content/gzip';
446 return fetch(url).then(function(res) {
447 expect(res.status).to.equal(204);
448 expect(res.statusText).to.equal('No Content');
449 expect(res.headers.get('content-encoding')).to.equal('gzip');
450 expect(res.ok).to.be.true;
451 return res.text().then(function(result) {
452 expect(result).to.be.a('string');
453 expect(result).to.be.empty;
454 });
455 });
456 });
457
458 it('should handle not modified response', function() {
459 url = base + '/not-modified';
460 return fetch(url).then(function(res) {
461 expect(res.status).to.equal(304);
462 expect(res.statusText).to.equal('Not Modified');
463 expect(res.ok).to.be.false;
464 return res.text().then(function(result) {
465 expect(result).to.be.a('string');
466 expect(result).to.be.empty;
467 });
468 });
469 });
470
471 it('should handle not modified response with gzip encoding', function() {
472 url = base + '/not-modified/gzip';
473 return fetch(url).then(function(res) {
474 expect(res.status).to.equal(304);
475 expect(res.statusText).to.equal('Not Modified');
476 expect(res.headers.get('content-encoding')).to.equal('gzip');
477 expect(res.ok).to.be.false;
478 return res.text().then(function(result) {
479 expect(result).to.be.a('string');
480 expect(result).to.be.empty;
481 });
482 });
483 });
484
485 it('should decompress gzip response', function() {
486 url = base + '/gzip';
487 return fetch(url).then(function(res) {
488 expect(res.headers.get('content-type')).to.equal('text/plain');
489 return res.text().then(function(result) {
490 expect(result).to.be.a('string');
491 expect(result).to.equal('hello world');
492 });
493 });
494 });
495
496 it('should decompress deflate response', function() {
497 url = base + '/deflate';
498 return fetch(url).then(function(res) {
499 expect(res.headers.get('content-type')).to.equal('text/plain');
500 return res.text().then(function(result) {
501 expect(result).to.be.a('string');
502 expect(result).to.equal('hello world');
503 });
504 });
505 });
506
507 it('should decompress deflate raw response from old apache server', function() {
508 url = base + '/deflate-raw';
509 return fetch(url).then(function(res) {
510 expect(res.headers.get('content-type')).to.equal('text/plain');
511 return res.text().then(function(result) {
512 expect(result).to.be.a('string');
513 expect(result).to.equal('hello world');
514 });
515 });
516 });
517
518 it('should skip decompression if unsupported', function() {
519 url = base + '/sdch';
520 return fetch(url).then(function(res) {
521 expect(res.headers.get('content-type')).to.equal('text/plain');
522 return res.text().then(function(result) {
523 expect(result).to.be.a('string');
524 expect(result).to.equal('fake sdch string');
525 });
526 });
527 });
528
529 it('should reject if response compression is invalid', function() {
530 url = base + '/invalid-content-encoding';
531 return fetch(url).then(function(res) {
532 expect(res.headers.get('content-type')).to.equal('text/plain');
533 return expect(res.text()).to.eventually.be.rejected
534 .and.be.an.instanceOf(FetchError)
535 .and.have.property('code', 'Z_DATA_ERROR');
536 });
537 });
538
539 it('should allow disabling auto decompression', function() {
540 url = base + '/gzip';
541 opts = {
542 compress: false
543 };
544 return fetch(url, opts).then(function(res) {
545 expect(res.headers.get('content-type')).to.equal('text/plain');
546 return res.text().then(function(result) {
547 expect(result).to.be.a('string');
548 expect(result).to.not.equal('hello world');
549 });
550 });
551 });
552
553 it('should allow custom timeout', function() {
554 this.timeout(500);
555 url = base + '/timeout';
556 opts = {
557 timeout: 100
558 };
559 return expect(fetch(url, opts)).to.eventually.be.rejected
560 .and.be.an.instanceOf(FetchError)
561 .and.have.property('type', 'request-timeout');
562 });
563
564 it('should allow custom timeout on response body', function() {
565 this.timeout(500);
566 url = base + '/slow';
567 opts = {
568 timeout: 100
569 };
570 return fetch(url, opts).then(function(res) {
571 expect(res.ok).to.be.true;
572 return expect(res.text()).to.eventually.be.rejected
573 .and.be.an.instanceOf(FetchError)
574 .and.have.property('type', 'body-timeout');
575 });
576 });
577
578 it('should clear internal timeout on fetch response', function (done) {
579 this.timeout(1000);
580 spawn('node', ['-e', 'require("./")("' + base + '/hello", { timeout: 5000 })'])
581 .on('exit', function () {
582 done();
583 });
584 });
585
586 it('should clear internal timeout on fetch redirect', function (done) {
587 this.timeout(1000);
588 spawn('node', ['-e', 'require("./")("' + base + '/redirect/301", { timeout: 5000 })'])
589 .on('exit', function () {
590 done();
591 });
592 });
593
594 it('should clear internal timeout on fetch error', function (done) {
595 this.timeout(1000);
596 spawn('node', ['-e', 'require("./")("' + base + '/error/reset", { timeout: 5000 })'])
597 .on('exit', function () {
598 done();
599 });
600 });
601
602 it('should allow POST request', function() {
603 url = base + '/inspect';
604 opts = {
605 method: 'POST'
606 };
607 return fetch(url, opts).then(function(res) {
608 return res.json();
609 }).then(function(res) {
610 expect(res.method).to.equal('POST');
611 expect(res.headers['transfer-encoding']).to.be.undefined;
612 expect(res.headers['content-length']).to.equal('0');
613 });
614 });
615
616 it('should allow POST request with string body', function() {
617 url = base + '/inspect';
618 opts = {
619 method: 'POST'
620 , body: 'a=1'
621 };
622 return fetch(url, opts).then(function(res) {
623 return res.json();
624 }).then(function(res) {
625 expect(res.method).to.equal('POST');
626 expect(res.body).to.equal('a=1');
627 expect(res.headers['transfer-encoding']).to.be.undefined;
628 expect(res.headers['content-length']).to.equal('3');
629 });
630 });
631
632 it('should allow POST request with buffer body', function() {
633 url = base + '/inspect';
634 opts = {
635 method: 'POST'
636 , body: new Buffer('a=1', 'utf-8')
637 };
638 return fetch(url, opts).then(function(res) {
639 return res.json();
640 }).then(function(res) {
641 expect(res.method).to.equal('POST');
642 expect(res.body).to.equal('a=1');
643 expect(res.headers['transfer-encoding']).to.equal('chunked');
644 expect(res.headers['content-length']).to.be.undefined;
645 });
646 });
647
648 it('should allow POST request with readable stream as body', function() {
649 var body = resumer().queue('a=1').end();
650 body = body.pipe(new stream.PassThrough());
651
652 url = base + '/inspect';
653 opts = {
654 method: 'POST'
655 , body: body
656 };
657 return fetch(url, opts).then(function(res) {
658 return res.json();
659 }).then(function(res) {
660 expect(res.method).to.equal('POST');
661 expect(res.body).to.equal('a=1');
662 expect(res.headers['transfer-encoding']).to.equal('chunked');
663 expect(res.headers['content-length']).to.be.undefined;
664 });
665 });
666
667 it('should allow POST request with form-data as body', function() {
668 var form = new FormData();
669 form.append('a','1');
670
671 url = base + '/multipart';
672 opts = {
673 method: 'POST'
674 , body: form
675 };
676 return fetch(url, opts).then(function(res) {
677 return res.json();
678 }).then(function(res) {
679 expect(res.method).to.equal('POST');
680 expect(res.headers['content-type']).to.contain('multipart/form-data');
681 expect(res.headers['content-length']).to.be.a('string');
682 expect(res.body).to.equal('a=1');
683 });
684 });
685
686 it('should allow POST request with form-data using stream as body', function() {
687 var form = new FormData();
688 form.append('my_field', fs.createReadStream('test/dummy.txt'));
689
690 url = base + '/multipart';
691 opts = {
692 method: 'POST'
693 , body: form
694 };
695
696 return fetch(url, opts).then(function(res) {
697 return res.json();
698 }).then(function(res) {
699 expect(res.method).to.equal('POST');
700 expect(res.headers['content-type']).to.contain('multipart/form-data');
701 expect(res.headers['content-length']).to.be.undefined;
702 expect(res.body).to.contain('my_field=');
703 });
704 });
705
706 it('should allow POST request with form-data as body and custom headers', function() {
707 var form = new FormData();
708 form.append('a','1');
709
710 var headers = form.getHeaders();
711 headers['b'] = '2';
712
713 url = base + '/multipart';
714 opts = {
715 method: 'POST'
716 , body: form
717 , headers: headers
718 };
719 return fetch(url, opts).then(function(res) {
720 return res.json();
721 }).then(function(res) {
722 expect(res.method).to.equal('POST');
723 expect(res.headers['content-type']).to.contain('multipart/form-data');
724 expect(res.headers['content-length']).to.be.a('string');
725 expect(res.headers.b).to.equal('2');
726 expect(res.body).to.equal('a=1');
727 });
728 });
729
730 it('should allow POST request with object body', function() {
731 url = base + '/inspect';
732 // note that fetch simply calls tostring on an object
733 opts = {
734 method: 'POST'
735 , body: { a:1 }
736 };
737 return fetch(url, opts).then(function(res) {
738 return res.json();
739 }).then(function(res) {
740 expect(res.method).to.equal('POST');
741 expect(res.body).to.equal('[object Object]');
742 });
743 });
744
745 it('should allow PUT request', function() {
746 url = base + '/inspect';
747 opts = {
748 method: 'PUT'
749 , body: 'a=1'
750 };
751 return fetch(url, opts).then(function(res) {
752 return res.json();
753 }).then(function(res) {
754 expect(res.method).to.equal('PUT');
755 expect(res.body).to.equal('a=1');
756 });
757 });
758
759 it('should allow DELETE request', function() {
760 url = base + '/inspect';
761 opts = {
762 method: 'DELETE'
763 };
764 return fetch(url, opts).then(function(res) {
765 return res.json();
766 }).then(function(res) {
767 expect(res.method).to.equal('DELETE');
768 });
769 });
770
771 it('should allow POST request with string body', function() {
772 url = base + '/inspect';
773 opts = {
774 method: 'POST'
775 , body: 'a=1'
776 };
777 return fetch(url, opts).then(function(res) {
778 return res.json();
779 }).then(function(res) {
780 expect(res.method).to.equal('POST');
781 expect(res.body).to.equal('a=1');
782 expect(res.headers['transfer-encoding']).to.be.undefined;
783 expect(res.headers['content-length']).to.equal('3');
784 });
785 });
786
787 it('should allow DELETE request with string body', function() {
788 url = base + '/inspect';
789 opts = {
790 method: 'DELETE'
791 , body: 'a=1'
792 };
793 return fetch(url, opts).then(function(res) {
794 return res.json();
795 }).then(function(res) {
796 expect(res.method).to.equal('DELETE');
797 expect(res.body).to.equal('a=1');
798 expect(res.headers['transfer-encoding']).to.be.undefined;
799 expect(res.headers['content-length']).to.equal('3');
800 });
801 });
802
803 it('should allow PATCH request', function() {
804 url = base + '/inspect';
805 opts = {
806 method: 'PATCH'
807 , body: 'a=1'
808 };
809 return fetch(url, opts).then(function(res) {
810 return res.json();
811 }).then(function(res) {
812 expect(res.method).to.equal('PATCH');
813 expect(res.body).to.equal('a=1');
814 });
815 });
816
817 it('should allow HEAD request', function() {
818 url = base + '/hello';
819 opts = {
820 method: 'HEAD'
821 };
822 return fetch(url, opts).then(function(res) {
823 expect(res.status).to.equal(200);
824 expect(res.statusText).to.equal('OK');
825 expect(res.headers.get('content-type')).to.equal('text/plain');
826 expect(res.body).to.be.an.instanceof(stream.Transform);
827 return res.text();
828 }).then(function(text) {
829 expect(text).to.equal('');
830 });
831 });
832
833 it('should allow HEAD request with content-encoding header', function() {
834 url = base + '/error/404';
835 opts = {
836 method: 'HEAD'
837 };
838 return fetch(url, opts).then(function(res) {
839 expect(res.status).to.equal(404);
840 expect(res.headers.get('content-encoding')).to.equal('gzip');
841 return res.text();
842 }).then(function(text) {
843 expect(text).to.equal('');
844 });
845 });
846
847 it('should allow OPTIONS request', function() {
848 url = base + '/options';
849 opts = {
850 method: 'OPTIONS'
851 };
852 return fetch(url, opts).then(function(res) {
853 expect(res.status).to.equal(200);
854 expect(res.statusText).to.equal('OK');
855 expect(res.headers.get('allow')).to.equal('GET, HEAD, OPTIONS');
856 expect(res.body).to.be.an.instanceof(stream.Transform);
857 });
858 });
859
860 it('should reject decoding body twice', function() {
861 url = base + '/plain';
862 return fetch(url).then(function(res) {
863 expect(res.headers.get('content-type')).to.equal('text/plain');
864 return res.text().then(function(result) {
865 expect(res.bodyUsed).to.be.true;
866 return expect(res.text()).to.eventually.be.rejectedWith(Error);
867 });
868 });
869 });
870
871 it('should support maximum response size, multiple chunk', function() {
872 url = base + '/size/chunk';
873 opts = {
874 size: 5
875 };
876 return fetch(url, opts).then(function(res) {
877 expect(res.status).to.equal(200);
878 expect(res.headers.get('content-type')).to.equal('text/plain');
879 return expect(res.text()).to.eventually.be.rejected
880 .and.be.an.instanceOf(FetchError)
881 .and.have.property('type', 'max-size');
882 });
883 });
884
885 it('should support maximum response size, single chunk', function() {
886 url = base + '/size/long';
887 opts = {
888 size: 5
889 };
890 return fetch(url, opts).then(function(res) {
891 expect(res.status).to.equal(200);
892 expect(res.headers.get('content-type')).to.equal('text/plain');
893 return expect(res.text()).to.eventually.be.rejected
894 .and.be.an.instanceOf(FetchError)
895 .and.have.property('type', 'max-size');
896 });
897 });
898
899 it('should support encoding decode, xml dtd detect', function() {
900 url = base + '/encoding/euc-jp';
901 return fetch(url).then(function(res) {
902 expect(res.status).to.equal(200);
903 return res.text().then(function(result) {
904 expect(result).to.equal('<?xml version="1.0" encoding="EUC-JP"?><title>日本語</title>');
905 });
906 });
907 });
908
909 it('should support encoding decode, content-type detect', function() {
910 url = base + '/encoding/shift-jis';
911 return fetch(url).then(function(res) {
912 expect(res.status).to.equal(200);
913 return res.text().then(function(result) {
914 expect(result).to.equal('<div>日本語</div>');
915 });
916 });
917 });
918
919 it('should support encoding decode, html5 detect', function() {
920 url = base + '/encoding/gbk';
921 return fetch(url).then(function(res) {
922 expect(res.status).to.equal(200);
923 return res.text().then(function(result) {
924 expect(result).to.equal('<meta charset="gbk"><div>中文</div>');
925 });
926 });
927 });
928
929 it('should support encoding decode, html4 detect', function() {
930 url = base + '/encoding/gb2312';
931 return fetch(url).then(function(res) {
932 expect(res.status).to.equal(200);
933 return res.text().then(function(result) {
934 expect(result).to.equal('<meta http-equiv="Content-Type" content="text/html; charset=gb2312"><div>中文</div>');
935 });
936 });
937 });
938
939 it('should default to utf8 encoding', function() {
940 url = base + '/encoding/utf8';
941 return fetch(url).then(function(res) {
942 expect(res.status).to.equal(200);
943 expect(res.headers.get('content-type')).to.be.null;
944 return res.text().then(function(result) {
945 expect(result).to.equal('中文');
946 });
947 });
948 });
949
950 it('should support uncommon content-type order, charset in front', function() {
951 url = base + '/encoding/order1';
952 return fetch(url).then(function(res) {
953 expect(res.status).to.equal(200);
954 return res.text().then(function(result) {
955 expect(result).to.equal('中文');
956 });
957 });
958 });
959
960 it('should support uncommon content-type order, end with qs', function() {
961 url = base + '/encoding/order2';
962 return fetch(url).then(function(res) {
963 expect(res.status).to.equal(200);
964 return res.text().then(function(result) {
965 expect(result).to.equal('中文');
966 });
967 });
968 });
969
970 it('should support chunked encoding, html4 detect', function() {
971 url = base + '/encoding/chunked';
972 return fetch(url).then(function(res) {
973 expect(res.status).to.equal(200);
974 // because node v0.12 doesn't have str.repeat
975 var padding = new Array(10 + 1).join('a');
976 return res.text().then(function(result) {
977 expect(result).to.equal(padding + '<meta http-equiv="Content-Type" content="text/html; charset=Shift_JIS" /><div>日本語</div>');
978 });
979 });
980 });
981
982 it('should only do encoding detection up to 1024 bytes', function() {
983 url = base + '/encoding/invalid';
984 return fetch(url).then(function(res) {
985 expect(res.status).to.equal(200);
986 // because node v0.12 doesn't have str.repeat
987 var padding = new Array(1200 + 1).join('a');
988 return res.text().then(function(result) {
989 expect(result).to.not.equal(padding + '中文');
990 });
991 });
992 });
993
994 it('should allow piping response body as stream', function(done) {
995 url = base + '/hello';
996 fetch(url).then(function(res) {
997 expect(res.body).to.be.an.instanceof(stream.Transform);
998 res.body.on('data', function(chunk) {
999 if (chunk === null) {
1000 return;
1001 }
1002 expect(chunk.toString()).to.equal('world');
1003 });
1004 res.body.on('end', function() {
1005 done();
1006 });
1007 });
1008 });
1009
1010 it('should allow cloning a response, and use both as stream', function(done) {
1011 url = base + '/hello';
1012 return fetch(url).then(function(res) {
1013 var counter = 0;
1014 var r1 = res.clone();
1015 expect(res.body).to.be.an.instanceof(stream.Transform);
1016 expect(r1.body).to.be.an.instanceof(stream.Transform);
1017 res.body.on('data', function(chunk) {
1018 if (chunk === null) {
1019 return;
1020 }
1021 expect(chunk.toString()).to.equal('world');
1022 });
1023 res.body.on('end', function() {
1024 counter++;
1025 if (counter == 2) {
1026 done();
1027 }
1028 });
1029 r1.body.on('data', function(chunk) {
1030 if (chunk === null) {
1031 return;
1032 }
1033 expect(chunk.toString()).to.equal('world');
1034 });
1035 r1.body.on('end', function() {
1036 counter++;
1037 if (counter == 2) {
1038 done();
1039 }
1040 });
1041 });
1042 });
1043
1044 it('should allow cloning a json response and log it as text response', function() {
1045 url = base + '/json';
1046 return fetch(url).then(function(res) {
1047 var r1 = res.clone();
1048 return fetch.Promise.all([res.json(), r1.text()]).then(function(results) {
1049 expect(results[0]).to.deep.equal({name: 'value'});
1050 expect(results[1]).to.equal('{"name":"value"}');
1051 });
1052 });
1053 });
1054
1055 it('should allow cloning a json response, and then log it as text response', function() {
1056 url = base + '/json';
1057 return fetch(url).then(function(res) {
1058 var r1 = res.clone();
1059 return res.json().then(function(result) {
1060 expect(result).to.deep.equal({name: 'value'});
1061 return r1.text().then(function(result) {
1062 expect(result).to.equal('{"name":"value"}');
1063 });
1064 });
1065 });
1066 });
1067
1068 it('should allow cloning a json response, first log as text response, then return json object', function() {
1069 url = base + '/json';
1070 return fetch(url).then(function(res) {
1071 var r1 = res.clone();
1072 return r1.text().then(function(result) {
1073 expect(result).to.equal('{"name":"value"}');
1074 return res.json().then(function(result) {
1075 expect(result).to.deep.equal({name: 'value'});
1076 });
1077 });
1078 });
1079 });
1080
1081 it('should not allow cloning a response after its been used', function() {
1082 url = base + '/hello';
1083 return fetch(url).then(function(res) {
1084 return res.text().then(function(result) {
1085 expect(function() {
1086 var r1 = res.clone();
1087 }).to.throw(Error);
1088 });
1089 })
1090 });
1091
1092 it('should allow get all responses of a header', function() {
1093 url = base + '/cookie';
1094 return fetch(url).then(function(res) {
1095 expect(res.headers.get('set-cookie')).to.equal('a=1');
1096 expect(res.headers.get('Set-Cookie')).to.equal('a=1');
1097 expect(res.headers.getAll('set-cookie')).to.deep.equal(['a=1', 'b=1']);
1098 expect(res.headers.getAll('Set-Cookie')).to.deep.equal(['a=1', 'b=1']);
1099 });
1100 });
1101
1102 it('should allow iterating through all headers', function() {
1103 var headers = new Headers({
1104 a: 1
1105 , b: [2, 3]
1106 , c: [4]
1107 });
1108 expect(headers).to.have.property('forEach');
1109
1110 var result = [];
1111 headers.forEach(function(val, key) {
1112 result.push([key, val]);
1113 });
1114
1115 expected = [
1116 ["a", "1"]
1117 , ["b", "2"]
1118 , ["b", "3"]
1119 , ["c", "4"]
1120 ];
1121 expect(result).to.deep.equal(expected);
1122 });
1123
1124 it('should allow deleting header', function() {
1125 url = base + '/cookie';
1126 return fetch(url).then(function(res) {
1127 res.headers.delete('set-cookie');
1128 expect(res.headers.get('set-cookie')).to.be.null;
1129 expect(res.headers.getAll('set-cookie')).to.be.empty;
1130 });
1131 });
1132
1133 it('should send request with connection keep-alive if agent is provided', function() {
1134 url = base + '/inspect';
1135 opts = {
1136 agent: new http.Agent({
1137 keepAlive: true
1138 })
1139 };
1140 return fetch(url, opts).then(function(res) {
1141 return res.json();
1142 }).then(function(res) {
1143 expect(res.headers['connection']).to.equal('keep-alive');
1144 });
1145 });
1146
1147 it('should ignore unsupported attributes while reading headers', function() {
1148 var FakeHeader = function() {};
1149 // prototypes are ignored
1150 FakeHeader.prototype.z = 'fake';
1151
1152 var res = new FakeHeader;
1153 // valid
1154 res.a = 'string';
1155 res.b = ['1','2'];
1156 res.c = '';
1157 res.d = [];
1158 // common mistakes, normalized
1159 res.e = 1;
1160 res.f = [1, 2];
1161 // invalid, ignored
1162 res.g = { a:1 };
1163 res.h = undefined;
1164 res.i = null;
1165 res.j = NaN;
1166 res.k = true;
1167 res.l = false;
1168 res.m = new Buffer('test');
1169
1170 var h1 = new Headers(res);
1171
1172 expect(h1._headers['a']).to.include('string');
1173 expect(h1._headers['b']).to.include('1');
1174 expect(h1._headers['b']).to.include('2');
1175 expect(h1._headers['c']).to.include('');
1176 expect(h1._headers['d']).to.be.undefined;
1177
1178 expect(h1._headers['e']).to.include('1');
1179 expect(h1._headers['f']).to.include('1');
1180 expect(h1._headers['f']).to.include('2');
1181
1182 expect(h1._headers['g']).to.be.undefined;
1183 expect(h1._headers['h']).to.be.undefined;
1184 expect(h1._headers['i']).to.be.undefined;
1185 expect(h1._headers['j']).to.be.undefined;
1186 expect(h1._headers['k']).to.be.undefined;
1187 expect(h1._headers['l']).to.be.undefined;
1188 expect(h1._headers['m']).to.be.undefined;
1189
1190 expect(h1._headers['z']).to.be.undefined;
1191 });
1192
1193 it('should wrap headers', function() {
1194 var h1 = new Headers({
1195 a: '1'
1196 });
1197
1198 var h2 = new Headers(h1);
1199 h2.set('b', '1');
1200
1201 var h3 = new Headers(h2);
1202 h3.append('a', '2');
1203
1204 expect(h1._headers['a']).to.include('1');
1205 expect(h1._headers['a']).to.not.include('2');
1206
1207 expect(h2._headers['a']).to.include('1');
1208 expect(h2._headers['a']).to.not.include('2');
1209 expect(h2._headers['b']).to.include('1');
1210
1211 expect(h3._headers['a']).to.include('1');
1212 expect(h3._headers['a']).to.include('2');
1213 expect(h3._headers['b']).to.include('1');
1214 });
1215
1216 it('should support fetch with Request instance', function() {
1217 url = base + '/hello';
1218 var req = new Request(url);
1219 return fetch(req).then(function(res) {
1220 expect(res.url).to.equal(url);
1221 expect(res.ok).to.be.true;
1222 expect(res.status).to.equal(200);
1223 });
1224 });
1225
1226 it('should support wrapping Request instance', function() {
1227 url = base + '/hello';
1228
1229 var form = new FormData();
1230 form.append('a', '1');
1231
1232 var r1 = new Request(url, {
1233 method: 'POST'
1234 , follow: 1
1235 , body: form
1236 });
1237 var r2 = new Request(r1, {
1238 follow: 2
1239 });
1240
1241 expect(r2.url).to.equal(url);
1242 expect(r2.method).to.equal('POST');
1243 // note that we didn't clone the body
1244 expect(r2.body).to.equal(form);
1245 expect(r1.follow).to.equal(1);
1246 expect(r2.follow).to.equal(2);
1247 expect(r1.counter).to.equal(0);
1248 expect(r2.counter).to.equal(0);
1249 });
1250
1251 it('should support overwrite Request instance', function() {
1252 url = base + '/inspect';
1253 var req = new Request(url, {
1254 method: 'POST'
1255 , headers: {
1256 a: '1'
1257 }
1258 });
1259 return fetch(req, {
1260 method: 'GET'
1261 , headers: {
1262 a: '2'
1263 }
1264 }).then(function(res) {
1265 return res.json();
1266 }).then(function(body) {
1267 expect(body.method).to.equal('GET');
1268 expect(body.headers.a).to.equal('2');
1269 });
1270 });
1271
1272 it('should support empty options in Response constructor', function() {
1273 var body = resumer().queue('a=1').end();
1274 body = body.pipe(new stream.PassThrough());
1275 var res = new Response(body);
1276 return res.text().then(function(result) {
1277 expect(result).to.equal('a=1');
1278 });
1279 });
1280
1281 it('should support parsing headers in Response constructor', function() {
1282 var res = new Response(null, {
1283 headers: {
1284 a: '1'
1285 }
1286 });
1287 expect(res.headers.get('a')).to.equal('1');
1288 });
1289
1290 it('should support text() method in Response constructor', function() {
1291 var res = new Response('a=1');
1292 return res.text().then(function(result) {
1293 expect(result).to.equal('a=1');
1294 });
1295 });
1296
1297 it('should support json() method in Response constructor', function() {
1298 var res = new Response('{"a":1}');
1299 return res.json().then(function(result) {
1300 expect(result.a).to.equal(1);
1301 });
1302 });
1303
1304 it('should support buffer() method in Response constructor', function() {
1305 var res = new Response('a=1');
1306 return res.buffer().then(function(result) {
1307 expect(result.toString()).to.equal('a=1');
1308 });
1309 });
1310
1311 it('should support clone() method in Response constructor', function() {
1312 var body = resumer().queue('a=1').end();
1313 body = body.pipe(new stream.PassThrough());
1314 var res = new Response(body, {
1315 headers: {
1316 a: '1'
1317 }
1318 , url: base
1319 , status: 346
1320 , statusText: 'production'
1321 });
1322 var cl = res.clone();
1323 expect(cl.headers.get('a')).to.equal('1');
1324 expect(cl.url).to.equal(base);
1325 expect(cl.status).to.equal(346);
1326 expect(cl.statusText).to.equal('production');
1327 expect(cl.ok).to.be.false;
1328 // clone body shouldn't be the same body
1329 expect(cl.body).to.not.equal(body);
1330 return cl.text().then(function(result) {
1331 expect(result).to.equal('a=1');
1332 });
1333 });
1334
1335 it('should support stream as body in Response constructor', function() {
1336 var body = resumer().queue('a=1').end();
1337 body = body.pipe(new stream.PassThrough());
1338 var res = new Response(body);
1339 return res.text().then(function(result) {
1340 expect(result).to.equal('a=1');
1341 });
1342 });
1343
1344 it('should support string as body in Response constructor', function() {
1345 var res = new Response('a=1');
1346 return res.text().then(function(result) {
1347 expect(result).to.equal('a=1');
1348 });
1349 });
1350
1351 it('should support buffer as body in Response constructor', function() {
1352 var res = new Response(new Buffer('a=1'));
1353 return res.text().then(function(result) {
1354 expect(result).to.equal('a=1');
1355 });
1356 });
1357
1358 it('should default to 200 as status code', function() {
1359 var res = new Response(null);
1360 expect(res.status).to.equal(200);
1361 });
1362
1363 it('should support parsing headers in Request constructor', function() {
1364 url = base;
1365 var req = new Request(url, {
1366 headers: {
1367 a: '1'
1368 }
1369 });
1370 expect(req.url).to.equal(url);
1371 expect(req.headers.get('a')).to.equal('1');
1372 });
1373
1374 it('should support text() method in Request constructor', function() {
1375 url = base;
1376 var req = new Request(url, {
1377 body: 'a=1'
1378 });
1379 expect(req.url).to.equal(url);
1380 return req.text().then(function(result) {
1381 expect(result).to.equal('a=1');
1382 });
1383 });

1384
1385 it('should support json() method in Request constructor', function() {
1386 url = base;
1387 var req = new Request(url, {
1388 body: '{"a":1}'
1389 });
1390 expect(req.url).to.equal(url);
1391 return req.json().then(function(result) {
1392 expect(result.a).to.equal(1);
1393 });
1394 });

1395
1396 it('should support buffer() method in Request constructor', function() {
1397 url = base;
1398 var req = new Request(url, {
1399 body: 'a=1'
1400 });
1401 expect(req.url).to.equal(url);
1402 return req.buffer().then(function(result) {
1403 expect(result.toString()).to.equal('a=1');
1404 });
1405 });

1406
1407 it('should support arbitrary url in Request constructor', function() {
1408 url = 'anything';
1409 var req = new Request(url);
1410 expect(req.url).to.equal('anything');
1411 });
1412
1413 it('should support clone() method in Request constructor', function() {
1414 url = base;
1415 var body = resumer().queue('a=1').end();
1416 body = body.pipe(new stream.PassThrough());
1417 var agent = new http.Agent();
1418 var req = new Request(url, {
1419 body: body
1420 , method: 'POST'
1421 , redirect: 'manual'
1422 , headers: {
1423 b: '2'
1424 }
1425 , follow: 3
1426 , compress: false
1427 , agent: agent
1428 });
1429 var cl = req.clone();
1430 expect(cl.url).to.equal(url);
1431 expect(cl.method).to.equal('POST');
1432 expect(cl.redirect).to.equal('manual');
1433 expect(cl.headers.get('b')).to.equal('2');
1434 expect(cl.follow).to.equal(3);
1435 expect(cl.compress).to.equal(false);
1436 expect(cl.method).to.equal('POST');
1437 expect(cl.counter).to.equal(0);
1438 expect(cl.agent).to.equal(agent);
1439 // clone body shouldn't be the same body
1440 expect(cl.body).to.not.equal(body);
1441 return fetch.Promise.all([cl.text(), req.text()]).then(function(results) {
1442 expect(results[0]).to.equal('a=1');
1443 expect(results[1]).to.equal('a=1');
1444 });
1445 });
1446
1447 it('should support text(), json() and buffer() method in Body constructor', function() {
1448 var body = new Body('a=1');
1449 expect(body).to.have.property('text');
1450 expect(body).to.have.property('json');
1451 expect(body).to.have.property('buffer');
1452 });

1453
1454 it('should create custom FetchError', function() {
1455 var systemError = new Error('system');
1456 systemError.code = 'ESOMEERROR';
1457
1458 var err = new FetchError('test message', 'test-error', systemError);
1459 expect(err).to.be.an.instanceof(Error);
1460 expect(err).to.be.an.instanceof(FetchError);
1461 expect(err.name).to.equal('FetchError');
1462 expect(err.message).to.equal('test message');
1463 expect(err.type).to.equal('test-error');
1464 expect(err.code).to.equal('ESOMEERROR');
1465 expect(err.errno).to.equal('ESOMEERROR');
1466 });

1467
1468 it('should support https request', function() {
1469 this.timeout(5000);
1470 url = 'https://github.com/';
1471 opts = {
1472 method: 'HEAD'
1473 };
1474 return fetch(url, opts).then(function(res) {
1475 expect(res.status).to.equal(200);
1476 expect(res.ok).to.be.true;
1477 });
1478 });
1479
1480});