diff --git a/.travis.yml b/.travis.yml index 668a450..966ed0d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,4 +3,4 @@ node_js: - "0.10" - "0.11" before_install: npm install -g npm -script: npm test \ No newline at end of file +script: npm run coverage \ No newline at end of file diff --git a/README.md b/README.md index 271e1eb..d74dcc4 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ node-fetch [![npm version][npm-image]][npm-url] [![build status][travis-image]][travis-url] +[![coverage status][coveralls-image]][coveralls-url] A light-weight module that brings `window.fetch` to node.js @@ -57,3 +58,5 @@ Thanks to [github/fetch](https://github.com/github/fetch) for providing a solid [npm-url]: https://www.npmjs.com/package/node-fetch [travis-image]: https://img.shields.io/travis/bitinn/node-fetch.svg?style=flat-square [travis-url]: https://travis-ci.org/bitinn/node-fetch +[coveralls-image]: https://img.shields.io/coveralls/bitinn/node-fetch.svg?style=flat-square +[coveralls-url]: https://coveralls.io/r/bitinn/node-fetch diff --git a/index.js b/index.js index 774f3fc..f3f2b3b 100644 --- a/index.js +++ b/index.js @@ -66,7 +66,7 @@ function Fetch(url, opts) { var options = { hostname: uri.hostname , port: uri.port - , path: uri.path || '/' + , path: uri.path , auth: uri.auth , method: opts.method || 'GET' , headers: opts.headers || {} @@ -156,6 +156,7 @@ function Fetch(url, opts) { url: uri.href , status: res.statusCode , headers: headers + , size: options.size }); resolve(output); diff --git a/lib/response.js b/lib/response.js index aedcc02..53a70cc 100644 --- a/lib/response.js +++ b/lib/response.js @@ -6,7 +6,6 @@ */ var http = require('http'); -var stream = require('stream'); var convert = require('encoding').convert; module.exports = Response; @@ -24,8 +23,9 @@ function Response(body, opts) { this.status = opts.status; this.statusText = http.STATUS_CODES[this.status]; this.headers = opts.headers; - this.body = body.pipe(new stream.PassThrough()); + this.body = body; this.bodyUsed = false; + this.size = opts.size; } @@ -73,6 +73,10 @@ Response.prototype._decode = function() { return new Response.Promise(function(resolve, reject) { self.body.on('data', function(chunk) { + if (self._abort) { + return; + } + if (chunk === null) { return; } @@ -80,7 +84,6 @@ Response.prototype._decode = function() { if (self.size && self._bytes > self.size) { self._abort = true; reject(new Error('content size at ' + self.url + ' over limit: ' + self.size)); - self.body.abort(); return; } @@ -137,7 +140,7 @@ Response.prototype._convert = function(encoding) { charset = res.pop(); // prevent decode issues when sites use incorrect encoding - // see: https://hsivonen.fi/encoding-menu/ + // ref: https://hsivonen.fi/encoding-menu/ if (charset === 'gb2312' || charset === 'gbk') { charset = 'gb18030'; } diff --git a/package.json b/package.json index 99894ba..1d95bc7 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,9 @@ "description": "A light-weight module that brings window.fetch to node.js", "main": "index.js", "scripts": { - "test": "mocha test/test.js" + "test": "mocha test/test.js", + "report": "istanbul cover _mocha -- -R spec test/test.js", + "coverage": "istanbul cover _mocha --report lcovonly -- -R spec test/test.js && cat ./coverage/lcov.info | coveralls" }, "repository": { "type": "git", @@ -25,6 +27,8 @@ "bluebird": "^2.9.1", "chai": "^1.10.0", "chai-as-promised": "^4.1.1", + "coveralls": "^2.11.2", + "istanbul": "^0.3.5", "mocha": "^2.1.0", "promise": "^6.1.0", "resumer": "0.0.0" diff --git a/test/server.js b/test/server.js index 02f2931..3368ebd 100644 --- a/test/server.js +++ b/test/server.js @@ -3,6 +3,7 @@ var http = require('http'); var parse = require('url').parse; var zlib = require('zlib'); var stream = require('stream'); +var convert = require('encoding').convert; module.exports = TestServer; @@ -53,6 +54,17 @@ TestServer.prototype.router = function(req, res) { })); } + if (p === '/long') { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/plain'); + setTimeout(function() { + res.write('test'); + }, 50); + setTimeout(function() { + res.end('test'); + }, 100); + } + if (p === '/gzip') { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); @@ -79,6 +91,30 @@ TestServer.prototype.router = function(req, res) { }, 1000); } + if (p === '/cookie') { + res.statusCode = 200; + res.setHeader('Set-Cookie', ['a=1', 'b=1']); + res.end('cookie'); + } + + if (p === '/encoding/gbk') { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/html'); + res.end(convert('
中文
', 'gbk')); + } + + if (p === '/encoding/gb2312') { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/html'); + res.end(convert('
中文
', 'gb2312')); + } + + if (p === '/encoding/shift-jis') { + res.statusCode = 200; + res.setHeader('Content-Type', 'text/html; charset=Shift-JIS'); + res.end(convert('
日本語
', 'Shift_JIS')); + } + if (p === '/redirect/301') { res.statusCode = 301; res.setHeader('Location', '/inspect'); @@ -115,6 +151,28 @@ TestServer.prototype.router = function(req, res) { res.end(); } + if (p === '/error/redirect') { + res.statusCode = 301; + //res.setHeader('Location', '/inspect'); + res.end(); + } + + if (p === '/error/400') { + res.statusCode = 400; + res.setHeader('Content-Type', 'text/plain'); + res.end('client error'); + } + + if (p === '/error/500') { + res.statusCode = 500; + res.setHeader('Content-Type', 'text/plain'); + res.end('server error'); + } + + if (p === '/error/reset') { + res.destroy(); + } + if (p === '/inspect') { res.statusCode = 200; res.setHeader('Content-Type', 'application/json'); diff --git a/test/test.js b/test/test.js index 4aa3c48..344c63e 100644 --- a/test/test.js +++ b/test/test.js @@ -189,14 +189,60 @@ describe('node-fetch', function() { }); }); - it('should obey maximum redirect limit', function() { + it('should follow redirect chain', function() { + url = base + '/redirect/chain'; + return fetch(url).then(function(res) { + expect(res.url).to.equal(base + '/inspect'); + expect(res.status).to.equal(200); + }); + }); + + it('should obey maximum redirect', function() { url = base + '/redirect/chain'; opts = { follow: 1 - }; + } return expect(fetch(url, opts)).to.eventually.be.rejectedWith(Error); }); + it('should reject broken redirect', function() { + url = base + '/error/redirect'; + return expect(fetch(url)).to.eventually.be.rejectedWith(Error); + }); + + it('should handle client-error response', function() { + url = base + '/error/400'; + return fetch(url).then(function(res) { + expect(res.headers.get('content-type')).to.equal('text/plain'); + expect(res.status).to.equal(400); + expect(res.statusText).to.equal('Bad Request'); + return res.text().then(function(result) { + expect(res.bodyUsed).to.be.true; + expect(result).to.be.a('string'); + expect(result).to.equal('client error'); + }); + }); + }); + + it('should handle server-error response', function() { + url = base + '/error/500'; + return fetch(url).then(function(res) { + expect(res.headers.get('content-type')).to.equal('text/plain'); + expect(res.status).to.equal(500); + expect(res.statusText).to.equal('Internal Server Error'); + return res.text().then(function(result) { + expect(res.bodyUsed).to.be.true; + expect(result).to.be.a('string'); + expect(result).to.equal('server error'); + }); + }); + }); + + it('should handle network-error response', function() { + url = base + '/error/reset'; + return expect(fetch(url)).to.eventually.be.rejectedWith(Error); + }); + it('should decompress gzip response', function() { url = base + '/gzip'; return fetch(url).then(function(res) { @@ -281,4 +327,124 @@ describe('node-fetch', function() { }); }); + it('should allow PUT request', function() { + url = base + '/inspect'; + opts = { + method: 'PUT' + , body: 'a=1' + }; + return fetch(url, opts).then(function(res) { + return res.json(); + }).then(function(res) { + expect(res.method).to.equal('PUT'); + expect(res.body).to.equal('a=1'); + }); + }); + + it('should allow DELETE request', function() { + url = base + '/inspect'; + opts = { + method: 'DELETE' + }; + return fetch(url, opts).then(function(res) { + return res.json(); + }).then(function(res) { + expect(res.method).to.equal('DELETE'); + }); + }); + + it('should allow HEAD request', function() { + url = base + '/hello'; + opts = { + method: 'HEAD' + + }; + return fetch(url, opts).then(function(res) { + expect(res.status).to.equal(200); + expect(res.statusText).to.equal('OK'); + expect(res.headers.get('content-type')).to.equal('text/plain'); + expect(res.body).to.be.an.instanceof(stream.Transform); + }); + }); + + it('should reject decoding body twice', function() { + url = base + '/plain'; + return fetch(url).then(function(res) { + expect(res.headers.get('content-type')).to.equal('text/plain'); + return res.text().then(function(result) { + expect(res.bodyUsed).to.be.true; + return expect(res.text()).to.eventually.be.rejectedWith(Error); + }); + }); + }); + + it('should support maximum response size', function() { + url = base + '/long'; + opts = { + size: 1 + }; + return fetch(url, opts).then(function(res) { + expect(res.status).to.equal(200); + expect(res.headers.get('content-type')).to.equal('text/plain'); + return expect(res.text()).to.eventually.be.rejectedWith(Error); + }); + }); + + it('should support encoding decode, conte-type detect', function() { + url = base + '/encoding/shift-jis'; + return fetch(url).then(function(res) { + expect(res.status).to.equal(200); + return res.text().then(function(result) { + expect(result).to.equal('
日本語
'); + }); + }); + }); + + it('should support encoding decode, html5 detect', function() { + url = base + '/encoding/gbk'; + return fetch(url).then(function(res) { + expect(res.status).to.equal(200); + return res.text().then(function(result) { + expect(result).to.equal('
中文
'); + }); + }); + }); + + it('should support encoding decode, html4 detect', function() { + url = base + '/encoding/gb2312'; + return fetch(url).then(function(res) { + expect(res.status).to.equal(200); + return res.text().then(function(result) { + expect(result).to.equal('
中文
'); + }); + }); + }); + + it('should allow get all responses of a header', function() { + url = base + '/cookie'; + return fetch(url).then(function(res) { + expect(res.headers.getAll('set-cookie')).to.deep.equal(['a=1', 'b=1']); + }); + }); + + it('should allow deleting header', function() { + url = base + '/cookie'; + return fetch(url).then(function(res) { + res.headers.delete('set-cookie'); + expect(res.headers.get('set-cookie')).to.be.null; + expect(res.headers.getAll('set-cookie')).to.be.empty; + }); + }); + + it('should support https request', function() { + this.timeout(5000); + url = 'https://github.com/'; + opts = { + method: 'HEAD' + }; + return fetch(url, opts).then(function(res) { + expect(res.status).to.equal(200); + }); + }); + });