1 |
|
2 |
|
3 | var chai = require('chai');
|
4 | var cap = require('chai-as-promised');
|
5 | chai.use(cap);
|
6 | var expect = chai.expect;
|
7 | var bluebird = require('bluebird');
|
8 | var then = require('promise');
|
9 | var stream = require('stream');
|
10 | var resumer = require('resumer');
|
11 |
|
12 | var TestServer = require('./server');
|
13 |
|
14 |
|
15 | var fetch = require('../index.js');
|
16 | var Headers = require('../lib/headers.js');
|
17 | var Response = require('../lib/response.js');
|
18 |
|
19 | fetch.Promise = fetch.Promise || bluebird;
|
20 |
|
21 | var url, opts, local, base;
|
22 |
|
23 | describe('node-fetch', function() {
|
24 |
|
25 | before(function(done) {
|
26 | local = new TestServer();
|
27 | base = 'http://' + local.hostname + ':' + local.port;
|
28 | local.start(done);
|
29 | });
|
30 |
|
31 | after(function(done) {
|
32 | local.stop(done);
|
33 | });
|
34 |
|
35 | it('should return a promise', function() {
|
36 | url = 'http://example.com/';
|
37 | var p = fetch(url);
|
38 | expect(p).to.be.an.instanceof(fetch.Promise);
|
39 | expect(p).to.have.property('then');
|
40 | });
|
41 |
|
42 | it('should allow custom promise', function() {
|
43 | url = 'http://example.com/';
|
44 | var old = fetch.Promise;
|
45 | fetch.Promise = then;
|
46 | expect(fetch(url)).to.be.an.instanceof(then);
|
47 | expect(fetch(url)).to.not.be.an.instanceof(bluebird);
|
48 | fetch.Promise = old;
|
49 | });
|
50 |
|
51 | it('should throw error when no promise implementation are found', function() {
|
52 | url = 'http://example.com/';
|
53 | var old = fetch.Promise;
|
54 | fetch.Promise = undefined;
|
55 | expect(function() {
|
56 | fetch(url)
|
57 | }).to.throw(Error);
|
58 | fetch.Promise = old;
|
59 | });
|
60 |
|
61 | it('should reject with error if url is protocol relative', function() {
|
62 | url = '//example.com/';
|
63 | return expect(fetch(url)).to.eventually.be.rejectedWith(Error);
|
64 | });
|
65 |
|
66 | it('should reject with error if url is relative path', function() {
|
67 | url = '/some/path';
|
68 | return expect(fetch(url)).to.eventually.be.rejectedWith(Error);
|
69 | });
|
70 |
|
71 | it('should reject with error if protocol is unsupported', function() {
|
72 | url = 'ftp://example.com/';
|
73 | return expect(fetch(url)).to.eventually.be.rejectedWith(Error);
|
74 | });
|
75 |
|
76 | it('should reject with error on network failure', function() {
|
77 | url = 'http://localhost:50000/';
|
78 | return expect(fetch(url)).to.eventually.be.rejectedWith(Error);
|
79 | });
|
80 |
|
81 | it('should resolve into response', function() {
|
82 | url = base + '/hello';
|
83 | return fetch(url).then(function(res) {
|
84 | expect(res).to.be.an.instanceof(Response);
|
85 | expect(res.headers).to.be.an.instanceof(Headers);
|
86 | expect(res.body).to.be.an.instanceof(stream.Transform);
|
87 | expect(res.bodyUsed).to.be.false;
|
88 |
|
89 | expect(res.url).to.equal(url);
|
90 | expect(res.ok).to.be.true;
|
91 | expect(res.status).to.equal(200);
|
92 | expect(res.statusText).to.equal('OK');
|
93 | });
|
94 | });
|
95 |
|
96 | it('should accept plain text response', function() {
|
97 | url = base + '/plain';
|
98 | return fetch(url).then(function(res) {
|
99 | expect(res.headers.get('content-type')).to.equal('text/plain');
|
100 | return res.text().then(function(result) {
|
101 | expect(res.bodyUsed).to.be.true;
|
102 | expect(result).to.be.a('string');
|
103 | expect(result).to.equal('text');
|
104 | });
|
105 | });
|
106 | });
|
107 |
|
108 | it('should accept html response (like plain text)', function() {
|
109 | url = base + '/html';
|
110 | return fetch(url).then(function(res) {
|
111 | expect(res.headers.get('content-type')).to.equal('text/html');
|
112 | return res.text().then(function(result) {
|
113 | expect(res.bodyUsed).to.be.true;
|
114 | expect(result).to.be.a('string');
|
115 | expect(result).to.equal('<html></html>');
|
116 | });
|
117 | });
|
118 | });
|
119 |
|
120 | it('should accept json response', function() {
|
121 | url = base + '/json';
|
122 | return fetch(url).then(function(res) {
|
123 | expect(res.headers.get('content-type')).to.equal('application/json');
|
124 | return res.json().then(function(result) {
|
125 | expect(res.bodyUsed).to.be.true;
|
126 | expect(result).to.be.an('object');
|
127 | expect(result).to.deep.equal({ name: 'value' });
|
128 | });
|
129 | });
|
130 | });
|
131 |
|
132 | it('should send request with custom headers', function() {
|
133 | url = base + '/inspect';
|
134 | opts = {
|
135 | headers: { 'x-custom-header': 'abc' }
|
136 | };
|
137 | return fetch(url, opts).then(function(res) {
|
138 | return res.json();
|
139 | }).then(function(res) {
|
140 | expect(res.headers['x-custom-header']).to.equal('abc');
|
141 | });
|
142 | });
|
143 |
|
144 | it('should follow redirect code 301', function() {
|
145 | url = base + '/redirect/301';
|
146 | return fetch(url).then(function(res) {
|
147 | expect(res.url).to.equal(base + '/inspect');
|
148 | expect(res.status).to.equal(200);
|
149 | expect(res.ok).to.be.true;
|
150 | });
|
151 | });
|
152 |
|
153 | it('should follow redirect code 302', function() {
|
154 | url = base + '/redirect/302';
|
155 | return fetch(url).then(function(res) {
|
156 | expect(res.url).to.equal(base + '/inspect');
|
157 | expect(res.status).to.equal(200);
|
158 | });
|
159 | });
|
160 |
|
161 | it('should follow redirect code 303', function() {
|
162 | url = base + '/redirect/303';
|
163 | return fetch(url).then(function(res) {
|
164 | expect(res.url).to.equal(base + '/inspect');
|
165 | expect(res.status).to.equal(200);
|
166 | });
|
167 | });
|
168 |
|
169 | it('should follow redirect code 307', function() {
|
170 | url = base + '/redirect/307';
|
171 | return fetch(url).then(function(res) {
|
172 | expect(res.url).to.equal(base + '/inspect');
|
173 | expect(res.status).to.equal(200);
|
174 | });
|
175 | });
|
176 |
|
177 | it('should follow redirect code 308', function() {
|
178 | url = base + '/redirect/308';
|
179 | return fetch(url).then(function(res) {
|
180 | expect(res.url).to.equal(base + '/inspect');
|
181 | expect(res.status).to.equal(200);
|
182 | });
|
183 | });
|
184 |
|
185 | it('should follow redirect chain', function() {
|
186 | url = base + '/redirect/chain';
|
187 | return fetch(url).then(function(res) {
|
188 | expect(res.url).to.equal(base + '/inspect');
|
189 | expect(res.status).to.equal(200);
|
190 | });
|
191 | });
|
192 |
|
193 | it('should obey maximum redirect', function() {
|
194 | url = base + '/redirect/chain';
|
195 | opts = {
|
196 | follow: 1
|
197 | }
|
198 | return expect(fetch(url, opts)).to.eventually.be.rejectedWith(Error);
|
199 | });
|
200 |
|
201 | it('should allow not following redirect', function() {
|
202 | url = base + '/redirect/301';
|
203 | opts = {
|
204 | follow: 0
|
205 | }
|
206 | return expect(fetch(url, opts)).to.eventually.be.rejectedWith(Error);
|
207 | });
|
208 |
|
209 | it('should reject broken redirect', function() {
|
210 | url = base + '/error/redirect';
|
211 | return expect(fetch(url)).to.eventually.be.rejectedWith(Error);
|
212 | });
|
213 |
|
214 | it('should handle client-error response', function() {
|
215 | url = base + '/error/400';
|
216 | return fetch(url).then(function(res) {
|
217 | expect(res.headers.get('content-type')).to.equal('text/plain');
|
218 | expect(res.status).to.equal(400);
|
219 | expect(res.statusText).to.equal('Bad Request');
|
220 | expect(res.ok).to.be.false;
|
221 | return res.text().then(function(result) {
|
222 | expect(res.bodyUsed).to.be.true;
|
223 | expect(result).to.be.a('string');
|
224 | expect(result).to.equal('client error');
|
225 | });
|
226 | });
|
227 | });
|
228 |
|
229 | it('should handle server-error response', function() {
|
230 | url = base + '/error/500';
|
231 | return fetch(url).then(function(res) {
|
232 | expect(res.headers.get('content-type')).to.equal('text/plain');
|
233 | expect(res.status).to.equal(500);
|
234 | expect(res.statusText).to.equal('Internal Server Error');
|
235 | expect(res.ok).to.be.false;
|
236 | return res.text().then(function(result) {
|
237 | expect(res.bodyUsed).to.be.true;
|
238 | expect(result).to.be.a('string');
|
239 | expect(result).to.equal('server error');
|
240 | });
|
241 | });
|
242 | });
|
243 |
|
244 | it('should handle network-error response', function() {
|
245 | url = base + '/error/reset';
|
246 | return expect(fetch(url)).to.eventually.be.rejectedWith(Error);
|
247 | });
|
248 |
|
249 | it('should reject invalid json response', function() {
|
250 | url = base + '/error/json';
|
251 | return fetch(url).then(function(res) {
|
252 | expect(res.headers.get('content-type')).to.equal('application/json');
|
253 | return expect(res.json()).to.eventually.be.rejectedWith(Error);
|
254 | });
|
255 | });
|
256 |
|
257 | it('should handle empty response', function() {
|
258 | url = base + '/empty';
|
259 | return fetch(url).then(function(res) {
|
260 | expect(res.status).to.equal(204);
|
261 | expect(res.statusText).to.equal('No Content');
|
262 | expect(res.ok).to.be.true;
|
263 | return res.text().then(function(result) {
|
264 | expect(result).to.be.a('string');
|
265 | expect(result).to.be.empty;
|
266 | });
|
267 | });
|
268 | });
|
269 |
|
270 | it('should decompress gzip response', function() {
|
271 | url = base + '/gzip';
|
272 | return fetch(url).then(function(res) {
|
273 | expect(res.headers.get('content-type')).to.equal('text/plain');
|
274 | return res.text().then(function(result) {
|
275 | expect(result).to.be.a('string');
|
276 | expect(result).to.equal('hello world');
|
277 | });
|
278 | });
|
279 | });
|
280 |
|
281 | it('should decompress deflate response', function() {
|
282 | url = base + '/deflate';
|
283 | return fetch(url).then(function(res) {
|
284 | expect(res.headers.get('content-type')).to.equal('text/plain');
|
285 | return res.text().then(function(result) {
|
286 | expect(result).to.be.a('string');
|
287 | expect(result).to.equal('hello world');
|
288 | });
|
289 | });
|
290 | });
|
291 |
|
292 | it('should skip decompression if unsupported', function() {
|
293 | url = base + '/sdch';
|
294 | return fetch(url).then(function(res) {
|
295 | expect(res.headers.get('content-type')).to.equal('text/plain');
|
296 | return res.text().then(function(result) {
|
297 | expect(result).to.be.a('string');
|
298 | expect(result).to.equal('fake sdch string');
|
299 | });
|
300 | });
|
301 | });
|
302 |
|
303 | it('should allow disabling auto decompression', function() {
|
304 | url = base + '/gzip';
|
305 | opts = {
|
306 | compress: false
|
307 | };
|
308 | return fetch(url, opts).then(function(res) {
|
309 | expect(res.headers.get('content-type')).to.equal('text/plain');
|
310 | return res.text().then(function(result) {
|
311 | expect(result).to.be.a('string');
|
312 | expect(result).to.not.equal('hello world');
|
313 | });
|
314 | });
|
315 | });
|
316 |
|
317 | it('should allow custom timeout', function() {
|
318 | url = base + '/timeout';
|
319 | opts = {
|
320 | timeout: 100
|
321 | };
|
322 | return expect(fetch(url, opts)).to.eventually.be.rejectedWith(Error);
|
323 | });
|
324 |
|
325 | it('should allow POST request', function() {
|
326 | url = base + '/inspect';
|
327 | opts = {
|
328 | method: 'POST'
|
329 | };
|
330 | return fetch(url, opts).then(function(res) {
|
331 | return res.json();
|
332 | }).then(function(res) {
|
333 | expect(res.method).to.equal('POST');
|
334 | });
|
335 | });
|
336 |
|
337 | it('should allow POST request with string body', function() {
|
338 | url = base + '/inspect';
|
339 | opts = {
|
340 | method: 'POST'
|
341 | , body: 'a=1'
|
342 | };
|
343 | return fetch(url, opts).then(function(res) {
|
344 | return res.json();
|
345 | }).then(function(res) {
|
346 | expect(res.method).to.equal('POST');
|
347 | expect(res.body).to.equal('a=1');
|
348 | });
|
349 | });
|
350 |
|
351 | it('should allow POST request with readable stream as body', function() {
|
352 | url = base + '/inspect';
|
353 | opts = {
|
354 | method: 'POST'
|
355 | , body: resumer().queue('a=1').end()
|
356 | };
|
357 | return fetch(url, opts).then(function(res) {
|
358 | return res.json();
|
359 | }).then(function(res) {
|
360 | expect(res.method).to.equal('POST');
|
361 | expect(res.body).to.equal('a=1');
|
362 | });
|
363 | });
|
364 |
|
365 | it('should allow PUT request', function() {
|
366 | url = base + '/inspect';
|
367 | opts = {
|
368 | method: 'PUT'
|
369 | , body: 'a=1'
|
370 | };
|
371 | return fetch(url, opts).then(function(res) {
|
372 | return res.json();
|
373 | }).then(function(res) {
|
374 | expect(res.method).to.equal('PUT');
|
375 | expect(res.body).to.equal('a=1');
|
376 | });
|
377 | });
|
378 |
|
379 | it('should allow DELETE request', function() {
|
380 | url = base + '/inspect';
|
381 | opts = {
|
382 | method: 'DELETE'
|
383 | };
|
384 | return fetch(url, opts).then(function(res) {
|
385 | return res.json();
|
386 | }).then(function(res) {
|
387 | expect(res.method).to.equal('DELETE');
|
388 | });
|
389 | });
|
390 |
|
391 | it('should allow PATCH request', function() {
|
392 | url = base + '/inspect';
|
393 | opts = {
|
394 | method: 'PATCH'
|
395 | , body: 'a=1'
|
396 | };
|
397 | return fetch(url, opts).then(function(res) {
|
398 | return res.json();
|
399 | }).then(function(res) {
|
400 | expect(res.method).to.equal('PATCH');
|
401 | expect(res.body).to.equal('a=1');
|
402 | });
|
403 | });
|
404 |
|
405 | it('should allow HEAD request', function() {
|
406 | url = base + '/hello';
|
407 | opts = {
|
408 | method: 'HEAD'
|
409 |
|
410 | };
|
411 | return fetch(url, opts).then(function(res) {
|
412 | expect(res.status).to.equal(200);
|
413 | expect(res.statusText).to.equal('OK');
|
414 | expect(res.headers.get('content-type')).to.equal('text/plain');
|
415 | expect(res.body).to.be.an.instanceof(stream.Transform);
|
416 | });
|
417 | });
|
418 |
|
419 | it('should reject decoding body twice', function() {
|
420 | url = base + '/plain';
|
421 | return fetch(url).then(function(res) {
|
422 | expect(res.headers.get('content-type')).to.equal('text/plain');
|
423 | return res.text().then(function(result) {
|
424 | expect(res.bodyUsed).to.be.true;
|
425 | return expect(res.text()).to.eventually.be.rejectedWith(Error);
|
426 | });
|
427 | });
|
428 | });
|
429 |
|
430 | it('should support maximum response size, multiple chunk', function() {
|
431 | url = base + '/size/chunk';
|
432 | opts = {
|
433 | size: 5
|
434 | };
|
435 | return fetch(url, opts).then(function(res) {
|
436 | expect(res.status).to.equal(200);
|
437 | expect(res.headers.get('content-type')).to.equal('text/plain');
|
438 | return expect(res.text()).to.eventually.be.rejectedWith(Error);
|
439 | });
|
440 | });
|
441 |
|
442 | it('should support maximum response size, single chunk', function() {
|
443 | url = base + '/size/long';
|
444 | opts = {
|
445 | size: 5
|
446 | };
|
447 | return fetch(url, opts).then(function(res) {
|
448 | expect(res.status).to.equal(200);
|
449 | expect(res.headers.get('content-type')).to.equal('text/plain');
|
450 | return expect(res.text()).to.eventually.be.rejectedWith(Error);
|
451 | });
|
452 | });
|
453 |
|
454 | it('should support encoding decode, xml dtd detect', function() {
|
455 | url = base + '/encoding/euc-jp';
|
456 | return fetch(url).then(function(res) {
|
457 | expect(res.status).to.equal(200);
|
458 | return res.text().then(function(result) {
|
459 | expect(result).to.equal('<?xml version="1.0" encoding="EUC-JP"?><title>日本語</title>');
|
460 | });
|
461 | });
|
462 | });
|
463 |
|
464 | it('should support encoding decode, content-type detect', function() {
|
465 | url = base + '/encoding/shift-jis';
|
466 | return fetch(url).then(function(res) {
|
467 | expect(res.status).to.equal(200);
|
468 | return res.text().then(function(result) {
|
469 | expect(result).to.equal('<div>日本語</div>');
|
470 | });
|
471 | });
|
472 | });
|
473 |
|
474 | it('should support encoding decode, html5 detect', function() {
|
475 | url = base + '/encoding/gbk';
|
476 | return fetch(url).then(function(res) {
|
477 | expect(res.status).to.equal(200);
|
478 | return res.text().then(function(result) {
|
479 | expect(result).to.equal('<meta charset="gbk"><div>中文</div>');
|
480 | });
|
481 | });
|
482 | });
|
483 |
|
484 | it('should support encoding decode, html4 detect', function() {
|
485 | url = base + '/encoding/gb2312';
|
486 | return fetch(url).then(function(res) {
|
487 | expect(res.status).to.equal(200);
|
488 | return res.text().then(function(result) {
|
489 | expect(result).to.equal('<meta http-equiv="Content-Type" content="text/html; charset=gb2312"><div>中文</div>');
|
490 | });
|
491 | });
|
492 | });
|
493 |
|
494 | it('should default to utf8 encoding', function() {
|
495 | url = base + '/encoding/utf8';
|
496 | return fetch(url).then(function(res) {
|
497 | expect(res.status).to.equal(200);
|
498 | expect(res.headers.get('content-type')).to.be.null;
|
499 | return res.text().then(function(result) {
|
500 | expect(result).to.equal('中文');
|
501 | });
|
502 | });
|
503 | });
|
504 |
|
505 | it('should allow piping response body as stream', function(done) {
|
506 | url = base + '/hello';
|
507 | fetch(url).then(function(res) {
|
508 | expect(res.body).to.be.an.instanceof(stream.Transform);
|
509 | res.body.on('data', function(chunk) {
|
510 | if (chunk === null) {
|
511 | return;
|
512 | }
|
513 | expect(chunk.toString()).to.equal('world');
|
514 | });
|
515 | res.body.on('end', function() {
|
516 | done();
|
517 | });
|
518 | });
|
519 | });
|
520 |
|
521 | it('should allow get all responses of a header', function() {
|
522 | url = base + '/cookie';
|
523 | return fetch(url).then(function(res) {
|
524 | expect(res.headers.get('set-cookie')).to.equal('a=1');
|
525 | expect(res.headers.get('Set-Cookie')).to.equal('a=1');
|
526 | expect(res.headers.getAll('set-cookie')).to.deep.equal(['a=1', 'b=1']);
|
527 | expect(res.headers.getAll('Set-Cookie')).to.deep.equal(['a=1', 'b=1']);
|
528 | });
|
529 | });
|
530 |
|
531 | it('should allow deleting header', function() {
|
532 | url = base + '/cookie';
|
533 | return fetch(url).then(function(res) {
|
534 | res.headers.delete('set-cookie');
|
535 | expect(res.headers.get('set-cookie')).to.be.null;
|
536 | expect(res.headers.getAll('set-cookie')).to.be.empty;
|
537 | });
|
538 | });
|
539 |
|
540 | it('should skip prototype when reading response headers', function() {
|
541 | var FakeHeader = function() {};
|
542 | FakeHeader.prototype.c = 'string';
|
543 | FakeHeader.prototype.d = [1,2,3,4];
|
544 | var res = new FakeHeader;
|
545 | res.a = 'string';
|
546 | res.b = [];
|
547 | var headers = new Headers(res);
|
548 | expect(headers._headers['a']).to.include('string');
|
549 | expect(headers._headers['b']).to.be.undefined;
|
550 | expect(headers._headers['c']).to.be.undefined;
|
551 | expect(headers._headers['d']).to.be.undefined;
|
552 | });
|
553 |
|
554 | it('should support https request', function() {
|
555 | this.timeout(5000);
|
556 | url = 'https://github.com/';
|
557 | opts = {
|
558 | method: 'HEAD'
|
559 | };
|
560 | return fetch(url, opts).then(function(res) {
|
561 | expect(res.status).to.equal(200);
|
562 | expect(res.ok).to.be.true;
|
563 | });
|
564 | });
|
565 |
|
566 | });
|