diff --git a/.travis.yml b/.travis.yml index ebeed7008dd..7b11677ec87 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,14 +7,16 @@ node_js: - "3.3" - "4.9" - "5.12" - - "6.14" + - "6.17" - "7.10" - - "8.12" + - "8.16" + - "9.11" + - "10.15" + - "11.15" + - "12.2" matrix: include: - - node_js: "9" - env: "NVM_NODEJS_ORG_MIRROR=https://nodejs.org/download/nightly" - - node_js: "10" + - node_js: "13" env: "NVM_NODEJS_ORG_MIRROR=https://nodejs.org/download/nightly" allow_failures: # Allow the nightly installs to fail @@ -60,5 +62,5 @@ script: after_script: - | # Upload coverage to coveralls - npm install --save-dev coveralls@2.10.0 + npm install --save-dev coveralls@2.12.0 coveralls < ./coverage/lcov.info diff --git a/Contributing.md b/Contributing.md index 41386568d64..f84c0138cb0 100644 --- a/Contributing.md +++ b/Contributing.md @@ -19,7 +19,7 @@ expertise to resolve rare disputes. Log an issue for any question or problem you might have. When in doubt, log an issue, and any additional policies about what to include will be provided in the responses. The only -exception is security dislosures which should be sent privately. +exception is security disclosures which should be sent privately. Committers may direct you to another repository, ask for additional clarifications, and add appropriate metadata before the issue is addressed. diff --git a/History.md b/History.md index 2f6eab101ae..da36866652c 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,54 @@ +unreleased +========== + + * Add `express.raw` to parse bodies into `Buffer` + * Add `express.text` to parse bodies into string + * Improve error message for non-strings to `res.sendFile` + * Improve error message for `null`/`undefined` to `res.status` + * Support multiple hosts in `X-Forwarded-Host` + * deps: accepts@~1.3.7 + * deps: body-parser@1.19.0 + - Add encoding MIK + - Add petabyte (`pb`) support + - Fix parsing array brackets after index + - deps: bytes@3.1.0 + - deps: http-errors@1.7.2 + - deps: iconv-lite@0.4.24 + - deps: qs@6.7.0 + - deps: raw-body@2.4.0 + - deps: type-is@~1.6.17 + * deps: content-disposition@0.5.3 + * deps: cookie@0.4.0 + - Add `SameSite=None` support + * deps: finalhandler@~1.1.2 + - Set stricter `Content-Security-Policy` header + - deps: parseurl@~1.3.3 + - deps: statuses@~1.5.0 + * deps: parseurl@~1.3.3 + * deps: proxy-addr@~2.0.5 + - deps: ipaddr.js@1.9.0 + * deps: qs@6.7.0 + - Fix parsing array brackets after index + * deps: range-parser@~1.2.1 + * deps: send@0.17.1 + - Set stricter CSP header in redirect & error responses + - deps: http-errors@~1.7.2 + - deps: mime@1.6.0 + - deps: ms@2.1.1 + - deps: range-parser@~1.2.1 + - deps: statuses@~1.5.0 + - perf: remove redundant `path.normalize` call + * deps: serve-static@1.14.1 + - Set stricter CSP header in redirect response + - deps: parseurl@~1.3.3 + - deps: send@0.17.1 + * deps: setprototypeof@1.1.1 + * deps: statuses@~1.5.0 + - Add `103 Early Hints` + * deps: type-is@~1.6.18 + - deps: mime-types@~2.1.24 + - perf: prevent internal `throw` on invalid type + 4.16.4 / 2018-10-10 =================== @@ -294,7 +345,7 @@ - Fix including type extensions in parameters in `Accept` parsing - Fix parsing `Accept` parameters with quoted equals - Fix parsing `Accept` parameters with quoted semicolons - - Many performance improvments + - Many performance improvements - deps: mime-types@~2.1.11 - deps: negotiator@0.6.1 * deps: content-type@~1.0.2 @@ -309,7 +360,7 @@ - perf: enable strict mode - perf: hoist regular expression - perf: use for loop in parse - - perf: use string concatination for serialization + - perf: use string concatenation for serialization * deps: finalhandler@0.5.0 - Change invalid or non-numeric status code to 500 - Overwrite status message to match set status code @@ -319,7 +370,7 @@ * deps: proxy-addr@~1.1.2 - Fix accepting various invalid netmasks - Fix IPv6-mapped IPv4 validation edge cases - - IPv4 netmasks must be contingous + - IPv4 netmasks must be contiguous - IPv6 addresses cannot be used as a netmask - deps: ipaddr.js@1.1.1 * deps: qs@6.2.0 @@ -1097,13 +1148,13 @@ - deps: negotiator@0.4.6 * deps: debug@1.0.2 * deps: send@0.4.3 - - Do not throw un-catchable error on file open race condition + - Do not throw uncatchable error on file open race condition - Use `escape-html` for HTML escaping - deps: debug@1.0.2 - deps: finished@1.2.2 - deps: fresh@0.2.2 * deps: serve-static@1.2.3 - - Do not throw un-catchable error on file open race condition + - Do not throw uncatchable error on file open race condition - deps: send@0.4.3 4.4.2 / 2014-06-09 @@ -1983,7 +2034,7 @@ - deps: serve-static@1.2.3 * deps: debug@1.0.2 * deps: send@0.4.3 - - Do not throw un-catchable error on file open race condition + - Do not throw uncatchable error on file open race condition - Use `escape-html` for HTML escaping - deps: debug@1.0.2 - deps: finished@1.2.2 @@ -3168,7 +3219,7 @@ Shaw] * Updated haml submodule * Changed ETag; removed inode, modified time only * Fixed LF to CRLF for setting multiple cookies - * Fixed cookie complation; values are now urlencoded + * Fixed cookie compilation; values are now urlencoded * Fixed cookies parsing; accepts quoted values and url escaped cookies 0.11.0 / 2010-05-06 @@ -3363,7 +3414,7 @@ Shaw] * Added "plot" format option for Profiler (for gnuplot processing) * Added request number to Profiler plugin - * Fixed binary encoding for multi-part file uploads, was previously defaulting to UTF8 + * Fixed binary encoding for multipart file uploads, was previously defaulting to UTF8 * Fixed issue with routes not firing when not files are present. Closes #184 * Fixed process.Promise -> events.Promise @@ -3409,7 +3460,7 @@ Shaw] * Updated sample chat app to show messages on load * Updated libxmljs parseString -> parseHtmlString * Fixed `make init` to work with older versions of git - * Fixed specs can now run independent specs for those who cant build deps. Closes #127 + * Fixed specs can now run independent specs for those who can't build deps. Closes #127 * Fixed issues introduced by the node url module changes. Closes 126. * Fixed two assertions failing due to Collection#keys() returning strings * Fixed faulty Collection#toArray() spec due to keys() returning strings diff --git a/Readme.md b/Readme.md index 582e8958c59..81d8d91615e 100644 --- a/Readme.md +++ b/Readme.md @@ -9,8 +9,8 @@ [![Test Coverage][coveralls-image]][coveralls-url] ```js -var express = require('express') -var app = express() +const express = require('express') +const app = express() app.get('/', function (req, res) { res.send('Hello World') @@ -90,6 +90,8 @@ $ npm install $ npm start ``` + View the website at: http://localhost:3000 + ## Philosophy The Express philosophy is to provide small, robust tooling for HTTP servers, making @@ -125,6 +127,10 @@ $ npm install $ npm test ``` +## Contributing + +[Contributing Guide](Contributing.md) + ## People The original author of Express is [TJ Holowaychuk](https://github.com/tj) diff --git a/appveyor.yml b/appveyor.yml index fc3582e4a5a..84476a597d7 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -7,9 +7,13 @@ environment: - nodejs_version: "3.3" - nodejs_version: "4.9" - nodejs_version: "5.12" - - nodejs_version: "6.14" + - nodejs_version: "6.17" - nodejs_version: "7.10" - - nodejs_version: "8.12" + - nodejs_version: "8.16" + - nodejs_version: "9.11" + - nodejs_version: "10.15" + - nodejs_version: "11.15" + - nodejs_version: "12.2" cache: - node_modules install: diff --git a/examples/downloads/index.js b/examples/downloads/index.js index e6f3fa9db69..5f0772697c1 100644 --- a/examples/downloads/index.js +++ b/examples/downloads/index.js @@ -21,7 +21,7 @@ app.get('/files/:file(*)', function(req, res, next){ res.download(filePath, function (err) { if (!err) return; // file sent - if (err && err.status !== 404) return next(err); // non-404 error + if (err.status !== 404) return next(err); // non-404 error // file for download not found res.statusCode = 404; res.send('Cant find that file, sorry!'); diff --git a/examples/mvc/public/style.css b/examples/mvc/public/style.css index 69fde2e23aa..8a23f9d41c4 100644 --- a/examples/mvc/public/style.css +++ b/examples/mvc/public/style.css @@ -1,6 +1,6 @@ body { padding: 50px; - font: 16px "Helvetica Neue", Helvetica, Arial; + font: 16px "Helvetica Neue", Helvetica, Arial, sans-serif; } a { color: #107aff; diff --git a/examples/static-files/public/js/app.js b/examples/static-files/public/js/app.js index 257cc5642cb..775eb734b02 100644 --- a/examples/static-files/public/js/app.js +++ b/examples/static-files/public/js/app.js @@ -1 +1 @@ -foo +// foo diff --git a/lib/express.js b/lib/express.js index 594007b5b43..d188a16db70 100644 --- a/lib/express.js +++ b/lib/express.js @@ -77,7 +77,9 @@ exports.Router = Router; exports.json = bodyParser.json exports.query = require('./middleware/query'); +exports.raw = bodyParser.raw exports.static = require('serve-static'); +exports.text = bodyParser.text exports.urlencoded = bodyParser.urlencoded /** diff --git a/lib/request.js b/lib/request.js index 8bb86a9acc4..a9400ef99de 100644 --- a/lib/request.js +++ b/lib/request.js @@ -430,6 +430,10 @@ defineGetter(req, 'hostname', function hostname(){ if (!host || !trust(this.connection.remoteAddress, 0)) { host = this.get('Host'); + } else if (host.indexOf(',') !== -1) { + // Note: X-Forwarded-Host is normally only ever a + // single value, but this is to be safe. + host = host.substring(0, host.indexOf(',')).trimRight() } if (!host) return; diff --git a/lib/response.js b/lib/response.js index 2e445ac02c5..a4f10cbb2e1 100644 --- a/lib/response.js +++ b/lib/response.js @@ -64,6 +64,10 @@ var charsetRegExp = /;\s*charset\s*=/; */ res.status = function status(code) { + if (code === undefined || code === null) { + throw new TypeError('code argument is required to res.status') + } + this.statusCode = code; return this; }; @@ -411,6 +415,10 @@ res.sendFile = function sendFile(path, options, callback) { throw new TypeError('path argument is required to res.sendFile'); } + if (typeof path !== 'string') { + throw new TypeError('path must be a string to res.sendFile') + } + // support function as second arg if (typeof options === 'function') { done = options; @@ -814,7 +822,7 @@ res.clearCookie = function clearCookie(name, options) { * // "Remember Me" for 15 minutes * res.cookie('rememberme', '1', { expires: new Date(Date.now() + 900000), httpOnly: true }); * - * // save as above + * // same as above * res.cookie('rememberme', '1', { maxAge: 900000, httpOnly: true }) * * @param {String} name @@ -1127,6 +1135,7 @@ function stringify (value, replacer, spaces, escape) { return '\\u003e' case 0x26: return '\\u0026' + /* istanbul ignore next: unreachable default */ default: return c } diff --git a/package.json b/package.json index 74196ad68e2..87b80ce33d2 100644 --- a/package.json +++ b/package.json @@ -27,48 +27,48 @@ "api" ], "dependencies": { - "accepts": "~1.3.5", + "accepts": "~1.3.7", "array-flatten": "1.1.1", - "body-parser": "1.18.3", - "content-disposition": "0.5.2", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", "content-type": "~1.0.4", - "cookie": "0.3.1", + "cookie": "0.4.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "~1.1.2", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.1.1", + "finalhandler": "~1.1.2", "fresh": "0.5.2", "merge-descriptors": "1.0.1", "methods": "~1.1.2", "on-finished": "~2.3.0", - "parseurl": "~1.3.2", + "parseurl": "~1.3.3", "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.4", - "qs": "6.5.2", - "range-parser": "~1.2.0", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", "safe-buffer": "5.1.2", - "send": "0.16.2", - "serve-static": "1.13.2", - "setprototypeof": "1.1.0", - "statuses": "~1.4.0", - "type-is": "~1.6.16", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" }, "devDependencies": { "after": "0.8.2", - "connect-redis": "3.4.0", - "cookie-parser": "~1.4.3", - "cookie-session": "1.3.2", + "connect-redis": "3.4.1", + "cookie-parser": "~1.4.4", + "cookie-session": "1.3.3", "ejs": "2.6.1", "eslint": "2.13.1", - "express-session": "1.15.6", - "hbs": "4.0.1", + "express-session": "1.16.1", + "hbs": "4.0.4", "istanbul": "0.4.5", - "marked": "0.5.1", + "marked": "0.6.2", "method-override": "3.0.0", "mocha": "5.2.0", "morgan": "1.9.1", diff --git a/test/app.router.js b/test/app.router.js index a6c8cef202a..5a31b5fb90c 100644 --- a/test/app.router.js +++ b/test/app.router.js @@ -152,15 +152,12 @@ describe('app.router', function(){ app.use(function(req, res, next){ calls.push('after'); - res.end(); + res.json(calls) }); request(app) .get('/') - .end(function(res){ - calls.should.eql(['before', 'GET /', 'after']) - done(); - }) + .expect(200, ['before', 'GET /', 'after'], done) }) describe('when given a regexp', function(){ @@ -570,7 +567,7 @@ describe('app.router', function(){ .expect('/user/tobi.json', done) }) - it('should decore the capture', function (done) { + it('should decode the capture', function (done) { var app = express() app.get('*', function (req, res) { @@ -891,15 +888,12 @@ describe('app.router', function(){ app.get('/foo', function(req, res, next){ calls.push('/foo 2'); - res.end('done'); + res.json(calls) }); request(app) .get('/foo') - .expect('done', function(){ - calls.should.eql(['/foo/:bar?', '/foo', '/foo 2']); - done(); - }) + .expect(200, ['/foo/:bar?', '/foo', '/foo 2'], done) }) }) @@ -982,15 +976,15 @@ describe('app.router', function(){ }); app.use(function(err, req, res, next){ - res.end(err.message); + res.json({ + calls: calls, + error: err.message + }) }) request(app) .get('/foo') - .expect('fail', function(){ - calls.should.eql(['/foo/:bar?', '/foo']); - done(); - }) + .expect(200, { calls: ['/foo/:bar?', '/foo'], error: 'fail' }, done) }) it('should call handler in same route, if exists', function(done){ diff --git a/test/exports.js b/test/exports.js index 2a80eedbbe8..7624a8c8641 100644 --- a/test/exports.js +++ b/test/exports.js @@ -1,4 +1,5 @@ +var assert = require('assert') var express = require('../'); var request = require('supertest'); var should = require('should'); @@ -8,6 +9,31 @@ describe('exports', function(){ express.Router.should.be.a.Function() }) + it('should expose json middleware', function () { + assert.equal(typeof express.json, 'function') + assert.equal(express.json.length, 1) + }) + + it('should expose raw middleware', function () { + assert.equal(typeof express.raw, 'function') + assert.equal(express.raw.length, 1) + }) + + it('should expose static middleware', function () { + assert.equal(typeof express.static, 'function') + assert.equal(express.static.length, 2) + }) + + it('should expose text middleware', function () { + assert.equal(typeof express.text, 'function') + assert.equal(express.text.length, 1) + }) + + it('should expose urlencoded middleware', function () { + assert.equal(typeof express.urlencoded, 'function') + assert.equal(express.urlencoded.length, 1) + }) + it('should expose the application prototype', function(){ express.application.set.should.be.a.Function() }) diff --git a/test/express.json.js b/test/express.json.js new file mode 100644 index 00000000000..907fa0cfeb8 --- /dev/null +++ b/test/express.json.js @@ -0,0 +1,664 @@ + +var assert = require('assert') +var Buffer = require('safe-buffer').Buffer +var express = require('..') +var request = require('supertest') + +describe('express.json()', function () { + it('should parse JSON', function (done) { + request(createApp()) + .post('/') + .set('Content-Type', 'application/json') + .send('{"user":"tobi"}') + .expect(200, '{"user":"tobi"}', done) + }) + + it('should handle Content-Length: 0', function (done) { + request(createApp()) + .post('/') + .set('Content-Type', 'application/json') + .set('Content-Length', '0') + .expect(200, '{}', done) + }) + + it('should handle empty message-body', function (done) { + request(createApp()) + .post('/') + .set('Content-Type', 'application/json') + .set('Transfer-Encoding', 'chunked') + .expect(200, '{}', done) + }) + + it('should handle no message-body', function (done) { + request(createApp()) + .post('/') + .set('Content-Type', 'application/json') + .unset('Transfer-Encoding') + .expect(200, '{}', done) + }) + + it('should 400 when invalid content-length', function (done) { + var app = express() + + app.use(function (req, res, next) { + req.headers['content-length'] = '20' // bad length + next() + }) + + app.use(express.json()) + + app.post('/', function (req, res) { + res.json(req.body) + }) + + request(app) + .post('/') + .set('Content-Type', 'application/json') + .send('{"str":') + .expect(400, /content length/, done) + }) + + it('should handle duplicated middleware', function (done) { + var app = express() + + app.use(express.json()) + app.use(express.json()) + + app.post('/', function (req, res) { + res.json(req.body) + }) + + request(app) + .post('/') + .set('Content-Type', 'application/json') + .send('{"user":"tobi"}') + .expect(200, '{"user":"tobi"}', done) + }) + + describe('when JSON is invalid', function () { + before(function () { + this.app = createApp() + }) + + it('should 400 for bad token', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .send('{:') + .expect(400, parseError('{:'), done) + }) + + it('should 400 for incomplete', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .send('{"user"') + .expect(400, parseError('{"user"'), done) + }) + + it('should error with type = "entity.parse.failed"', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .set('X-Error-Property', 'type') + .send(' {"user"') + .expect(400, 'entity.parse.failed', done) + }) + + it('should include original body on error object', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .set('X-Error-Property', 'body') + .send(' {"user"') + .expect(400, ' {"user"', done) + }) + }) + + describe('with limit option', function () { + it('should 413 when over limit with Content-Length', function (done) { + var buf = Buffer.alloc(1024, '.') + request(createApp({ limit: '1kb' })) + .post('/') + .set('Content-Type', 'application/json') + .set('Content-Length', '1034') + .send(JSON.stringify({ str: buf.toString() })) + .expect(413, done) + }) + + it('should error with type = "entity.too.large"', function (done) { + var buf = Buffer.alloc(1024, '.') + request(createApp({ limit: '1kb' })) + .post('/') + .set('Content-Type', 'application/json') + .set('Content-Length', '1034') + .set('X-Error-Property', 'type') + .send(JSON.stringify({ str: buf.toString() })) + .expect(413, 'entity.too.large', done) + }) + + it('should 413 when over limit with chunked encoding', function (done) { + var buf = Buffer.alloc(1024, '.') + var server = createApp({ limit: '1kb' }) + var test = request(server).post('/') + test.set('Content-Type', 'application/json') + test.set('Transfer-Encoding', 'chunked') + test.write('{"str":') + test.write('"' + buf.toString() + '"}') + test.expect(413, done) + }) + + it('should accept number of bytes', function (done) { + var buf = Buffer.alloc(1024, '.') + request(createApp({ limit: 1024 })) + .post('/') + .set('Content-Type', 'application/json') + .send(JSON.stringify({ str: buf.toString() })) + .expect(413, done) + }) + + it('should not change when options altered', function (done) { + var buf = Buffer.alloc(1024, '.') + var options = { limit: '1kb' } + var server = createApp(options) + + options.limit = '100kb' + + request(server) + .post('/') + .set('Content-Type', 'application/json') + .send(JSON.stringify({ str: buf.toString() })) + .expect(413, done) + }) + + it('should not hang response', function (done) { + var buf = Buffer.alloc(10240, '.') + var server = createApp({ limit: '8kb' }) + var test = request(server).post('/') + test.set('Content-Type', 'application/json') + test.write(buf) + test.write(buf) + test.write(buf) + test.expect(413, done) + }) + }) + + describe('with inflate option', function () { + describe('when false', function () { + before(function () { + this.app = createApp({ inflate: false }) + }) + + it('should not accept content-encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('1f8b080000000000000bab56ca4bcc4d55b2527ab16e97522d00515be1cc0e000000', 'hex')) + test.expect(415, 'content encoding unsupported', done) + }) + }) + + describe('when true', function () { + before(function () { + this.app = createApp({ inflate: true }) + }) + + it('should accept content-encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('1f8b080000000000000bab56ca4bcc4d55b2527ab16e97522d00515be1cc0e000000', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + }) + }) + + describe('with strict option', function () { + describe('when undefined', function () { + before(function () { + this.app = createApp() + }) + + it('should 400 on primitives', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .send('true') + .expect(400, parseError('#rue').replace('#', 't'), done) + }) + }) + + describe('when false', function () { + before(function () { + this.app = createApp({ strict: false }) + }) + + it('should parse primitives', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .send('true') + .expect(200, 'true', done) + }) + }) + + describe('when true', function () { + before(function () { + this.app = createApp({ strict: true }) + }) + + it('should not parse primitives', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .send('true') + .expect(400, parseError('#rue').replace('#', 't'), done) + }) + + it('should not parse primitives with leading whitespaces', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .send(' true') + .expect(400, parseError(' #rue').replace('#', 't'), done) + }) + + it('should allow leading whitespaces in JSON', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .send(' { "user": "tobi" }') + .expect(200, '{"user":"tobi"}', done) + }) + + it('should error with type = "entity.parse.failed"', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .set('X-Error-Property', 'type') + .send('true') + .expect(400, 'entity.parse.failed', done) + }) + + it('should include correct message in stack trace', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .set('X-Error-Property', 'stack') + .send('true') + .expect(400) + .expect(shouldContainInBody(parseError('#rue').replace('#', 't'))) + .end(done) + }) + }) + }) + + describe('with type option', function () { + describe('when "application/vnd.api+json"', function () { + before(function () { + this.app = createApp({ type: 'application/vnd.api+json' }) + }) + + it('should parse JSON for custom type', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/vnd.api+json') + .send('{"user":"tobi"}') + .expect(200, '{"user":"tobi"}', done) + }) + + it('should ignore standard type', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .send('{"user":"tobi"}') + .expect(200, '{}', done) + }) + }) + + describe('when ["application/json", "application/vnd.api+json"]', function () { + before(function () { + this.app = createApp({ + type: ['application/json', 'application/vnd.api+json'] + }) + }) + + it('should parse JSON for "application/json"', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/json') + .send('{"user":"tobi"}') + .expect(200, '{"user":"tobi"}', done) + }) + + it('should parse JSON for "application/vnd.api+json"', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/vnd.api+json') + .send('{"user":"tobi"}') + .expect(200, '{"user":"tobi"}', done) + }) + + it('should ignore "application/x-json"', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/x-json') + .send('{"user":"tobi"}') + .expect(200, '{}', done) + }) + }) + + describe('when a function', function () { + it('should parse when truthy value returned', function (done) { + var app = createApp({ type: accept }) + + function accept (req) { + return req.headers['content-type'] === 'application/vnd.api+json' + } + + request(app) + .post('/') + .set('Content-Type', 'application/vnd.api+json') + .send('{"user":"tobi"}') + .expect(200, '{"user":"tobi"}', done) + }) + + it('should work without content-type', function (done) { + var app = createApp({ type: accept }) + + function accept (req) { + return true + } + + var test = request(app).post('/') + test.write('{"user":"tobi"}') + test.expect(200, '{"user":"tobi"}', done) + }) + + it('should not invoke without a body', function (done) { + var app = createApp({ type: accept }) + + function accept (req) { + throw new Error('oops!') + } + + request(app) + .get('/') + .expect(404, done) + }) + }) + }) + + describe('with verify option', function () { + it('should assert value if function', function () { + assert.throws(createApp.bind(null, { verify: 'lol' }), + /TypeError: option verify must be function/) + }) + + it('should error from verify', function (done) { + var app = createApp({ verify: function (req, res, buf) { + if (buf[0] === 0x5b) throw new Error('no arrays') + } }) + + request(app) + .post('/') + .set('Content-Type', 'application/json') + .send('["tobi"]') + .expect(403, 'no arrays', done) + }) + + it('should error with type = "entity.verify.failed"', function (done) { + var app = createApp({ verify: function (req, res, buf) { + if (buf[0] === 0x5b) throw new Error('no arrays') + } }) + + request(app) + .post('/') + .set('Content-Type', 'application/json') + .set('X-Error-Property', 'type') + .send('["tobi"]') + .expect(403, 'entity.verify.failed', done) + }) + + it('should allow custom codes', function (done) { + var app = createApp({ verify: function (req, res, buf) { + if (buf[0] !== 0x5b) return + var err = new Error('no arrays') + err.status = 400 + throw err + } }) + + request(app) + .post('/') + .set('Content-Type', 'application/json') + .send('["tobi"]') + .expect(400, 'no arrays', done) + }) + + it('should allow custom type', function (done) { + var app = createApp({ verify: function (req, res, buf) { + if (buf[0] !== 0x5b) return + var err = new Error('no arrays') + err.type = 'foo.bar' + throw err + } }) + + request(app) + .post('/') + .set('Content-Type', 'application/json') + .set('X-Error-Property', 'type') + .send('["tobi"]') + .expect(403, 'foo.bar', done) + }) + + it('should include original body on error object', function (done) { + var app = createApp({ verify: function (req, res, buf) { + if (buf[0] === 0x5b) throw new Error('no arrays') + } }) + + request(app) + .post('/') + .set('Content-Type', 'application/json') + .set('X-Error-Property', 'body') + .send('["tobi"]') + .expect(403, '["tobi"]', done) + }) + + it('should allow pass-through', function (done) { + var app = createApp({ verify: function (req, res, buf) { + if (buf[0] === 0x5b) throw new Error('no arrays') + } }) + + request(app) + .post('/') + .set('Content-Type', 'application/json') + .send('{"user":"tobi"}') + .expect(200, '{"user":"tobi"}', done) + }) + + it('should work with different charsets', function (done) { + var app = createApp({ verify: function (req, res, buf) { + if (buf[0] === 0x5b) throw new Error('no arrays') + } }) + + var test = request(app).post('/') + test.set('Content-Type', 'application/json; charset=utf-16') + test.write(Buffer.from('feff007b0022006e0061006d00650022003a00228bba0022007d', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + it('should 415 on unknown charset prior to verify', function (done) { + var app = createApp({ verify: function (req, res, buf) { + throw new Error('unexpected verify call') + } }) + + var test = request(app).post('/') + test.set('Content-Type', 'application/json; charset=x-bogus') + test.write(Buffer.from('00000000', 'hex')) + test.expect(415, 'unsupported charset "X-BOGUS"', done) + }) + }) + + describe('charset', function () { + before(function () { + this.app = createApp() + }) + + it('should parse utf-8', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/json; charset=utf-8') + test.write(Buffer.from('7b226e616d65223a22e8aeba227d', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + it('should parse utf-16', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/json; charset=utf-16') + test.write(Buffer.from('feff007b0022006e0061006d00650022003a00228bba0022007d', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + it('should parse when content-length != char length', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/json; charset=utf-8') + test.set('Content-Length', '13') + test.write(Buffer.from('7b2274657374223a22c3a5227d', 'hex')) + test.expect(200, '{"test":"å"}', done) + }) + + it('should default to utf-8', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('7b226e616d65223a22e8aeba227d', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + it('should fail on unknown charset', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/json; charset=koi8-r') + test.write(Buffer.from('7b226e616d65223a22cec5d4227d', 'hex')) + test.expect(415, 'unsupported charset "KOI8-R"', done) + }) + + it('should error with type = "charset.unsupported"', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/json; charset=koi8-r') + test.set('X-Error-Property', 'type') + test.write(Buffer.from('7b226e616d65223a22cec5d4227d', 'hex')) + test.expect(415, 'charset.unsupported', done) + }) + }) + + describe('encoding', function () { + before(function () { + this.app = createApp({ limit: '1kb' }) + }) + + it('should parse without encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('7b226e616d65223a22e8aeba227d', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + it('should support identity encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'identity') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('7b226e616d65223a22e8aeba227d', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + it('should support gzip encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('1f8b080000000000000bab56ca4bcc4d55b2527ab16e97522d00515be1cc0e000000', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + it('should support deflate encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'deflate') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('789cab56ca4bcc4d55b2527ab16e97522d00274505ac', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + it('should be case-insensitive', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'GZIP') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('1f8b080000000000000bab56ca4bcc4d55b2527ab16e97522d00515be1cc0e000000', 'hex')) + test.expect(200, '{"name":"论"}', done) + }) + + it('should 415 on unknown encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'nulls') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('000000000000', 'hex')) + test.expect(415, 'unsupported content encoding "nulls"', done) + }) + + it('should error with type = "encoding.unsupported"', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'nulls') + test.set('Content-Type', 'application/json') + test.set('X-Error-Property', 'type') + test.write(Buffer.from('000000000000', 'hex')) + test.expect(415, 'encoding.unsupported', done) + }) + + it('should 400 on malformed encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('1f8b080000000000000bab56cc4d55b2527ab16e97522d00515be1cc0e000000', 'hex')) + test.expect(400, done) + }) + + it('should 413 when inflated value exceeds limit', function (done) { + // gzip'd data exceeds 1kb, but deflated below 1kb + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/json') + test.write(Buffer.from('1f8b080000000000000bedc1010d000000c2a0f74f6d0f071400000000000000', 'hex')) + test.write(Buffer.from('0000000000000000000000000000000000000000000000000000000000000000', 'hex')) + test.write(Buffer.from('0000000000000000004f0625b3b71650c30000', 'hex')) + test.expect(413, done) + }) + }) +}) + +function createApp (options) { + var app = express() + + app.use(express.json(options)) + + app.use(function (err, req, res, next) { + res.status(err.status || 500) + res.send(String(err[req.headers['x-error-property'] || 'message'])) + }) + + app.post('/', function (req, res) { + res.json(req.body) + }) + + return app +} + +function parseError (str) { + try { + JSON.parse(str); throw new SyntaxError('strict violation') + } catch (e) { + return e.message + } +} + +function shouldContainInBody (str) { + return function (res) { + assert.ok(res.text.indexOf(str) !== -1, + 'expected \'' + res.text + '\' to contain \'' + str + '\'') + } +} diff --git a/test/express.raw.js b/test/express.raw.js new file mode 100644 index 00000000000..571c29ca9bc --- /dev/null +++ b/test/express.raw.js @@ -0,0 +1,387 @@ + +var assert = require('assert') +var Buffer = require('safe-buffer').Buffer +var express = require('..') +var request = require('supertest') + +describe('express.raw()', function () { + before(function () { + this.app = createApp() + }) + + it('should parse application/octet-stream', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/octet-stream') + .send('the user is tobi') + .expect(200, { buf: '746865207573657220697320746f6269' }, done) + }) + + it('should 400 when invalid content-length', function (done) { + var app = express() + + app.use(function (req, res, next) { + req.headers['content-length'] = '20' // bad length + next() + }) + + app.use(express.raw()) + + app.post('/', function (req, res) { + if (Buffer.isBuffer(req.body)) { + res.json({ buf: req.body.toString('hex') }) + } else { + res.json(req.body) + } + }) + + request(app) + .post('/') + .set('Content-Type', 'application/octet-stream') + .send('stuff') + .expect(400, /content length/, done) + }) + + it('should handle Content-Length: 0', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/octet-stream') + .set('Content-Length', '0') + .expect(200, { buf: '' }, done) + }) + + it('should handle empty message-body', function (done) { + request(this.app) + .post('/') + .set('Content-Type', 'application/octet-stream') + .set('Transfer-Encoding', 'chunked') + .send('') + .expect(200, { buf: '' }, done) + }) + + it('should handle duplicated middleware', function (done) { + var app = express() + + app.use(express.raw()) + app.use(express.raw()) + + app.post('/', function (req, res) { + if (Buffer.isBuffer(req.body)) { + res.json({ buf: req.body.toString('hex') }) + } else { + res.json(req.body) + } + }) + + request(app) + .post('/') + .set('Content-Type', 'application/octet-stream') + .send('the user is tobi') + .expect(200, { buf: '746865207573657220697320746f6269' }, done) + }) + + describe('with limit option', function () { + it('should 413 when over limit with Content-Length', function (done) { + var buf = Buffer.alloc(1028, '.') + var app = createApp({ limit: '1kb' }) + var test = request(app).post('/') + test.set('Content-Type', 'application/octet-stream') + test.set('Content-Length', '1028') + test.write(buf) + test.expect(413, done) + }) + + it('should 413 when over limit with chunked encoding', function (done) { + var buf = Buffer.alloc(1028, '.') + var app = createApp({ limit: '1kb' }) + var test = request(app).post('/') + test.set('Content-Type', 'application/octet-stream') + test.set('Transfer-Encoding', 'chunked') + test.write(buf) + test.expect(413, done) + }) + + it('should accept number of bytes', function (done) { + var buf = Buffer.alloc(1028, '.') + var app = createApp({ limit: 1024 }) + var test = request(app).post('/') + test.set('Content-Type', 'application/octet-stream') + test.write(buf) + test.expect(413, done) + }) + + it('should not change when options altered', function (done) { + var buf = Buffer.alloc(1028, '.') + var options = { limit: '1kb' } + var app = createApp(options) + + options.limit = '100kb' + + var test = request(app).post('/') + test.set('Content-Type', 'application/octet-stream') + test.write(buf) + test.expect(413, done) + }) + + it('should not hang response', function (done) { + var buf = Buffer.alloc(10240, '.') + var app = createApp({ limit: '8kb' }) + var test = request(app).post('/') + test.set('Content-Type', 'application/octet-stream') + test.write(buf) + test.write(buf) + test.write(buf) + test.expect(413, done) + }) + }) + + describe('with inflate option', function () { + describe('when false', function () { + before(function () { + this.app = createApp({ inflate: false }) + }) + + it('should not accept content-encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) + test.expect(415, 'content encoding unsupported', done) + }) + }) + + describe('when true', function () { + before(function () { + this.app = createApp({ inflate: true }) + }) + + it('should accept content-encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) + test.expect(200, { buf: '6e616d653de8aeba' }, done) + }) + }) + }) + + describe('with type option', function () { + describe('when "application/vnd+octets"', function () { + before(function () { + this.app = createApp({ type: 'application/vnd+octets' }) + }) + + it('should parse for custom type', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/vnd+octets') + test.write(Buffer.from('000102', 'hex')) + test.expect(200, { buf: '000102' }, done) + }) + + it('should ignore standard type', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('000102', 'hex')) + test.expect(200, '{}', done) + }) + }) + + describe('when ["application/octet-stream", "application/vnd+octets"]', function () { + before(function () { + this.app = createApp({ + type: ['application/octet-stream', 'application/vnd+octets'] + }) + }) + + it('should parse "application/octet-stream"', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('000102', 'hex')) + test.expect(200, { buf: '000102' }, done) + }) + + it('should parse "application/vnd+octets"', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/vnd+octets') + test.write(Buffer.from('000102', 'hex')) + test.expect(200, { buf: '000102' }, done) + }) + + it('should ignore "application/x-foo"', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/x-foo') + test.write(Buffer.from('000102', 'hex')) + test.expect(200, '{}', done) + }) + }) + + describe('when a function', function () { + it('should parse when truthy value returned', function (done) { + var app = createApp({ type: accept }) + + function accept (req) { + return req.headers['content-type'] === 'application/vnd.octet' + } + + var test = request(app).post('/') + test.set('Content-Type', 'application/vnd.octet') + test.write(Buffer.from('000102', 'hex')) + test.expect(200, { buf: '000102' }, done) + }) + + it('should work without content-type', function (done) { + var app = createApp({ type: accept }) + + function accept (req) { + return true + } + + var test = request(app).post('/') + test.write(Buffer.from('000102', 'hex')) + test.expect(200, { buf: '000102' }, done) + }) + + it('should not invoke without a body', function (done) { + var app = createApp({ type: accept }) + + function accept (req) { + throw new Error('oops!') + } + + request(app) + .get('/') + .expect(404, done) + }) + }) + }) + + describe('with verify option', function () { + it('should assert value is function', function () { + assert.throws(createApp.bind(null, { verify: 'lol' }), + /TypeError: option verify must be function/) + }) + + it('should error from verify', function (done) { + var app = createApp({ verify: function (req, res, buf) { + if (buf[0] === 0x00) throw new Error('no leading null') + } }) + + var test = request(app).post('/') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('000102', 'hex')) + test.expect(403, 'no leading null', done) + }) + + it('should allow custom codes', function (done) { + var app = createApp({ verify: function (req, res, buf) { + if (buf[0] !== 0x00) return + var err = new Error('no leading null') + err.status = 400 + throw err + } }) + + var test = request(app).post('/') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('000102', 'hex')) + test.expect(400, 'no leading null', done) + }) + + it('should allow pass-through', function (done) { + var app = createApp({ verify: function (req, res, buf) { + if (buf[0] === 0x00) throw new Error('no leading null') + } }) + + var test = request(app).post('/') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('0102', 'hex')) + test.expect(200, { buf: '0102' }, done) + }) + }) + + describe('charset', function () { + before(function () { + this.app = createApp() + }) + + it('should ignore charset', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/octet-stream; charset=utf-8') + test.write(Buffer.from('6e616d6520697320e8aeba', 'hex')) + test.expect(200, { buf: '6e616d6520697320e8aeba' }, done) + }) + }) + + describe('encoding', function () { + before(function () { + this.app = createApp({ limit: '10kb' }) + }) + + it('should parse without encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('6e616d653de8aeba', 'hex')) + test.expect(200, { buf: '6e616d653de8aeba' }, done) + }) + + it('should support identity encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'identity') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('6e616d653de8aeba', 'hex')) + test.expect(200, { buf: '6e616d653de8aeba' }, done) + }) + + it('should support gzip encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'gzip') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) + test.expect(200, { buf: '6e616d653de8aeba' }, done) + }) + + it('should support deflate encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'deflate') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('789ccb4bcc4db57db16e17001068042f', 'hex')) + test.expect(200, { buf: '6e616d653de8aeba' }, done) + }) + + it('should be case-insensitive', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'GZIP') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('1f8b080000000000000bcb4bcc4db57db16e170099a4bad608000000', 'hex')) + test.expect(200, { buf: '6e616d653de8aeba' }, done) + }) + + it('should fail on unknown encoding', function (done) { + var test = request(this.app).post('/') + test.set('Content-Encoding', 'nulls') + test.set('Content-Type', 'application/octet-stream') + test.write(Buffer.from('000000000000', 'hex')) + test.expect(415, 'unsupported content encoding "nulls"', done) + }) + }) +}) + +function createApp (options) { + var app = express() + + app.use(express.raw(options)) + + app.use(function (err, req, res, next) { + res.status(err.status || 500) + res.send(String(err[req.headers['x-error-property'] || 'message'])) + }) + + app.post('/', function (req, res) { + if (Buffer.isBuffer(req.body)) { + res.json({ buf: req.body.toString('hex') }) + } else { + res.json(req.body) + } + }) + + return app +} diff --git a/test/express.static.js b/test/express.static.js new file mode 100644 index 00000000000..485ee4c0c10 --- /dev/null +++ b/test/express.static.js @@ -0,0 +1,813 @@ + +var assert = require('assert') +var Buffer = require('safe-buffer').Buffer +var express = require('..') +var path = require('path') +var request = require('supertest') +var utils = require('./support/utils') + +var fixtures = path.join(__dirname, '/fixtures') +var relative = path.relative(process.cwd(), fixtures) + +var skipRelative = ~relative.indexOf('..') || path.resolve(relative) === relative + +describe('express.static()', function () { + describe('basic operations', function () { + before(function () { + this.app = createApp() + }) + + it('should require root path', function () { + assert.throws(express.static.bind(), /root path required/) + }) + + it('should require root path to be string', function () { + assert.throws(express.static.bind(null, 42), /root path.*string/) + }) + + it('should serve static files', function (done) { + request(this.app) + .get('/todo.txt') + .expect(200, '- groceries', done) + }) + + it('should support nesting', function (done) { + request(this.app) + .get('/users/tobi.txt') + .expect(200, 'ferret', done) + }) + + it('should set Content-Type', function (done) { + request(this.app) + .get('/todo.txt') + .expect('Content-Type', 'text/plain; charset=UTF-8') + .expect(200, done) + }) + + it('should set Last-Modified', function (done) { + request(this.app) + .get('/todo.txt') + .expect('Last-Modified', /\d{2} \w{3} \d{4}/) + .expect(200, done) + }) + + it('should default max-age=0', function (done) { + request(this.app) + .get('/todo.txt') + .expect('Cache-Control', 'public, max-age=0') + .expect(200, done) + }) + + it('should support urlencoded pathnames', function (done) { + request(this.app) + .get('/%25%20of%20dogs.txt') + .expect(200, '20%', done) + }) + + it('should not choke on auth-looking URL', function (done) { + request(this.app) + .get('//todo@txt') + .expect(404, 'Not Found', done) + }) + + it('should support index.html', function (done) { + request(this.app) + .get('/users/') + .expect(200) + .expect('Content-Type', /html/) + .expect('
tobi, loki, jane
', done) + }) + + it('should support ../', function (done) { + request(this.app) + .get('/users/../todo.txt') + .expect(200, '- groceries', done) + }) + + it('should support HEAD', function (done) { + request(this.app) + .head('/todo.txt') + .expect(200) + .expect(utils.shouldNotHaveBody()) + .end(done) + }) + + it('should skip POST requests', function (done) { + request(this.app) + .post('/todo.txt') + .expect(404, 'Not Found', done) + }) + + it('should support conditional requests', function (done) { + var app = this.app + + request(app) + .get('/todo.txt') + .end(function (err, res) { + if (err) throw err + request(app) + .get('/todo.txt') + .set('If-None-Match', res.headers.etag) + .expect(304, done) + }) + }) + + it('should support precondition checks', function (done) { + request(this.app) + .get('/todo.txt') + .set('If-Match', '"foo"') + .expect(412, done) + }) + + it('should serve zero-length files', function (done) { + request(this.app) + .get('/empty.txt') + .expect(200, '', done) + }) + + it('should ignore hidden files', function (done) { + request(this.app) + .get('/.name') + .expect(404, 'Not Found', done) + }) + }); + + (skipRelative ? describe.skip : describe)('current dir', function () { + before(function () { + this.app = createApp('.') + }) + + it('should be served with "."', function (done) { + var dest = relative.split(path.sep).join('/') + request(this.app) + .get('/' + dest + '/todo.txt') + .expect(200, '- groceries', done) + }) + }) + + describe('acceptRanges', function () { + describe('when false', function () { + it('should not include Accept-Ranges', function (done) { + request(createApp(fixtures, { 'acceptRanges': false })) + .get('/nums.txt') + .expect(utils.shouldNotHaveHeader('Accept-Ranges')) + .expect(200, '123456789', done) + }) + + it('should ignore Rage request header', function (done) { + request(createApp(fixtures, { 'acceptRanges': false })) + .get('/nums.txt') + .set('Range', 'bytes=0-3') + .expect(utils.shouldNotHaveHeader('Accept-Ranges')) + .expect(utils.shouldNotHaveHeader('Content-Range')) + .expect(200, '123456789', done) + }) + }) + + describe('when true', function () { + it('should include Accept-Ranges', function (done) { + request(createApp(fixtures, { 'acceptRanges': true })) + .get('/nums.txt') + .expect('Accept-Ranges', 'bytes') + .expect(200, '123456789', done) + }) + + it('should obey Rage request header', function (done) { + request(createApp(fixtures, { 'acceptRanges': true })) + .get('/nums.txt') + .set('Range', 'bytes=0-3') + .expect('Accept-Ranges', 'bytes') + .expect('Content-Range', 'bytes 0-3/9') + .expect(206, '1234', done) + }) + }) + }) + + describe('cacheControl', function () { + describe('when false', function () { + it('should not include Cache-Control', function (done) { + request(createApp(fixtures, { 'cacheControl': false })) + .get('/nums.txt') + .expect(utils.shouldNotHaveHeader('Cache-Control')) + .expect(200, '123456789', done) + }) + + it('should ignore maxAge', function (done) { + request(createApp(fixtures, { 'cacheControl': false, 'maxAge': 12000 })) + .get('/nums.txt') + .expect(utils.shouldNotHaveHeader('Cache-Control')) + .expect(200, '123456789', done) + }) + }) + + describe('when true', function () { + it('should include Cache-Control', function (done) { + request(createApp(fixtures, { 'cacheControl': true })) + .get('/nums.txt') + .expect('Cache-Control', 'public, max-age=0') + .expect(200, '123456789', done) + }) + }) + }) + + describe('extensions', function () { + it('should be not be enabled by default', function (done) { + request(createApp(fixtures)) + .get('/todo') + .expect(404, done) + }) + + it('should be configurable', function (done) { + request(createApp(fixtures, { 'extensions': 'txt' })) + .get('/todo') + .expect(200, '- groceries', done) + }) + + it('should support disabling extensions', function (done) { + request(createApp(fixtures, { 'extensions': false })) + .get('/todo') + .expect(404, done) + }) + + it('should support fallbacks', function (done) { + request(createApp(fixtures, { 'extensions': ['htm', 'html', 'txt'] })) + .get('/todo') + .expect(200, 'tobi, loki, jane
\ No newline at end of file diff --git a/test/fixtures/users/tobi.txt b/test/fixtures/users/tobi.txt new file mode 100644 index 00000000000..9d9529d47d7 --- /dev/null +++ b/test/fixtures/users/tobi.txt @@ -0,0 +1 @@ +ferret \ No newline at end of file diff --git a/test/middleware.basic.js b/test/middleware.basic.js index ce59589230b..4616842ed60 100644 --- a/test/middleware.basic.js +++ b/test/middleware.basic.js @@ -1,4 +1,5 @@ +var assert = require('assert') var express = require('../'); var request = require('supertest'); @@ -33,6 +34,7 @@ describe('middleware', function(){ .set('Content-Type', 'application/json') .send('{"foo":"bar"}') .expect('Content-Type', 'application/json') + .expect(function () { assert.deepEqual(calls, ['one', 'two']) }) .expect(200, '{"foo":"bar"}', done) }) }) diff --git a/test/req.acceptsCharset.js b/test/req.acceptsCharset.js index 0d0ed8b5e41..f7d0cc0e300 100644 --- a/test/req.acceptsCharset.js +++ b/test/req.acceptsCharset.js @@ -18,8 +18,8 @@ describe('req', function(){ }) }) - describe('when Accept-Charset is not present', function(){ - it('should return true when present', function(done){ + describe('when Accept-Charset is present', function () { + it('should return true', function (done) { var app = express(); app.use(function(req, res, next){ diff --git a/test/req.acceptsCharsets.js b/test/req.acceptsCharsets.js index 2f4574c5244..d1c459174a9 100644 --- a/test/req.acceptsCharsets.js +++ b/test/req.acceptsCharsets.js @@ -18,8 +18,8 @@ describe('req', function(){ }) }) - describe('when Accept-Charset is not present', function(){ - it('should return true when present', function(done){ + describe('when Accept-Charset is present', function () { + it('should return true', function (done) { var app = express(); app.use(function(req, res, next){ diff --git a/test/req.acceptsEncodings.js b/test/req.acceptsEncodings.js index aba8ea5fbeb..a5cf747d41c 100644 --- a/test/req.acceptsEncodings.js +++ b/test/req.acceptsEncodings.js @@ -3,7 +3,7 @@ var express = require('../') , request = require('supertest'); describe('req', function(){ - describe('.acceptsEncodingss', function(){ + describe('.acceptsEncodings', function () { it('should be true if encoding accepted', function(done){ var app = express(); diff --git a/test/req.hostname.js b/test/req.hostname.js index 816cd597990..09bfb899898 100644 --- a/test/req.hostname.js +++ b/test/req.hostname.js @@ -116,6 +116,56 @@ describe('req', function(){ .set('Host', 'example.com') .expect('example.com', done); }) + + describe('when multiple X-Forwarded-Host', function () { + it('should use the first value', function (done) { + var app = express() + + app.enable('trust proxy') + + app.use(function (req, res) { + res.send(req.hostname) + }) + + request(app) + .get('/') + .set('Host', 'localhost') + .set('X-Forwarded-Host', 'example.com, foobar.com') + .expect(200, 'example.com', done) + }) + + it('should remove OWS around comma', function (done) { + var app = express() + + app.enable('trust proxy') + + app.use(function (req, res) { + res.send(req.hostname) + }) + + request(app) + .get('/') + .set('Host', 'localhost') + .set('X-Forwarded-Host', 'example.com , foobar.com') + .expect(200, 'example.com', done) + }) + + it('should strip port number', function (done) { + var app = express() + + app.enable('trust proxy') + + app.use(function (req, res) { + res.send(req.hostname) + }) + + request(app) + .get('/') + .set('Host', 'localhost') + .set('X-Forwarded-Host', 'example.com:8080 , foobar.com:8888') + .expect(200, 'example.com', done) + }) + }) }) describe('when "trust proxy" is disabled', function(){ diff --git a/test/req.query.js b/test/req.query.js index d3d29abd16d..0e810b8ef91 100644 --- a/test/req.query.js +++ b/test/req.query.js @@ -25,8 +25,8 @@ describe('req', function(){ var app = createApp('extended'); request(app) - .get('/?user[name]=tj') - .expect(200, '{"user":{"name":"tj"}}', done); + .get('/?foo[0][bar]=baz&foo[0][fizz]=buzz&foo[]=done!') + .expect(200, '{"foo":[{"bar":"baz","fizz":"buzz"},"done!"]}', done); }); it('should parse parameters with dots', function (done) { @@ -70,7 +70,7 @@ describe('req', function(){ }); }); - describe('when "query parser" disabled', function () { + describe('when "query parser" enabled', function () { it('should not parse complex keys', function (done) { var app = createApp(true); diff --git a/test/res.cookie.js b/test/res.cookie.js index 4eeaaf094ad..271a0969e62 100644 --- a/test/res.cookie.js +++ b/test/res.cookie.js @@ -108,15 +108,12 @@ describe('res', function(){ app.use(function(req, res){ res.cookie('name', 'tobi', options) - res.end(); + res.json(options) }); request(app) .get('/') - .end(function(err, res){ - options.should.eql(optionsCopy); - done(); - }) + .expect(200, optionsCopy, done) }) }) diff --git a/test/res.download.js b/test/res.download.js index 084b3c7164d..cf3b3ca53e1 100644 --- a/test/res.download.js +++ b/test/res.download.js @@ -110,7 +110,7 @@ describe('res', function(){ }) describe('when options.headers contains Content-Disposition', function () { - it('should should be ignored', function (done) { + it('should be ignored', function (done) { var app = express() app.use(function (req, res) { @@ -130,7 +130,7 @@ describe('res', function(){ .end(done) }) - it('should should be ignored case-insensitively', function (done) { + it('should be ignored case-insensitively', function (done) { var app = express() app.use(function (req, res) { diff --git a/test/res.locals.js b/test/res.locals.js index 3c83e66c54f..a1c819667a3 100644 --- a/test/res.locals.js +++ b/test/res.locals.js @@ -8,13 +8,12 @@ describe('res', function(){ var app = express(); app.use(function(req, res){ - Object.keys(res.locals).should.eql([]); - res.end(); + res.json(res.locals) }); request(app) .get('/') - .expect(200, done); + .expect(200, {}, done) }) }) @@ -30,12 +29,11 @@ describe('res', function(){ }); app.use(function(req, res){ - res.locals.foo.should.equal('bar'); - res.end(); + res.json(res.locals) }); request(app) .get('/') - .expect(200, done); + .expect(200, { foo: 'bar' }, done) }) }) diff --git a/test/res.sendFile.js b/test/res.sendFile.js index d7585b7704c..5f494f1e0bc 100644 --- a/test/res.sendFile.js +++ b/test/res.sendFile.js @@ -20,6 +20,14 @@ describe('res', function(){ .expect(500, /path.*required/, done); }); + it('should error for non-string path', function (done) { + var app = createApp(42) + + request(app) + .get('/') + .expect(500, /TypeError: path must be a string to res.sendFile/, done) + }) + it('should transfer a file', function (done) { var app = createApp(path.resolve(fixtures, 'name.txt')); diff --git a/test/res.status.js b/test/res.status.js index 8c173a645c5..3f928ec0b0b 100644 --- a/test/res.status.js +++ b/test/res.status.js @@ -16,5 +16,37 @@ describe('res', function(){ .expect('Created') .expect(201, done); }) + + describe('when code is undefined', function () { + it('should throw a TypeError', function (done) { + var app = express() + + app.use(function (req, res) { + res.status(undefined).send('OK') + }) + + request(app) + .get('/') + .expect(500) + .expect(/TypeError: code argument is required to res.status/) + .end(done) + }) + }) + + describe('when code is null', function () { + it('should throw a TypeError', function (done) { + var app = express() + + app.use(function (req, res) { + res.status(null).send('OK') + }) + + request(app) + .get('/') + .expect(500) + .expect(/TypeError: code argument is required to res.status/) + .end(done) + }) + }) }) }) diff --git a/test/support/env.js b/test/support/env.js index 0701f5e334f..000638ceeae 100644 --- a/test/support/env.js +++ b/test/support/env.js @@ -1,3 +1,3 @@ process.env.NODE_ENV = 'test'; -process.env.NO_DEPRECATION = 'express'; +process.env.NO_DEPRECATION = 'body-parser,express'; diff --git a/test/support/utils.js b/test/support/utils.js index ec6b801bc09..579f042a0c2 100644 --- a/test/support/utils.js +++ b/test/support/utils.js @@ -3,14 +3,48 @@ * Module dependencies. * @private */ + var assert = require('assert'); +var Buffer = require('safe-buffer').Buffer /** * Module exports. * @public */ + +exports.shouldHaveBody = shouldHaveBody +exports.shouldNotHaveBody = shouldNotHaveBody exports.shouldNotHaveHeader = shouldNotHaveHeader; +/** + * Assert that a supertest response has a specific body. + * + * @param {Buffer} buf + * @returns {function} + */ + +function shouldHaveBody (buf) { + return function (res) { + var body = !Buffer.isBuffer(res.body) + ? Buffer.from(res.text) + : res.body + assert.ok(body, 'response has body') + assert.strictEqual(body.toString('hex'), buf.toString('hex')) + } +} + +/** + * Assert that a supertest response does not have a body. + * + * @returns {function} + */ + +function shouldNotHaveBody () { + return function (res) { + assert.ok(res.text === '' || res.text === undefined) + } +} + /** * Assert that a supertest response does not have a header. *