diff --git a/.travis.yml b/.travis.yml index 90852320..95034721 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,8 +5,19 @@ node_js: - "1.8" - "2.5" - "3.3" - - "4.2" + - "4.4" + - "5.11" + - "6.2" sudo: false -before_install: "npm rm --save-dev connect-redis" +cache: + directories: + - node_modules +before_install: + # Remove all non-test dependencies + - "npm rm --save-dev connect-redis" + + # Update Node.js modules + - "test ! -d node_modules || npm prune" + - "test ! -d node_modules || npm rebuild" script: "npm run-script test-ci" after_script: "npm install coveralls@2.10.0 && cat ./coverage/lcov.info | coveralls" diff --git a/Collaborator-Guide.md b/Collaborator-Guide.md new file mode 100644 index 00000000..abbfc109 --- /dev/null +++ b/Collaborator-Guide.md @@ -0,0 +1,36 @@ + +## Website Issues + +Open issues for the expressjs.com website in https://github.com/expressjs/expressjs.com. + +## PRs and Code contributions + +* Tests must pass. +* Follow existing coding style. +* If you fix a bug, add a test. + +## Branches + +* Use the `master` branch for bug fixes or minor work that is intended for the current release stream +* Use the correspondingly named branch, e.g. `5.0`, for anything intended for a future release of Express + +## Steps for contributing + +* [Create an issue](https://github.com/expressjs/express/issues/new) for the bug you want to fix or the feature that you want to add. +* Create your own [fork](https://github.com/expressjs/express) on github, then checkout your fork. +* Write your code in your local copy. It's good practice to create a branch for each new issue you work on, although not compulsory. +* To run the test suite, first install the dependencies by running `npm install`, then run `npm test`. +* If the tests pass, you can commit your changes to your fork and then create a pull request from there. Make sure to reference your issue from the pull request comments by including the issue number e.g. #123. + +## Issues which are questions + +We will typically close any vague issues or questions that are specific to some app you are writing. Please double check the docs and other references before being trigger happy with posting a question issue. + +Things that will help get your question issue looked at: + +* Full and runnable JS code. +* Clear description of the problem or unexpected behavior. +* Clear description of the expected result. +* Steps you have taken to debug it yourself. + +If you post a question and do not outline the above items or make it easy for us to understand and reproduce your issue, it will be closed. diff --git a/Contributing.md b/Contributing.md index ce33ccd4..3fb2b7c2 100644 --- a/Contributing.md +++ b/Contributing.md @@ -1,21 +1,92 @@ -# Contributing +# Node.js Community Contributing Guide 1.0 -## PRs and Code contributions +This document describes a very simple process suitable for most projects +in the Node.js ecosystem. Projects are encouraged to adopt this whether they +are hosted in the Node.js Foundation or not. -* Tests must pass. -* Follow existing coding style. -* If you fix a bug, add a test. +The goal of this document is to create a contribution process that: + +* Encourages new contributions. +* Encourages contributors to remain involved. +* Avoids unnecessary processes and bureaucracy whenever possible. +* Creates a transparent decision making process which makes it clear how +contributors can be involved in decision making. + +This document is based on much prior art in the Node.js community, io.js, +and the Node.js project. + +## Vocabulary + +* A **Contributor** is any individual creating or commenting on an issue or pull request. +* A **Committer** is a subset of contributors who have been given write access to the repository. +* A **TC (Technical Committee)** is a group of committers representing the required technical +expertise to resolve rare disputes. + +# Logging Issues + +Log an issue for any question or problem you might have. When in doubt, log an issue, +any additional policies about what to include will be provided in the responses. The only +exception is security dislosures 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. + +Please be courteous, respectful, and every participant is expected to follow the +project's Code of Conduct. + +# Contributions + +Any change to resources in this repository must be through pull requests. This applies to all changes +to documentation, code, binary files, etc. Even long term committers and TC members must use +pull requests. + +No pull request can be merged without being reviewed. + +For non-trivial contributions, pull requests should sit for at least 36 hours to ensure that +contributors in other timezones have time to review. Consideration should also be given to +weekends and other holiday periods to ensure active committers all have reasonable time to +become involved in the discussion and review process if they wish. + +The default for each contribution is that it is accepted once no committer has an objection. +During review committers may also request that a specific contributor who is most versed in a +particular area gives a "LGTM" before the PR can be merged. There is no additional "sign off" +process for contributions to land. Once all issues brought by committers are addressed it can +be landed by any committer. + +In the case of an objection being raised in a pull request by another committer, all involved +committers should seek to arrive at a consensus by way of addressing concerns being expressed +by discussion, compromise on the proposed change, or withdrawal of the proposed change. + +If a contribution is controversial and committers cannot agree about how to get it to land +or if it should land then it should be escalated to the TC. TC members should regularly +discuss pending contributions in order to find a resolution. It is expected that only a +small minority of issues be brought to the TC for resolution and that discussion and +compromise among committers be the default resolution mechanism. + +# Becoming a Committer + +All contributors who land a non-trivial contribution should be on-boarded in a timely manner, +and added as a committer, and be given write access to the repository. + +Committers are expected to follow this policy and continue to send pull requests, go through +proper review, and have other committers merge their pull requests. + +# TC Process + +The TC uses a "consensus seeking" process for issues that are escalated to the TC. +The group tries to find a resolution that has no open objections among TC members. +If a consensus cannot be reached that has no objections then a majority wins vote +is called. It is also expected that the majority of decisions made by the TC are via +a consensus seeking process and that voting is only used as a last-resort. + +Resolution may involve returning the issue to committers with suggestions on how to +move forward towards a consensus. It is not expected that a meeting of the TC +will resolve all issues on its agenda during that meeting and may prefer to continue +the discussion happening among the committers. + +Members can be added to the TC at any time. Any committer can nominate another committer +to the TC and the TC uses its standard consensus seeking process to evaluate whether or +not to add this new member. Members who do not participate consistently at the level of +a majority of the other members are expected to resign. -## Issues which are questions - -We will typically close any vague issues or questions that are specific to some app you are writing. Please double check the docs and other references before being trigger happy with posting a question issue. - -Things that will help get your question issue looked at: - -* Full and runnable JS code. -* Clear description of the problem or unexpected behavior. -* Clear description of the expected result. -* Steps you have taken to debug it yourself. - -If you post a question and do not outline the above items or make it easy for us to understand and reproduce your issue, it will be closed. diff --git a/History.md b/History.md index 08a66814..bfd7e58a 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,8 @@ +5.x +=== + +This incorporates all changes after 4.13.1 up to 4.14.0. + 5.0.0-alpha.2 / 2015-07-06 ========================== @@ -35,6 +40,86 @@ This is the first Express 5.0 alpha release, based off 4.10.1. * add: - `app.router` is a reference to the base router +4.14.0 / 2016-06-16 +=================== + + * Add `acceptRanges` option to `res.sendFile`/`res.sendfile` + * Add `cacheControl` option to `res.sendFile`/`res.sendfile` + * Add `options` argument to `req.range` + - Includes the `combine` option + * Encode URL in `res.location`/`res.redirect` if not already encoded + * Fix some redirect handling in `res.sendFile`/`res.sendfile` + * Fix Windows absolute path check using forward slashes + * Improve error with invalid arguments to `req.get()` + * Improve performance for `res.json`/`res.jsonp` in most cases + * Improve `Range` header handling in `res.sendFile`/`res.sendfile` + * deps: accepts@~1.3.3 + - 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 + - deps: mime-types@~2.1.11 + - deps: negotiator@0.6.1 + * deps: content-type@~1.0.2 + - perf: enable strict mode + * deps: cookie@0.3.1 + - Add `sameSite` option + - Fix cookie `Max-Age` to never be a floating point number + - Improve error message when `encode` is not a function + - Improve error message when `expires` is not a `Date` + - Throw better error for invalid argument to parse + - Throw on invalid values provided to `serialize` + - perf: enable strict mode + - perf: hoist regular expression + - perf: use for loop in parse + - perf: use string concatination for serialization + * deps: finalhandler@0.5.0 + - Change invalid or non-numeric status code to 500 + - Overwrite status message to match set status code + - Prefer `err.statusCode` if `err.status` is invalid + - Set response headers from `err.headers` object + - Use `statuses` instead of `http` module for status messages + * deps: proxy-addr@~1.1.2 + - Fix accepting various invalid netmasks + - Fix IPv6-mapped IPv4 validation edge cases + - IPv4 netmasks must be contingous + - IPv6 addresses cannot be used as a netmask + - deps: ipaddr.js@1.1.1 + * deps: qs@6.2.0 + - Add `decoder` option in `parse` function + * deps: range-parser@~1.2.0 + - Add `combine` option to combine overlapping ranges + - Fix incorrectly returning -1 when there is at least one valid range + - perf: remove internal function + * deps: send@0.14.1 + - Add `acceptRanges` option + - Add `cacheControl` option + - Attempt to combine multiple ranges into single range + - Correctly inherit from `Stream` class + - Fix `Content-Range` header in 416 responses when using `start`/`end` options + - Fix `Content-Range` header missing from default 416 responses + - Fix redirect error when `path` contains raw non-URL characters + - Fix redirect when `path` starts with multiple forward slashes + - Ignore non-byte `Range` headers + - deps: http-errors@~1.5.0 + - deps: range-parser@~1.2.0 + - deps: statuses@~1.3.0 + - perf: remove argument reassignment + * deps: serve-static@~1.11.1 + - Add `acceptRanges` option + - Add `cacheControl` option + - Attempt to combine multiple ranges into single range + - Fix redirect error when `req.url` contains raw non-URL characters + - Ignore non-byte `Range` headers + - Use status code 301 for redirects + - deps: send@0.14.1 + * deps: type-is@~1.6.13 + - Fix type error when given invalid type to match against + - deps: mime-types@~2.1.11 + * deps: vary@~1.1.0 + - Only accept valid field names in the `field` argument + * perf: use strict equality when possible + 4.13.4 / 2016-01-21 =================== diff --git a/Readme.md b/Readme.md index 6e08454b..e9bfaeba 100644 --- a/Readme.md +++ b/Readme.md @@ -37,12 +37,16 @@ $ npm install express ## Docs & Community + * [Website and Documentation](http://expressjs.com/) - [[website repo](https://github.com/strongloop/expressjs.com)] * [#express](https://webchat.freenode.net/?channels=express) on freenode IRC * [Github Organization](https://github.com/expressjs) for Official Middleware & Modules + * Visit the [Wiki](https://github.com/expressjs/express/wiki) * [Google Group](https://groups.google.com/group/express-js) for discussion * [Gitter](https://gitter.im/expressjs/express) for support and discussion * [Русскоязычная документация](http://jsman.ru/express/) +**PROTIP** Be sure to read [Migrating from 3.x to 4.x](https://github.com/expressjs/express/wiki/Migrating-from-3.x-to-4.x) as well as [New features in 4.x](https://github.com/expressjs/express/wiki/New-features-in-4.x). + ###Security Issues If you discover a security vulnerability in Express, please see [Security Policies and Procedures](Security.md). diff --git a/appveyor.yml b/appveyor.yml index 242bb8b0..f1061373 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -5,10 +5,16 @@ environment: - nodejs_version: "1.8" - nodejs_version: "2.5" - nodejs_version: "3.3" - - nodejs_version: "4.2" + - nodejs_version: "4.4" + - nodejs_version: "5.11" + - nodejs_version: "6.2" +cache: + - node_modules install: - ps: Install-Product node $env:nodejs_version - npm rm --save-dev connect-redis + - if exist node_modules npm prune + - if exist node_modules npm rebuild - npm install build: off test_script: diff --git a/examples/error/index.js b/examples/error/index.js index d7db5015..94b3b4a4 100644 --- a/examples/error/index.js +++ b/examples/error/index.js @@ -31,6 +31,9 @@ app.get('/', function(req, res){ app.get('/next', function(req, res, next){ // We can also pass exceptions to next() + // The reason for process.nextTick() is to show that + // next() can be called inside an async operation, + // in real life it can be a DB read or HTTP request. process.nextTick(function(){ next(new Error('oh no!')); }); diff --git a/examples/markdown/index.js b/examples/markdown/index.js index db2f16fb..43062870 100644 --- a/examples/markdown/index.js +++ b/examples/markdown/index.js @@ -2,9 +2,10 @@ * Module dependencies. */ +var escapeHtml = require('escape-html'); var express = require('../..'); var fs = require('fs'); -var md = require('marked').parse; +var marked = require('marked'); var app = module.exports = express(); @@ -13,15 +14,10 @@ var app = module.exports = express(); app.engine('md', function(path, options, fn){ fs.readFile(path, 'utf8', function(err, str){ if (err) return fn(err); - try { - var html = md(str); - html = html.replace(/\{([^}]+)\}/g, function(_, name){ - return options[name] || ''; - }); - fn(null, html); - } catch(err) { - fn(err); - } + var html = marked.parse(str).replace(/\{([^}]+)\}/g, function(_, name){ + return escapeHtml(options[name] || ''); + }); + fn(null, html); }); }); diff --git a/examples/view-locals/user.js b/examples/view-locals/user.js index 0e3373d5..90ab1f38 100644 --- a/examples/view-locals/user.js +++ b/examples/view-locals/user.js @@ -9,6 +9,9 @@ function User(name, age, species) { } User.all = function(fn){ + // process.nextTick makes sure this function API + // behaves in an asynchronous manner, like if it + // was a real DB query to read all users. process.nextTick(function(){ fn(null, users); }); diff --git a/lib/request.js b/lib/request.js index b881df8d..9df73d35 100644 --- a/lib/request.js +++ b/lib/request.js @@ -56,6 +56,14 @@ var req = exports = module.exports = { req.get = req.header = function header(name) { + if (!name) { + throw new TypeError('name argument is required to req.get'); + } + + if (typeof name !== 'string') { + throw new TypeError('name must be a string to req.get'); + } + var lc = name.toLowerCase(); switch (lc) { @@ -161,29 +169,34 @@ req.acceptsLanguages = function(){ }; /** - * Parse Range header field, - * capping to the given `size`. + * Parse Range header field, capping to the given `size`. * - * Unspecified ranges such as "0-" require - * knowledge of your resource length. In - * the case of a byte range this is of course - * the total number of bytes. If the Range - * header field is not given `null` is returned, - * `-1` when unsatisfiable, `-2` when syntactically invalid. + * Unspecified ranges such as "0-" require knowledge of your resource length. In + * the case of a byte range this is of course the total number of bytes. If the + * Range header field is not given `undefined` is returned, `-1` when unsatisfiable, + * and `-2` when syntactically invalid. * - * NOTE: remember that ranges are inclusive, so - * for example "Range: users=0-3" should respond - * with 4 users when available, not 3. + * When ranges are returned, the array has a "type" property which is the type of + * range that is required (most commonly, "bytes"). Each array element is an object + * with a "start" and "end" property for the portion of the range. * - * @param {Number} size - * @return {Array} + * The "combine" option can be set to `true` and overlapping & adjacent ranges + * will be combined into a single range. + * + * NOTE: remember that ranges are inclusive, so for example "Range: users=0-3" + * should respond with 4 users when available, not 3. + * + * @param {number} size + * @param {object} [options] + * @param {boolean} [options.combine=false] + * @return {number|array} * @public */ -req.range = function(size){ +req.range = function range(size, options) { var range = this.get('Range'); if (!range) return; - return parseRange(size, range); + return parseRange(size, range, options); }; /** @@ -282,7 +295,7 @@ defineGetter(req, 'protocol', function protocol(){ /** * Short-hand for: * - * req.protocol == 'https' + * req.protocol === 'https' * * @return {Boolean} * @public @@ -427,10 +440,10 @@ defineGetter(req, 'fresh', function(){ var s = this.res.statusCode; // GET or HEAD for weak freshness validation only - if ('GET' != method && 'HEAD' != method) return false; + if ('GET' !== method && 'HEAD' !== method) return false; // 2xx or 304 as per rfc2616 14.26 - if ((s >= 200 && s < 300) || 304 == s) { + if ((s >= 200 && s < 300) || 304 === s) { return fresh(this.headers, (this.res._headers || {})); } diff --git a/lib/response.js b/lib/response.js index d10d63ee..6ab51223 100644 --- a/lib/response.js +++ b/lib/response.js @@ -14,6 +14,7 @@ var contentDisposition = require('content-disposition'); var deprecate = require('depd')('express'); +var encodeUrl = require('encodeurl'); var escapeHtml = require('escape-html'); var http = require('http'); var onFinished = require('on-finished'); @@ -171,7 +172,7 @@ res.send = function send(body) { if (req.fresh) this.statusCode = 304; // strip irrelevant headers - if (204 == this.statusCode || 304 == this.statusCode) { + if (204 === this.statusCode || 304 === this.statusCode) { this.removeHeader('Content-Type'); this.removeHeader('Content-Length'); this.removeHeader('Transfer-Encoding'); @@ -215,7 +216,7 @@ res.json = function json(obj) { var app = this.app; var replacer = app.get('json replacer'); var spaces = app.get('json spaces'); - var body = JSON.stringify(val, replacer, spaces); + var body = stringify(val, replacer, spaces); // content-type if (!this.get('Content-Type')) { @@ -251,7 +252,7 @@ res.jsonp = function jsonp(obj) { var app = this.app; var replacer = app.get('json replacer'); var spaces = app.get('json spaces'); - var body = JSON.stringify(val, replacer, spaces); + var body = stringify(val, replacer, spaces); var callback = this.req.query[app.get('jsonp callback name')]; // content-type @@ -638,7 +639,7 @@ res.get = function(field){ * Clear cookie `name`. * * @param {String} name - * @param {Object} options + * @param {Object} [options] * @return {ServerResponse} for chaining * @public */ @@ -730,8 +731,7 @@ res.location = function location(url) { } // set location - this.set('Location', loc); - return this; + return this.set('Location', encodeUrl(loc)); }; /** @@ -769,13 +769,12 @@ res.redirect = function redirect(url) { } // Set location header - this.location(address); - address = this.get('Location'); + address = this.location(address).get('Location'); // Support text/{plain,html} by default this.format({ text: function(){ - body = statusCodes[status] + '. Redirecting to ' + encodeURI(address); + body = statusCodes[status] + '. Redirecting to ' + address; }, html: function(){ @@ -949,3 +948,16 @@ function sendfile(res, file, options, callback) { // pipe file.pipe(res); } + +/** + * Stringify JSON, like JSON.stringify, but v8 optimized. + * @private + */ + +function stringify(value, replacer, spaces) { + // v8 checks arguments.length for optimizing simple call + // https://bugs.chromium.org/p/v8/issues/detail?id=4730 + return replacer || spaces + ? JSON.stringify(value, replacer, spaces) + : JSON.stringify(value); +} diff --git a/lib/utils.js b/lib/utils.js index 8c111eb7..078e0fd5 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -102,7 +102,7 @@ function acceptParams(str, index) { for (var i = 1; i < parts.length; ++i) { var pms = parts[i].split(/ *= */); - if ('q' == pms[0]) { + if ('q' === pms[0]) { ret.quality = parseFloat(pms[1]); } else { ret.params[pms[0]] = pms[1]; @@ -242,7 +242,6 @@ exports.setCharset = function setCharset(type, charset) { function parseExtendedQueryString(str) { return qs.parse(str, { - allowDots: false, allowPrototypes: true }); } diff --git a/package.json b/package.json index 856b054b..1ecc060b 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ ], "license": "MIT", "repository": "expressjs/express", + "homepage": "http://expressjs.com/", "keywords": [ "express", "framework", @@ -26,17 +27,18 @@ "api" ], "dependencies": { - "accepts": "~1.2.12", + "accepts": "~1.3.3", "array-flatten": "2.0.0", "content-disposition": "0.5.1", - "content-type": "~1.0.1", - "cookie": "0.1.5", + "content-type": "~1.0.2", + "cookie": "0.3.1", "cookie-signature": "1.0.6", "debug": "~2.2.0", "depd": "~1.1.0", + "encodeurl": "~1.0.1", "escape-html": "~1.0.3", "etag": "~1.7.0", - "finalhandler": "0.4.1", + "finalhandler": "0.5.0", "fresh": "0.3.0", "merge-descriptors": "1.0.1", "methods": "~1.1.2", @@ -44,34 +46,34 @@ "parseurl": "~1.3.1", "path-is-absolute": "1.0.0", "path-to-regexp": "0.1.7", - "proxy-addr": "~1.0.10", - "qs": "4.0.0", - "range-parser": "~1.0.3", + "proxy-addr": "~1.1.2", + "qs": "6.2.0", + "range-parser": "~1.2.0", "router": "~1.1.3", - "send": "0.13.1", - "serve-static": "~1.10.2", - "type-is": "~1.6.6", + "send": "0.14.1", + "serve-static": "~1.11.1", + "type-is": "~1.6.13", "utils-merge": "1.0.0", - "vary": "~1.0.1" + "vary": "~1.1.0" }, "devDependencies": { "after": "0.8.1", - "ejs": "2.3.4", - "istanbul": "0.4.2", + "body-parser": "~1.15.1", + "cookie-parser": "~1.4.3", + "ejs": "2.4.2", + "istanbul": "0.4.3", "marked": "0.3.5", - "mocha": "2.3.4", - "should": "7.1.1", - "supertest": "1.1.0", - "body-parser": "~1.14.2", + "method-override": "~2.3.6", + "mocha": "2.5.3", + "morgan": "~1.7.0", + "should": "9.0.2", + "supertest": "1.2.0", "connect-redis": "~2.4.1", - "cookie-parser": "~1.4.1", "cookie-session": "~1.2.0", "express-session": "~1.13.0", "jade": "~1.11.0", - "method-override": "~2.3.5", - "morgan": "~1.6.1", "multiparty": "~4.1.2", - "vhost": "~3.0.1" + "vhost": "~3.0.2" }, "engines": { "node": ">= 0.10.0" diff --git a/test/Route.js b/test/Route.js index c979fb11..ada54086 100644 --- a/test/Route.js +++ b/test/Route.js @@ -7,6 +7,11 @@ var express = require('../') , assert = require('assert'); describe('Route', function(){ + it('should work without handlers', function(done) { + var req = { method: 'GET', url: '/' } + var route = new Route('/foo') + route.dispatch(req, {}, done) + }) describe('.all', function(){ it('should add handler', function(done){ diff --git a/test/acceptance/cookies.js b/test/acceptance/cookies.js index 86add660..aa9e1fae 100644 --- a/test/acceptance/cookies.js +++ b/test/acceptance/cookies.js @@ -1,6 +1,7 @@ var app = require('../../examples/cookies') , request = require('supertest'); +var utils = require('../support/utils'); describe('cookies', function(){ describe('GET /', function(){ @@ -13,10 +14,8 @@ describe('cookies', function(){ it('should respond with no cookies', function(done){ request(app) .get('/') - .end(function(err, res){ - res.headers.should.not.have.property('set-cookie') - done() - }) + .expect(utils.shouldNotHaveHeader('Set-Cookie')) + .expect(200, done) }) it('should respond to cookie', function(done){ @@ -57,20 +56,16 @@ describe('cookies', function(){ .post('/') .type('urlencoded') .send({ remember: 1 }) - .expect(302, function(err, res){ - res.headers.should.have.property('set-cookie') - done() - }) + .expect('Set-Cookie', /remember=1/) + .expect(302, done) }) it('should no set cookie w/o reminder', function(done){ request(app) .post('/') .send({}) - .expect(302, function(err, res){ - res.headers.should.not.have.property('set-cookie') - done() - }) + .expect(utils.shouldNotHaveHeader('Set-Cookie')) + .expect(302, done) }) }) }) diff --git a/test/acceptance/downloads.js b/test/acceptance/downloads.js index ed9a02f9..a0aa7b75 100644 --- a/test/acceptance/downloads.js +++ b/test/acceptance/downloads.js @@ -15,11 +15,8 @@ describe('downloads', function(){ it('should have a download header', function(done){ request(app) .get('/files/amazing.txt') - .end(function(err, res){ - res.status.should.equal(200); - res.headers.should.have.property('content-disposition', 'attachment; filename="amazing.txt"') - done() - }) + .expect('Content-Disposition', 'attachment; filename="amazing.txt"') + .expect(200, done) }) }) diff --git a/test/middleware.basic.js b/test/middleware.basic.js index 3e1f1813..28a4dd18 100644 --- a/test/middleware.basic.js +++ b/test/middleware.basic.js @@ -32,13 +32,8 @@ describe('middleware', function(){ .get('/') .set('Content-Type', 'application/json') .send('{"foo":"bar"}') - .end(function(err, res){ - if (err) return done(err); - res.headers.should.have.property('content-type', 'application/json'); - res.statusCode.should.equal(200); - res.text.should.equal('{"foo":"bar"}'); - done(); - }) + .expect('Content-Type', 'application/json') + .expect(200, '{"foo":"bar"}', done) }) }) }) diff --git a/test/req.get.js b/test/req.get.js index 144a2568..109a2d90 100644 --- a/test/req.get.js +++ b/test/req.get.js @@ -31,5 +31,29 @@ describe('req', function(){ .set('Referrer', 'http://foobar.com') .expect('http://foobar.com', done); }) + + it('should throw missing header name', function (done) { + var app = express() + + app.use(function (req, res) { + res.end(req.get()) + }) + + request(app) + .get('/') + .expect(500, /TypeError: name argument is required to req.get/, done) + }) + + it('should throw for non-string header name', function (done) { + var app = express() + + app.use(function (req, res) { + res.end(req.get(42)) + }) + + request(app) + .get('/') + .expect(500, /TypeError: name must be a string to req.get/, done) + }) }) -}) \ No newline at end of file +}) diff --git a/test/req.query.js b/test/req.query.js index 8cdfaa85..1caeaa6a 100644 --- a/test/req.query.js +++ b/test/req.query.js @@ -43,7 +43,7 @@ describe('req', function(){ var app = createApp('simple'); request(app) - .get('/?user[name]=tj') + .get('/?user%5Bname%5D=tj') .expect(200, '{"user[name]":"tj"}', done); }); }); @@ -55,7 +55,7 @@ describe('req', function(){ }); request(app) - .get('/?user[name]=tj') + .get('/?user%5Bname%5D=tj') .expect(200, '{"length":17}', done); }); }); @@ -65,7 +65,7 @@ describe('req', function(){ var app = createApp(false); request(app) - .get('/?user[name]=tj') + .get('/?user%5Bname%5D=tj') .expect(200, '{}', done); }); }); @@ -75,7 +75,7 @@ describe('req', function(){ var app = createApp(true); request(app) - .get('/?user[name]=tj') + .get('/?user%5Bname%5D=tj') .expect(200, '{"user[name]":"tj"}', done); }); }); diff --git a/test/req.range.js b/test/req.range.js index 08cf8f91..d530d85a 100644 --- a/test/req.range.js +++ b/test/req.range.js @@ -12,9 +12,9 @@ function req(ret) { describe('req', function(){ describe('.range(size)', function(){ it('should return parsed ranges', function(){ - var ret = [{ start: 0, end: 50 }, { start: 60, end: 100 }]; - ret.type = 'bytes'; - req('bytes=0-50,60-100').range(120).should.eql(ret); + var ranges = [{ start: 0, end: 50 }, { start: 51, end: 100 }] + ranges.type = 'bytes' + assert.deepEqual(req('bytes=0-50,51-100').range(120), ranges) }) it('should cap to the given size', function(){ @@ -35,4 +35,14 @@ describe('req', function(){ assert(req('').range(120) === undefined); }) }) + + describe('.range(size, options)', function(){ + describe('with "combine: true" option', function(){ + it('should return combined ranges', function(){ + var ranges = [{ start: 0, end: 100 }] + ranges.type = 'bytes' + assert.deepEqual(req('bytes=0-50,51-100').range(120, { combine: true }), ranges) + }) + }) + }) }) diff --git a/test/res.cookie.js b/test/res.cookie.js index 8de6c0c6..543af99d 100644 --- a/test/res.cookie.js +++ b/test/res.cookie.js @@ -150,6 +150,22 @@ describe('res', function(){ }) }) + describe('signed without secret', function(){ + it('should throw an error', function(done){ + var app = express(); + + app.use(cookieParser()); + + app.use(function(req, res){ + res.cookie('name', 'tobi', { signed: true }).end(); + }); + + request(app) + .get('/') + .expect(500, /secret\S+ required for signed cookies/, done); + }) + }) + describe('.signedCookie(name, string)', function(){ it('should set a signed cookie', function(done){ var app = express(); diff --git a/test/res.jsonp.js b/test/res.jsonp.js index 490daff7..6bf49d94 100644 --- a/test/res.jsonp.js +++ b/test/res.jsonp.js @@ -2,6 +2,7 @@ var express = require('../') , request = require('supertest') , assert = require('assert'); +var utils = require('./support/utils'); describe('res', function(){ describe('.jsonp(object)', function(){ @@ -136,11 +137,8 @@ describe('res', function(){ request(app) .get('/') .expect('Content-Type', 'application/vnd.example+json; charset=utf-8') - .expect(200, '{"hello":"world"}', function (err, res) { - if (err) return done(err); - res.headers.should.not.have.property('x-content-type-options'); - done(); - }); + .expect(utils.shouldNotHaveHeader('X-Content-Type-Options')) + .expect(200, '{"hello":"world"}', done); }) it('should override previous Content-Types with callback', function(done){ diff --git a/test/res.location.js b/test/res.location.js index 64811601..bb9eb6f9 100644 --- a/test/res.location.js +++ b/test/res.location.js @@ -13,10 +13,34 @@ describe('res', function(){ request(app) .get('/') - .end(function(err, res){ - res.headers.should.have.property('location', 'http://google.com'); - done(); + .expect('Location', 'http://google.com') + .expect(200, done) + }) + + it('should encode "url"', function (done) { + var app = express() + + app.use(function (req, res) { + res.location('https://google.com?q=\u2603 §10').end() }) + + request(app) + .get('/') + .expect('Location', 'https://google.com?q=%E2%98%83%20%C2%A710') + .expect(200, done) + }) + + it('should not touch already-encoded sequences in "url"', function (done) { + var app = express() + + app.use(function (req, res) { + res.location('https://google.com?q=%A710').end() + }) + + request(app) + .get('/') + .expect('Location', 'https://google.com?q=%A710') + .expect(200, done) }) }) }) diff --git a/test/res.redirect.js b/test/res.redirect.js index 7ebc1ef0..9f3bd436 100644 --- a/test/res.redirect.js +++ b/test/res.redirect.js @@ -2,6 +2,7 @@ var http = require('http'); var express = require('..'); var request = require('supertest'); +var utils = require('./support/utils'); describe('res', function(){ describe('.redirect(url)', function(){ @@ -17,6 +18,32 @@ describe('res', function(){ .expect('location', 'http://google.com') .expect(302, done) }) + + it('should encode "url"', function (done) { + var app = express() + + app.use(function (req, res) { + res.redirect('https://google.com?q=\u2603 §10') + }) + + request(app) + .get('/') + .expect('Location', 'https://google.com?q=%E2%98%83%20%C2%A710') + .expect(302, done) + }) + + it('should not touch already-encoded sequences in "url"', function (done) { + var app = express() + + app.use(function (req, res) { + res.redirect('https://google.com?q=%A710') + }) + + request(app) + .get('/') + .expect('Location', 'https://google.com?q=%A710') + .expect(302, done) + }) }) describe('.redirect(status, url)', function(){ @@ -29,11 +56,8 @@ describe('res', function(){ request(app) .get('/') - .end(function(err, res){ - res.statusCode.should.equal(303); - res.headers.should.have.property('location', 'http://google.com'); - done(); - }) + .expect('Location', 'http://google.com') + .expect(303, done) }) }) @@ -47,11 +71,8 @@ describe('res', function(){ request(app) .get('/') - .end(function(err, res){ - res.statusCode.should.equal(303); - res.headers.should.have.property('location', 'http://google.com'); - done(); - }) + .expect('Location', 'http://google.com') + .expect(303, done) }) }) @@ -65,11 +86,8 @@ describe('res', function(){ request(app) .head('/') - .end(function(err, res){ - res.headers.should.have.property('location', 'http://google.com'); - res.text.should.equal(''); - done(); - }) + .expect('Location', 'http://google.com') + .expect(302, '', done) }) }) @@ -93,7 +111,7 @@ describe('res', function(){ var app = express(); app.use(function(req, res){ - res.redirect(''); + res.redirect(''); }); request(app) @@ -101,8 +119,8 @@ describe('res', function(){ .set('Host', 'http://example.com') .set('Accept', 'text/html') .expect('Content-Type', /html/) - .expect('Location', '') - .expect(302, '

' + http.STATUS_CODES[302] + '. Redirecting to <lame>

', done); + .expect('Location', '%3Cla\'me%3E') + .expect(302, '

' + http.STATUS_CODES[302] + '. Redirecting to %3Cla'me%3E

', done) }) it('should include the redirect type', function(done){ @@ -149,8 +167,8 @@ describe('res', function(){ .set('Host', 'http://example.com') .set('Accept', 'text/plain, */*') .expect('Content-Type', /plain/) - .expect('Location', 'http://example.com/?param=') - .expect(302, http.STATUS_CODES[302] + '. Redirecting to http://example.com/?param=%3Cscript%3Ealert(%22hax%22);%3C/script%3E', done); + .expect('Location', 'http://example.com/?param=%3Cscript%3Ealert(%22hax%22);%3C/script%3E') + .expect(302, http.STATUS_CODES[302] + '. Redirecting to http://example.com/?param=%3Cscript%3Ealert(%22hax%22);%3C/script%3E', done) }) it('should include the redirect type', function(done){ @@ -182,11 +200,8 @@ describe('res', function(){ .set('Accept', 'application/octet-stream') .expect('location', 'http://google.com') .expect('content-length', '0') - .expect(302, '', function(err, res){ - if (err) return done(err) - res.headers.should.not.have.property('content-type'); - done(); - }) + .expect(utils.shouldNotHaveHeader('Content-Type')) + .expect(302, '', done) }) }) }) diff --git a/test/res.send.js b/test/res.send.js index c0e2a576..6b1203e0 100644 --- a/test/res.send.js +++ b/test/res.send.js @@ -3,6 +3,7 @@ var assert = require('assert'); var express = require('..'); var methods = require('methods'); var request = require('supertest'); +var utils = require('./support/utils'); describe('res', function(){ describe('.send()', function(){ @@ -103,12 +104,8 @@ describe('res', function(){ request(app) .get('/') - .end(function(err, res){ - res.headers.should.have.property('content-type', 'text/html; charset=utf-8'); - res.text.should.equal('

hey

'); - res.statusCode.should.equal(200); - done(); - }) + .expect('Content-Type', 'text/html; charset=utf-8') + .expect(200, '

hey

', done); }) it('should set ETag', function (done) { @@ -175,12 +172,8 @@ describe('res', function(){ request(app) .get('/') - .end(function(err, res){ - res.headers.should.have.property('content-type', 'application/octet-stream'); - res.text.should.equal('hello'); - res.statusCode.should.equal(200); - done(); - }) + .expect('Content-Type', 'application/octet-stream') + .expect(200, 'hello', done); }) it('should set ETag', function (done) { @@ -206,12 +199,8 @@ describe('res', function(){ request(app) .get('/') - .end(function(err, res){ - res.headers.should.have.property('content-type', 'text/plain; charset=utf-8'); - res.text.should.equal('hey'); - res.statusCode.should.equal(200); - done(); - }) + .expect('Content-Type', 'text/plain; charset=utf-8') + .expect(200, 'hey', done); }) }) @@ -254,13 +243,10 @@ describe('res', function(){ request(app) .get('/') - .end(function(err, res){ - res.headers.should.not.have.property('content-type'); - res.headers.should.not.have.property('content-length'); - res.headers.should.not.have.property('transfer-encoding'); - res.text.should.equal(''); - done(); - }) + .expect(utils.shouldNotHaveHeader('Content-Type')) + .expect(utils.shouldNotHaveHeader('Content-Length')) + .expect(utils.shouldNotHaveHeader('Transfer-Encoding')) + .expect(204, '', done); }) }) @@ -274,13 +260,10 @@ describe('res', function(){ request(app) .get('/') - .end(function(err, res){ - res.headers.should.not.have.property('content-type'); - res.headers.should.not.have.property('content-length'); - res.headers.should.not.have.property('transfer-encoding'); - res.text.should.equal(''); - done(); - }) + .expect(utils.shouldNotHaveHeader('Content-Type')) + .expect(utils.shouldNotHaveHeader('Content-Length')) + .expect(utils.shouldNotHaveHeader('Transfer-Encoding')) + .expect(304, '', done); }) }) @@ -436,7 +419,7 @@ describe('res', function(){ request(app) .get('/') - .expect(shouldNotHaveHeader('ETag')) + .expect(utils.shouldNotHaveHeader('ETag')) .expect(200, done); }) }); @@ -454,7 +437,7 @@ describe('res', function(){ request(app) .get('/') - .expect(shouldNotHaveHeader('ETag')) + .expect(utils.shouldNotHaveHeader('ETag')) .expect(200, done); }); @@ -544,15 +527,9 @@ describe('res', function(){ request(app) .get('/') - .expect(shouldNotHaveHeader('ETag')) + .expect(utils.shouldNotHaveHeader('ETag')) .expect(200, done); }) }) }) }) - -function shouldNotHaveHeader(header) { - return function (res) { - assert.ok(!(header.toLowerCase() in res.headers), 'should not have header ' + header) - } -} diff --git a/test/res.sendFile.js b/test/res.sendFile.js index 19200d4b..3179581d 100644 --- a/test/res.sendFile.js +++ b/test/res.sendFile.js @@ -7,6 +7,7 @@ var onFinished = require('on-finished'); var path = require('path'); var should = require('should'); var fixtures = path.join(__dirname, 'fixtures'); +var utils = require('./support/utils'); describe('res', function(){ describe('.sendFile(path)', function () { @@ -154,11 +155,8 @@ describe('res', function(){ request(app) .get('/') - .expect(404, function (err, res) { - if (err) return done(err); - res.headers.should.not.have.property('x-success'); - done(); - }); + .expect(utils.shouldNotHaveHeader('X-Success')) + .expect(404, done); }); }); @@ -287,6 +285,14 @@ describe('res', function(){ .expect(200, 'got it', done); }) }) + + describe('.sendFile(path, options)', function () { + it('should pass options to send module', function (done) { + request(createApp(path.resolve(fixtures, 'name.txt'), { start: 0, end: 1 })) + .get('/') + .expect(200, 'to', done) + }) + }) }) function createApp(path, options, fn) { diff --git a/test/res.vary.js b/test/res.vary.js index 13c3af2b..9a2edd24 100644 --- a/test/res.vary.js +++ b/test/res.vary.js @@ -2,6 +2,7 @@ var assert = require('assert'); var express = require('..'); var request = require('supertest'); +var utils = require('./support/utils'); describe('res.vary()', function(){ describe('with no arguments', function(){ @@ -15,7 +16,7 @@ describe('res.vary()', function(){ request(app) .get('/') - .expect(shouldNotHaveHeader('Vary')) + .expect(utils.shouldNotHaveHeader('Vary')) .expect(200, done); }) }) @@ -31,7 +32,7 @@ describe('res.vary()', function(){ request(app) .get('/') - .expect(shouldNotHaveHeader('Vary')) + .expect(utils.shouldNotHaveHeader('Vary')) .expect(200, done); }) }) @@ -88,9 +89,3 @@ describe('res.vary()', function(){ }) }) }) - -function shouldNotHaveHeader(header) { - return function (res) { - assert.ok(!(header.toLowerCase() in res.headers), 'should not have header ' + header); - }; -} diff --git a/test/support/utils.js b/test/support/utils.js new file mode 100644 index 00000000..ec6b801b --- /dev/null +++ b/test/support/utils.js @@ -0,0 +1,24 @@ + +/** + * Module dependencies. + * @private + */ +var assert = require('assert'); + +/** + * Module exports. + * @public + */ +exports.shouldNotHaveHeader = shouldNotHaveHeader; + +/** + * Assert that a supertest response does not have a header. + * + * @param {string} header Header name to check + * @returns {function} + */ +function shouldNotHaveHeader(header) { + return function (res) { + assert.ok(!(header.toLowerCase() in res.headers), 'should not have header ' + header); + }; +}