diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 62562b74..00000000 --- a/.eslintignore +++ /dev/null @@ -1,2 +0,0 @@ -coverage -node_modules diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 07eb9654..00000000 --- a/.eslintrc +++ /dev/null @@ -1,8 +0,0 @@ -{ - "rules": { - "eol-last": "error", - "indent": ["error", 2, { "SwitchCase": 1 }], - "no-trailing-spaces": "error", - "semi": ["error", "never"] - } -} diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000..cfedcac1 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1 @@ +29a69f3cf7cb5acbdde0a9151b885d46423b0c72 diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..17c705f6 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: monthly + + - package-ecosystem: npm + directory: / + schedule: + interval: monthly + time: "23:00" + timezone: Europe/London + open-pull-requests-limit: 10 + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-major"] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..dffb14f4 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,74 @@ +name: ci +on: + workflow_call: + workflow_dispatch: + push: + branches: + - master + pull_request: + branches: + - master + +permissions: + contents: read + +jobs: + test: + name: Test - Node.js ${{ matrix.node-version }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: [18, 19, 20, 21, 22, 23, 24] + + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: ${{ matrix.node-version }} + check-latest: true + + - name: Install Node.js dependencies + run: npm install + + - name: Run tests + run: npm run test-ci + + - name: Lint code + run: npm run lint + + - name: Upload code coverage + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: coverage-node-${{ matrix.node-version }} + path: ./coverage/lcov.info + retention-days: 1 + + coverage: + needs: test + runs-on: ubuntu-latest + permissions: + contents: read + checks: write + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Install lcov + shell: bash + run: sudo apt-get -y install lcov + + - name: Collect coverage reports + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + path: ./coverage + pattern: coverage-node-* + + - name: Merge coverage reports + shell: bash + run: find ./coverage -name lcov.info -exec printf '-a %q\n' {} \; | xargs lcov -o ./lcov.info + + - name: Upload coverage report + uses: coverallsapp/github-action@648a8eb78e6d50909eff900e4ec85cab4524a45b # v2.3.6 + with: + file: ./lcov.info diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..cc15ef79 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,73 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: ["master"] + pull_request: + # The branches below must be a subset of the branches above + branches: ["master"] + schedule: + - cron: "0 0 * * 1" + +permissions: + contents: read + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: ["javascript"] + # CodeQL supports [ $supported-codeql-languages ] + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 + + # â„šī¸ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18 + with: + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 00000000..d23de00e --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,73 @@ +# This workflow uses actions that are not certified by GitHub. They are provided +# by a third-party and are governed by separate terms of service, privacy +# policy, and support documentation. + +name: Scorecard supply-chain security + +on: + # For Branch-Protection check. Only the default branch is supported. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection + branch_protection_rule: + # To guarantee Maintained check is occasionally updated. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained + schedule: + - cron: '16 21 * * 1' + push: + branches: [ "master" ] + +# Declare default permissions as read only. +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + # Needed to publish results and get a badge (see publish_results below). + id-token: write + # Uncomment the permissions below if installing in a private repository. + # contents: read + # actions: read + + steps: + - name: "Checkout code" + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - name: "Run analysis" + uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0 + with: + results_file: results.sarif + results_format: sarif + # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: + # - you want to enable the Branch-Protection check on a *public* repository, or + # - you are installing Scorecard on a *private* repository + # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. + # repo_token: ${{ secrets.SCORECARD_TOKEN }} + + # Public repositories: + # - Publish results to OpenSSF REST API for easy access by consumers + # - Allows the repository to include the Scorecard badge. + # - See https://github.com/ossf/scorecard-action#publishing-results. + # For private repositories: + # - `publish_results` will always be set to `false`, regardless + # of the value entered here. + publish_results: true + + # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF + # format to the repository Actions tab. + - name: "Upload artifact" + uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0 + with: + name: SARIF file + path: results.sarif + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard. + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@df409f7d9260372bd5f19e5b04e83cb3c43714ae # v3.27.9 + with: + sarif_file: results.sarif \ No newline at end of file diff --git a/.gitignore b/.gitignore index 0fa6951f..f15b98e2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.nyc_output/ coverage/ node_modules/ npm-debug.log diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 719be7ab..00000000 --- a/.travis.yml +++ /dev/null @@ -1,36 +0,0 @@ -language: node_js -node_js: - - "0.8" - - "0.10" - - "0.12" - - "1.8" - - "2.5" - - "3.3" - - "4.9" - - "5.12" - - "6.17" - - "7.10" - - "8.16" - - "9.11" - - "10.15" - - "11.15" -sudo: false -cache: - directories: - - node_modules -before_install: - # Skip updating shrinkwrap / lock - - "npm config set shrinkwrap false" - # Setup Node.js version-specific dependencies - - "test $TRAVIS_NODE_VERSION != '0.8' || npm rm --save-dev istanbul" - - "test $(echo $TRAVIS_NODE_VERSION | cut -d. -f1) -ge 4 || npm rm --save-dev eslint eslint-plugin-markdown" - # Update Node.js modules - - "test ! -d node_modules || npm prune" - - "test ! -d node_modules || npm rebuild" -script: - # Run test script, depending on istanbul install - - "test ! -z $(npm -ps ls istanbul) || npm test" - - "test -z $(npm -ps ls istanbul) || npm run-script test-travis" - - "test -z $(npm -ps ls eslint) || npm run-script lint" -after_script: - - "test -e ./coverage/lcov.info && npm install coveralls@2 && cat ./coverage/lcov.info | coveralls" diff --git a/HISTORY.md b/HISTORY.md index 408414c2..b292257a 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,8 +1,106 @@ -unreleased -========== +2.2.0 / 2025-03-26 +================== + +* Remove `setImmediate` support check +* Restore `debug` dependency + +2.1.0 / 2025-02-10 +================== + +* Updated `engines` field to Node@18 or higher +* Remove `Object.setPrototypeOf` polyfill +* Use `Array.flat` instead of `array-flatten` package +* Replace `methods` dependency with standard library +* deps: parseurl@^1.3.3 +* deps: is-promise@^4.0.0 +* Replace `utils-merge` dependency with `Object.assign` +* deps: Remove unused dep `after` + +2.0.0 / 2024-09-09 +================== + +* Drop support for node <18 +* deps: path-to-regexp@^8.0.0 + - Drop support for partial capture group `router.route('/user(s?)/:user/:op')` but still have optional non-capture `/user{s}/:user/:op` + - `:name?` becomes `{:name}` + - `:name*` becomes `*name`. + - The splat change also changes splat from strings to an array of strings + - Optional splats become `{*name}` + - `:name+` becomes `*name` and thus equivalent to `*name` so I dropped those tests + - Strings as regular expressions are fully removed, need to be converted to native regular expressions + +2.0.0-beta.2 / 2024-03-20 +========================= + +This incorporates all changes after 1.3.5 up to 1.3.8. + + * Add support for returned, rejected Promises to `router.param` + +2.0.0-beta.1 / 2020-03-29 +========================= + +This incorporates all changes after 1.3.3 up to 1.3.5. + + * Internalize private `router.process_params` method + * Remove `debug` dependency + * deps: array-flatten@3.0.0 + * deps: parseurl@~1.3.3 + * deps: path-to-regexp@3.2.0 + - Add new `?`, `*`, and `+` parameter modifiers. + - Matching group expressions are only RegExp syntax. + `(*)` is no longer valid and must be written as `(.*)`, for example. + - Named matching groups no longer available by position in `req.params`. + `/:foo(.*)` only captures as `req.params.foo` and not available as + `req.params[0]`. + - Regular expressions can only be used in a matching group. + `/\\d+` is no longer valid and must be written as `/(\\d+)`. + - Matching groups are now literal regular expressions. + `:foo` named captures can no longer be included inside a capture group. + - Special `*` path segment behavior removed. + `/foo/*/bar` will match a literal `*` as the middle segment. + * deps: setprototypeof@1.2.0 + +2.0.0-alpha.1 / 2018-07-27 +========================== + + * Add basic support for returned, rejected Promises + - Rejected Promises from middleware functions `next(error)` + * Drop support for Node.js below 0.10 + * deps: debug@3.1.0 + - Add `DEBUG_HIDE_DATE` environment variable + - Change timer to per-namespace instead of global + - Change non-TTY date format + - Remove `DEBUG_FD` environment variable support + - Support 256 namespace colors + +1.3.8 / 2023-02-24 +================== + + * Fix routing requests without method + +1.3.7 / 2022-04-28 +================== + + * Fix hanging on large stack of sync routes + +1.3.6 / 2021-11-15 +================== + + * Fix handling very large stacks of sync middleware + * deps: safe-buffer@5.2.1 + +1.3.5 / 2020-03-24 +================== + + * Fix incorrect middleware execution with unanchored `RegExp`s + * perf: use plain object for internal method map + +1.3.4 / 2020-01-24 +================== - * deps: array-flatten@2.1.2 - * deps: setprototypeof@1.1.1 + * deps: array-flatten@3.0.0 + * deps: parseurl@~1.3.3 + * deps: setprototypeof@1.2.0 1.3.3 / 2018-07-06 ================== diff --git a/LICENSE b/LICENSE index 31e7f7d4..237e1b67 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ (The MIT License) Copyright (c) 2013 Roman Shtylman -Copyright (c) 2014 Douglas Christopher Wilson +Copyright (c) 2014-2022 Douglas Christopher Wilson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/README.md b/README.md index 15d9b66c..156c380c 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![NPM Version][npm-image]][npm-url] [![NPM Downloads][downloads-image]][downloads-url] [![Node.js Version][node-version-image]][node-version-url] -[![Build Status][travis-image]][travis-url] +[![Build Status][ci-image]][ci-url] [![Test Coverage][coveralls-image]][coveralls-url] Simple middleware-style router @@ -22,8 +22,8 @@ $ npm install router ```js var finalhandler = require('finalhandler') -var http = require('http') -var Router = require('router') +var http = require('http') +var Router = require('router') var router = Router() router.get('/', function (req, res) { @@ -31,7 +31,7 @@ router.get('/', function (req, res) { res.end('Hello World!') }) -var server = http.createServer(function(req, res) { +var server = http.createServer(function (req, res) { router(req, res, finalhandler(req, res)) }) @@ -66,6 +66,8 @@ consider it one for handling `OPTIONS` requests. * Note: If a `path` is specified, that `path` is stripped from the start of `req.url`. + + ```js router.use(function (req, res, next) { // do your things @@ -89,6 +91,8 @@ the routing functionality in `router`. Method middleware and handlers follow usual [middleware](#middleware) behavior, except they will only be called when the method and path match the request. + + ```js // handle a `GET` request router.get('/', function (req, res) { @@ -111,11 +115,18 @@ Maps the specified path parameter `name` to a specialized param-capturing middle This function positions the middleware in the same stack as `.use`. +The function can optionally return a `Promise` object. If a `Promise` object +is returned from the function, the router will attach an `onRejected` callback +using `.then`. If the promise is rejected, `next` will be called with the +rejected value, or an error if the value is falsy. + Parameter mapping is used to provide pre-conditions to routes which use normalized placeholders. For example a _:user_id_ parameter could automatically load a user's information from the database without any additional code: + + ```js router.param('user_id', function (req, res, next, id) { User.find(id, function (err, user) { @@ -142,20 +153,24 @@ Routes can be used to handle http `methods` with their own, optional middleware. Using `router.route(path)` is a recommended approach to avoiding duplicate route naming and thus typo errors. + + ```js var api = router.route('/api/') ``` ## Router.Route(path) -Represents a single route as an instance that can be used can be used to handle -http `methods` with it's own, optional middleware. +Represents a single route as an instance that can be used to handle http +`methods` with it's own, optional middleware. ### route\[method](handler) These are functions which you can directly call on a route to register a new `handler` for the `method` on the route. + + ```js // handle a `GET` request var status = router.route('/status') @@ -173,16 +188,18 @@ Adds a handler for all HTTP methods to this route. The handler can behave like middleware and call `next` to continue processing rather than responding. + + ```js router.route('/') -.all(function (req, res, next) { - next() -}) -.all(check_something) -.get(function (req, res) { - res.setHeader('Content-Type', 'text/plain; charset=utf-8') - res.end('Hello World!') -}) + .all(function (req, res, next) { + next() + }) + .all(checkSomething) + .get(function (req, res) { + res.setHeader('Content-Type', 'text/plain; charset=utf-8') + res.end('Hello World!') + }) ``` ## Middleware @@ -195,6 +212,11 @@ format is with three parameters - "req", "res" and "next". - `res` - This is a [HTTP server response](https://nodejs.org/api/http.html#http_class_http_serverresponse) instance. - `next` - Calling this function that tells `router` to proceed to the next matching middleware or method handler. It accepts an error as the first argument. +The function can optionally return a `Promise` object. If a `Promise` object +is returned from the function, the router will attach an `onRejected` callback +using `.then`. If the promise is rejected, `next` will be called with the +rejected value, or an error if the value is falsy. + Middleware and method handlers can also be defined with four arguments. When the function has four parameters defined, the first argument is an error and subsequent arguments remain, becoming - "err", "req", "res", "next". These @@ -202,13 +224,15 @@ functions are "error handling middleware", and can be used for handling errors that occurred in previous handlers (E.g. from calling `next(err)`). This is most used when you want to define arbitrary rendering of errors. + + ```js router.get('/error_route', function (req, res, next) { return next(new Error('Bad Request')) }) router.use(function (err, req, res, next) { - res.end(err.message) //=> "Bad Request" + res.end(err.message) //= > "Bad Request" }) ``` @@ -220,18 +244,18 @@ bypassed - only error handling middleware will be invoked with an error. ```js // import our modules -var http = require('http') -var Router = require('router') +var http = require('http') +var Router = require('router') var finalhandler = require('finalhandler') -var compression = require('compression') -var bodyParser = require('body-parser') +var compression = require('compression') +var bodyParser = require('body-parser') // store our message to display -var message = "Hello World!" +var message = 'Hello World!' // initialize the router & server and add a final callback. var router = Router() -var server = http.createServer(function onRequest(req, res) { +var server = http.createServer(function onRequest (req, res) { router(req, res, finalhandler(req, res)) }) @@ -285,17 +309,16 @@ curl http://127.0.0.1:8080/api/set-message -X PATCH -H "Content-Type: applicatio ### Example using mergeParams ```js -var http = require('http') -var Router = require('router') +var http = require('http') +var Router = require('router') var finalhandler = require('finalhandler') // this example is about the mergeParams option var opts = { mergeParams: true } -// make a router with out special options +// make a router with our special options var router = Router(opts) -var server = http.createServer(function onRequest(req, res) { - +var server = http.createServer(function onRequest (req, res) { // set something to be passed into the router req.params = { type: 'kitten' } @@ -306,7 +329,7 @@ router.get('/', function (req, res) { res.statusCode = 200 res.setHeader('Content-Type', 'text/plain; charset=utf-8') - // with respond with the the params that were passed in + // with respond with the params that were passed in res.end(req.params.type + '\n') }) @@ -321,7 +344,7 @@ handler.get('/', function (req, res) { res.setHeader('Content-Type', 'text/plain; charset=utf-8') // will respond with the param of the router's parent route - res.end(path + '\n') + res.end(req.params.path + '\n') }) // make our http server listen to connections @@ -351,7 +374,7 @@ var Router = require('router') // create the router and server var router = new Router() -var server = http.createServer(function onRequest(req, res) { +var server = http.createServer(function onRequest (req, res) { router(req, res, finalhandler(req, res)) }) @@ -381,12 +404,12 @@ server.listen(8080) [MIT](LICENSE) +[ci-image]: https://badgen.net/github/checks/pillarjs/router/master?label=ci +[ci-url]: https://github.com/pillarjs/router/actions/workflows/ci.yml [npm-image]: https://img.shields.io/npm/v/router.svg [npm-url]: https://npmjs.org/package/router [node-version-image]: https://img.shields.io/node/v/router.svg [node-version-url]: http://nodejs.org/download/ -[travis-image]: https://img.shields.io/travis/pillarjs/router/master.svg -[travis-url]: https://travis-ci.org/pillarjs/router [coveralls-image]: https://img.shields.io/coveralls/pillarjs/router/master.svg [coveralls-url]: https://coveralls.io/r/pillarjs/router?branch=master [downloads-image]: https://img.shields.io/npm/dm/router.svg diff --git a/index.js b/index.js index a8927daf..4358aebd 100644 --- a/index.js +++ b/index.js @@ -1,7 +1,7 @@ /*! * router * Copyright(c) 2013 Roman Shtylman - * Copyright(c) 2014 Douglas Christopher Wilson + * Copyright(c) 2014-2022 Douglas Christopher Wilson * MIT Licensed */ @@ -12,26 +12,22 @@ * @private */ -var debug = require('debug')('router') -var flatten = require('array-flatten') -var Layer = require('./lib/layer') -var methods = require('methods') -var mixin = require('utils-merge') -var parseUrl = require('parseurl') -var Route = require('./lib/route') -var setPrototypeOf = require('setprototypeof') +const isPromise = require('is-promise') +const Layer = require('./lib/layer') +const { METHODS } = require('node:http') +const parseUrl = require('parseurl') +const Route = require('./lib/route') +const debug = require('debug')('router') +const deprecate = require('depd')('router') /** * Module variables. * @private */ -var slice = Array.prototype.slice - -/* istanbul ignore next */ -var defer = typeof setImmediate === 'function' - ? setImmediate - : function(fn){ process.nextTick(fn.bind.apply(fn, arguments)) } +const slice = Array.prototype.slice +const flatten = Array.prototype.flat +const methods = METHODS.map((method) => method.toLowerCase()) /** * Expose `Router`. @@ -53,19 +49,19 @@ module.exports.Route = Route * @public */ -function Router(options) { +function Router (options) { if (!(this instanceof Router)) { return new Router(options) } - var opts = options || {} + const opts = options || {} - function router(req, res, next) { + function router (req, res, next) { router.handle(req, res, next) } // inherit from the correct prototype - setPrototypeOf(router, this) + Object.setPrototypeOf(router, this) router.caseSensitive = opts.caseSensitive router.mergeParams = opts.mergeParams @@ -116,7 +112,7 @@ Router.prototype = function () {} * @public */ -Router.prototype.param = function param(name, fn) { +Router.prototype.param = function param (name, fn) { if (!name) { throw new TypeError('argument name is required') } @@ -133,7 +129,7 @@ Router.prototype.param = function param(name, fn) { throw new TypeError('argument fn must be a function') } - var params = this.params[name] + let params = this.params[name] if (!params) { params = this.params[name] = [] @@ -150,28 +146,29 @@ Router.prototype.param = function param(name, fn) { * @private */ -Router.prototype.handle = function handle(req, res, callback) { +Router.prototype.handle = function handle (req, res, callback) { if (!callback) { throw new TypeError('argument callback is required') } debug('dispatching %s %s', req.method, req.url) - var idx = 0 - var methods - var protohost = getProtohost(req.url) || '' - var removed = '' - var self = this - var slashAdded = false - var paramcalled = {} + let idx = 0 + let methods + const protohost = getProtohost(req.url) || '' + let removed = '' + const self = this + let slashAdded = false + let sync = 0 + const paramcalled = {} // middleware and routes - var stack = this.stack + const stack = this.stack // manage inter-router variables - var parentParams = req.params - var parentUrl = req.baseUrl || '' - var done = restore(callback, req, 'baseUrl', 'next', 'params') + const parentParams = req.params + const parentUrl = req.baseUrl || '' + let done = restore(callback, req, 'baseUrl', 'next', 'params') // setup next layer req.next = next @@ -188,47 +185,52 @@ Router.prototype.handle = function handle(req, res, callback) { next() - function next(err) { - var layerError = err === 'route' + function next (err) { + let layerError = err === 'route' ? null : err // remove added slash if (slashAdded) { - req.url = req.url.substr(1) + req.url = req.url.slice(1) slashAdded = false } // restore altered req.url if (removed.length !== 0) { req.baseUrl = parentUrl - req.url = protohost + removed + req.url.substr(protohost.length) + req.url = protohost + removed + req.url.slice(protohost.length) removed = '' } // signal to exit router if (layerError === 'router') { - defer(done, null) + setImmediate(done, null) return } // no more matching layers if (idx >= stack.length) { - defer(done, layerError) + setImmediate(done, layerError) return } + // max sync stack + if (++sync > 100) { + return setImmediate(next, err) + } + // get pathname of request - var path = getPathname(req) + const path = getPathname(req) if (path == null) { return done(layerError) } // find next matching layer - var layer - var match - var route + let layer + let match + let route while (match !== true && idx < stack.length) { layer = stack[idx++] @@ -255,18 +257,17 @@ Router.prototype.handle = function handle(req, res, callback) { continue } - var method = req.method - var has_method = route._handles_method(method) + const method = req.method + const hasMethod = route._handlesMethod(method) // build up automatic options response - if (!has_method && method === 'OPTIONS' && methods) { + if (!hasMethod && method === 'OPTIONS' && methods) { methods.push.apply(methods, route._methods()) } // don't even bother matching route - if (!has_method && method !== 'HEAD') { + if (!hasMethod && method !== 'HEAD') { match = false - continue } } @@ -284,26 +285,32 @@ Router.prototype.handle = function handle(req, res, callback) { req.params = self.mergeParams ? mergeParams(layer.params, parentParams) : layer.params - var layerPath = layer.path + const layerPath = layer.path // this should be done for the layer - self.process_params(layer, paramcalled, req, res, function (err) { + processParams(self.params, layer, paramcalled, req, res, function (err) { if (err) { - return next(layerError || err) + next(layerError || err) + } else if (route) { + layer.handleRequest(req, res, next) + } else { + trimPrefix(layer, layerError, layerPath, path) } - if (route) { - return layer.handle_request(req, res, next) - } - - trim_prefix(layer, layerError, layerPath, path) + sync = 0 }) } - function trim_prefix(layer, layerError, layerPath, path) { + function trimPrefix (layer, layerError, layerPath, path) { if (layerPath.length !== 0) { + // Validate path is a prefix match + if (layerPath !== path.substring(0, layerPath.length)) { + next(layerError) + return + } + // Validate path breaks on a path separator - var c = path[layerPath.length] + const c = path[layerPath.length] if (c && c !== '/') { next(layerError) return @@ -313,7 +320,7 @@ Router.prototype.handle = function handle(req, res, callback) { // middleware (.use stuff) needs to have the path stripped debug('trim prefix (%s) from url %s', layerPath, req.url) removed = layerPath - req.url = protohost + req.url.substr(protohost.length + removed.length) + req.url = protohost + req.url.slice(protohost.length + removed.length) // Ensure leading slash if (!protohost && req.url[0] !== '/') { @@ -330,105 +337,13 @@ Router.prototype.handle = function handle(req, res, callback) { debug('%s %s : %s', layer.name, layerPath, req.originalUrl) if (layerError) { - layer.handle_error(layerError, req, res, next) + layer.handleError(layerError, req, res, next) } else { - layer.handle_request(req, res, next) + layer.handleRequest(req, res, next) } } } -/** - * Process any parameters for the layer. - * - * @private - */ - -Router.prototype.process_params = function process_params(layer, called, req, res, done) { - var params = this.params - - // captured parameters from the layer, keys and values - var keys = layer.keys - - // fast track - if (!keys || keys.length === 0) { - return done() - } - - var i = 0 - var name - var paramIndex = 0 - var key - var paramVal - var paramCallbacks - var paramCalled - - // process params in order - // param callbacks can be async - function param(err) { - if (err) { - return done(err) - } - - if (i >= keys.length ) { - return done() - } - - paramIndex = 0 - key = keys[i++] - name = key.name - paramVal = req.params[name] - paramCallbacks = params[name] - paramCalled = called[name] - - if (paramVal === undefined || !paramCallbacks) { - return param() - } - - // param previously called with same value or error occurred - if (paramCalled && (paramCalled.match === paramVal - || (paramCalled.error && paramCalled.error !== 'route'))) { - // restore value - req.params[name] = paramCalled.value - - // next param - return param(paramCalled.error) - } - - called[name] = paramCalled = { - error: null, - match: paramVal, - value: paramVal - } - - paramCallback() - } - - // single param callbacks - function paramCallback(err) { - var fn = paramCallbacks[paramIndex++] - - // store updated value - paramCalled.value = req.params[key.name] - - if (err) { - // store error - paramCalled.error = err - param(err) - return - } - - if (!fn) return param() - - try { - fn(req, res, paramCallback, paramVal, key.name) - } catch (e) { - paramCallback(e) - } - } - - param() -} - /** * Use the given middleware function, with optional path, defaulting to "/". * @@ -444,14 +359,14 @@ Router.prototype.process_params = function process_params(layer, called, req, re * @public */ -Router.prototype.use = function use(handler) { - var offset = 0 - var path = '/' +Router.prototype.use = function use (handler) { + let offset = 0 + let path = '/' // default path to '/' // disambiguate router.use([handler]) if (typeof handler !== 'function') { - var arg = handler + let arg = handler while (Array.isArray(arg) && arg.length !== 0) { arg = arg[0] @@ -464,14 +379,14 @@ Router.prototype.use = function use(handler) { } } - var callbacks = flatten(slice.call(arguments, offset)) + const callbacks = flatten.call(slice.call(arguments, offset), Infinity) if (callbacks.length === 0) { throw new TypeError('argument handler is required') } - for (var i = 0; i < callbacks.length; i++) { - var fn = callbacks[i] + for (let i = 0; i < callbacks.length; i++) { + const fn = callbacks[i] if (typeof fn !== 'function') { throw new TypeError('argument handler must be a function') @@ -480,7 +395,7 @@ Router.prototype.use = function use(handler) { // add the middleware debug('use %o %s', path, fn.name || '') - var layer = new Layer(path, { + const layer = new Layer(path, { sensitive: this.caseSensitive, strict: false, end: false @@ -507,16 +422,16 @@ Router.prototype.use = function use(handler) { * @public */ -Router.prototype.route = function route(path) { - var route = new Route(path) +Router.prototype.route = function route (path) { + const route = new Route(path) - var layer = new Layer(path, { + const layer = new Layer(path, { sensitive: this.caseSensitive, strict: this.strict, end: true }, handle) - function handle(req, res, next) { + function handle (req, res, next) { route.dispatch(req, res, next) } @@ -527,9 +442,9 @@ Router.prototype.route = function route(path) { } // create Router#VERB functions -methods.concat('all').forEach(function(method){ +methods.concat('all').forEach(function (method) { Router.prototype[method] = function (path) { - var route = this.route(path) + const route = this.route(path) route[method].apply(route, slice.call(arguments, 1)) return this } @@ -543,8 +458,8 @@ methods.concat('all').forEach(function(method){ * @private */ -function generateOptionsResponder(res, methods) { - return function onDone(fn, err) { +function generateOptionsResponder (res, methods) { + return function onDone (fn, err) { if (err || methods.length === 0) { return fn(err) } @@ -560,7 +475,7 @@ function generateOptionsResponder(res, methods) { * @private */ -function getPathname(req) { +function getPathname (req) { try { return parseUrl(req).pathname } catch (err) { @@ -575,19 +490,19 @@ function getPathname(req) { * @private */ -function getProtohost(url) { +function getProtohost (url) { if (typeof url !== 'string' || url.length === 0 || url[0] === '/') { return undefined } - var searchIndex = url.indexOf('?') - var pathLength = searchIndex !== -1 + const searchIndex = url.indexOf('?') + const pathLength = searchIndex !== -1 ? searchIndex : url.length - var fqdnIndex = url.substr(0, pathLength).indexOf('://') + const fqdnIndex = url.substring(0, pathLength).indexOf('://') return fqdnIndex !== -1 - ? url.substr(0, url.indexOf('/', 3 + fqdnIndex)) + ? url.substring(0, url.indexOf('/', 3 + fqdnIndex)) : undefined } @@ -599,7 +514,7 @@ function getProtohost(url) { * @private */ -function matchLayer(layer, path) { +function matchLayer (layer, path) { try { return layer.match(path) } catch (err) { @@ -613,21 +528,21 @@ function matchLayer(layer, path) { * @private */ -function mergeParams(params, parent) { +function mergeParams (params, parent) { if (typeof parent !== 'object' || !parent) { return params } // make copy of parent for base - var obj = mixin({}, parent) + const obj = Object.assign({}, parent) // simple non-numeric merging if (!(0 in params) || !(0 in parent)) { - return mixin(obj, params) + return Object.assign(obj, params) } - var i = 0 - var o = 0 + let i = 0 + let o = 0 // determine numeric gap in params while (i in params) { @@ -649,7 +564,104 @@ function mergeParams(params, parent) { } } - return mixin(obj, params) + return Object.assign(obj, params) +} + +/** + * Process any parameters for the layer. + * + * @private + */ + +function processParams (params, layer, called, req, res, done) { + // captured parameters from the layer, keys and values + const keys = layer.keys + + // fast track + if (!keys || keys.length === 0) { + return done() + } + + let i = 0 + let paramIndex = 0 + let key + let paramVal + let paramCallbacks + let paramCalled + + // process params in order + // param callbacks can be async + function param (err) { + if (err) { + return done(err) + } + + if (i >= keys.length) { + return done() + } + + paramIndex = 0 + key = keys[i++] + paramVal = req.params[key] + paramCallbacks = params[key] + paramCalled = called[key] + + if (paramVal === undefined || !paramCallbacks) { + return param() + } + + // param previously called with same value or error occurred + if (paramCalled && (paramCalled.match === paramVal || + (paramCalled.error && paramCalled.error !== 'route'))) { + // restore value + req.params[key] = paramCalled.value + + // next param + return param(paramCalled.error) + } + + called[key] = paramCalled = { + error: null, + match: paramVal, + value: paramVal + } + + paramCallback() + } + + // single param callbacks + function paramCallback (err) { + const fn = paramCallbacks[paramIndex++] + + // store updated value + paramCalled.value = req.params[key] + + if (err) { + // store error + paramCalled.error = err + param(err) + return + } + + if (!fn) return param() + + try { + const ret = fn(req, res, paramCallback, paramVal, key) + if (isPromise(ret)) { + if (!(ret instanceof Promise)) { + deprecate('parameters that are Promise-like are deprecated, use a native Promise instead') + } + + ret.then(null, function (error) { + paramCallback(error || new Error('Rejected promise')) + }) + } + } catch (e) { + paramCallback(e) + } + } + + param() } /** @@ -658,18 +670,18 @@ function mergeParams(params, parent) { * @private */ -function restore(fn, obj) { - var props = new Array(arguments.length - 2) - var vals = new Array(arguments.length - 2) +function restore (fn, obj) { + const props = new Array(arguments.length - 2) + const vals = new Array(arguments.length - 2) - for (var i = 0; i < props.length; i++) { + for (let i = 0; i < props.length; i++) { props[i] = arguments[i + 2] vals[i] = obj[props[i]] } - return function(){ + return function () { // restore vals - for (var i = 0; i < props.length; i++) { + for (let i = 0; i < props.length; i++) { obj[props[i]] = vals[i] } @@ -683,16 +695,16 @@ function restore(fn, obj) { * @private */ -function sendOptionsResponse(res, methods) { - var options = Object.create(null) +function sendOptionsResponse (res, methods) { + const options = Object.create(null) // build unique method map - for (var i = 0; i < methods.length; i++) { + for (let i = 0; i < methods.length; i++) { options[methods[i]] = true } // construct the allow list - var allow = Object.keys(options).sort().join(', ') + const allow = Object.keys(options).sort().join(', ') // send response res.setHeader('Allow', allow) @@ -708,7 +720,7 @@ function sendOptionsResponse(res, methods) { * @private */ -function trySendOptionsResponse(res, methods, next) { +function trySendOptionsResponse (res, methods, next) { try { sendOptionsResponse(res, methods) } catch (err) { @@ -722,12 +734,12 @@ function trySendOptionsResponse(res, methods, next) { * @private */ -function wrap(old, fn) { - return function proxy() { - var args = new Array(arguments.length + 1) +function wrap (old, fn) { + return function proxy () { + const args = new Array(arguments.length + 1) args[0] = old - for (var i = 0, len = arguments.length; i < len; i++) { + for (let i = 0, len = arguments.length; i < len; i++) { args[i + 1] = arguments[i] } diff --git a/lib/layer.js b/lib/layer.js index 60a737f5..6a4408ff 100644 --- a/lib/layer.js +++ b/lib/layer.js @@ -1,7 +1,7 @@ /*! * router * Copyright(c) 2013 Roman Shtylman - * Copyright(c) 2014 Douglas Christopher Wilson + * Copyright(c) 2014-2022 Douglas Christopher Wilson * MIT Licensed */ @@ -12,15 +12,18 @@ * @private */ -var pathRegexp = require('path-to-regexp') -var debug = require('debug')('router:layer') +const isPromise = require('is-promise') +const pathRegexp = require('path-to-regexp') +const debug = require('debug')('router:layer') +const deprecate = require('depd')('router') /** * Module variables. * @private */ -var hasOwnProperty = Object.prototype.hasOwnProperty +const TRAILING_SLASH_REGEXP = /\/+$/ +const MATCHING_GROUP_REGEXP = /\((?:\?<(.*?)>)?(?!\?)/g /** * Expose `Layer`. @@ -28,23 +31,66 @@ var hasOwnProperty = Object.prototype.hasOwnProperty module.exports = Layer -function Layer(path, options, fn) { +function Layer (path, options, fn) { if (!(this instanceof Layer)) { return new Layer(path, options, fn) } debug('new %o', path) - var opts = options || {} + const opts = options || {} this.handle = fn + this.keys = [] this.name = fn.name || '' this.params = undefined this.path = undefined - this.regexp = pathRegexp(path, this.keys = [], opts) + this.slash = path === '/' && opts.end === false + + function matcher (_path) { + if (_path instanceof RegExp) { + const keys = [] + let name = 0 + let m + // eslint-disable-next-line no-cond-assign + while (m = MATCHING_GROUP_REGEXP.exec(_path.source)) { + keys.push({ + name: m[1] || name++, + offset: m.index + }) + } + + return function regexpMatcher (p) { + const match = _path.exec(p) + if (!match) { + return false + } + + const params = {} + for (let i = 1; i < match.length; i++) { + const key = keys[i - 1] + const prop = key.name + const val = decodeParam(match[i]) + + if (val !== undefined) { + params[prop] = val + } + } + + return { + params, + path: match[0] + } + } + } - // set fast path flags - this.regexp.fast_star = path === '*' - this.regexp.fast_slash = path === '/' && opts.end === false + return pathRegexp.match((opts.strict ? _path : loosen(_path)), { + sensitive: opts.sensitive, + end: opts.end, + trailing: !opts.strict, + decode: decodeParam + }) + } + this.matchers = Array.isArray(path) ? path.map(matcher) : [matcher(path)] } /** @@ -57,8 +103,8 @@ function Layer(path, options, fn) { * @api private */ -Layer.prototype.handle_error = function handle_error(error, req, res, next) { - var fn = this.handle +Layer.prototype.handleError = function handleError (error, req, res, next) { + const fn = this.handle if (fn.length !== 4) { // not a standard error handler @@ -66,7 +112,19 @@ Layer.prototype.handle_error = function handle_error(error, req, res, next) { } try { - fn(error, req, res, next) + // invoke function + const ret = fn(error, req, res, next) + + // wait for returned promise + if (isPromise(ret)) { + if (!(ret instanceof Promise)) { + deprecate('handlers that are Promise-like are deprecated, use a native Promise instead') + } + + ret.then(null, function (error) { + next(error || new Error('Rejected promise')) + }) + } } catch (err) { next(err) } @@ -81,8 +139,8 @@ Layer.prototype.handle_error = function handle_error(error, req, res, next) { * @api private */ -Layer.prototype.handle_request = function handle(req, res, next) { - var fn = this.handle +Layer.prototype.handleRequest = function handleRequest (req, res, next) { + const fn = this.handle if (fn.length > 3) { // not a standard request handler @@ -90,7 +148,19 @@ Layer.prototype.handle_request = function handle(req, res, next) { } try { - fn(req, res, next) + // invoke function + const ret = fn(req, res, next) + + // wait for returned promise + if (isPromise(ret)) { + if (!(ret instanceof Promise)) { + deprecate('handlers that are Promise-like are deprecated, use a native Promise instead') + } + + ret.then(null, function (error) { + next(error || new Error('Rejected promise')) + }) + } } catch (err) { next(err) } @@ -105,26 +175,23 @@ Layer.prototype.handle_request = function handle(req, res, next) { * @api private */ -Layer.prototype.match = function match(path) { - var match +Layer.prototype.match = function match (path) { + let match if (path != null) { // fast path non-ending match for / (any path matches) - if (this.regexp.fast_slash) { + if (this.slash) { this.params = {} this.path = '' return true } - // fast path for * (everything matched in a param) - if (this.regexp.fast_star) { - this.params = {'0': decode_param(path)} - this.path = path - return true + let i = 0 + while (!match && i < this.matchers.length) { + // match the path + match = this.matchers[i](path) + i++ } - - // match the path - match = this.regexp.exec(path) } if (!match) { @@ -134,22 +201,9 @@ Layer.prototype.match = function match(path) { } // store values - this.params = {} - this.path = match[0] - - // iterate matches - var keys = this.keys - var params = this.params - - for (var i = 1; i < match.length; i++) { - var key = keys[i - 1] - var prop = key.name - var val = decode_param(match[i]) - - if (val !== undefined || !(hasOwnProperty.call(params, prop))) { - params[prop] = val - } - } + this.params = match.params + this.path = match.path + this.keys = Object.keys(match.params) return true } @@ -162,7 +216,7 @@ Layer.prototype.match = function match(path) { * @private */ -function decode_param(val){ +function decodeParam (val) { if (typeof val !== 'string' || val.length === 0) { return val } @@ -178,3 +232,16 @@ function decode_param(val){ throw err } } + +/** + * Loosens the given path for path-to-regexp matching. + */ +function loosen (path) { + if (path instanceof RegExp || path === '/') { + return path + } + + return Array.isArray(path) + ? path.map(function (p) { return loosen(p) }) + : String(path).replace(TRAILING_SLASH_REGEXP, '') +} diff --git a/lib/route.js b/lib/route.js index c501b08e..1887d789 100644 --- a/lib/route.js +++ b/lib/route.js @@ -1,7 +1,7 @@ /*! * router * Copyright(c) 2013 Roman Shtylman - * Copyright(c) 2014 Douglas Christopher Wilson + * Copyright(c) 2014-2022 Douglas Christopher Wilson * MIT Licensed */ @@ -12,17 +12,18 @@ * @private */ -var debug = require('debug')('router:route') -var flatten = require('array-flatten') -var Layer = require('./layer') -var methods = require('methods') +const debug = require('debug')('router:route') +const Layer = require('./layer') +const { METHODS } = require('node:http') /** * Module variables. * @private */ -var slice = Array.prototype.slice +const slice = Array.prototype.slice +const flatten = Array.prototype.flat +const methods = METHODS.map((method) => method.toLowerCase()) /** * Expose `Route`. @@ -37,28 +38,30 @@ module.exports = Route * @api private */ -function Route(path) { +function Route (path) { debug('new %o', path) this.path = path this.stack = [] // route handlers for various http methods - this.methods = {} + this.methods = Object.create(null) } /** * @private */ -Route.prototype._handles_method = function _handles_method(method) { +Route.prototype._handlesMethod = function _handlesMethod (method) { if (this.methods._all) { return true } // normalize name - var name = method.toLowerCase() + let name = typeof method === 'string' + ? method.toLowerCase() + : method - if (name === 'head' && !this.methods['head']) { + if (name === 'head' && !this.methods.head) { name = 'get' } @@ -70,15 +73,15 @@ Route.prototype._handles_method = function _handles_method(method) { * @private */ -Route.prototype._methods = function _methods() { - var methods = Object.keys(this.methods) +Route.prototype._methods = function _methods () { + const methods = Object.keys(this.methods) // append automatic head if (this.methods.get && !this.methods.head) { methods.push('head') } - for (var i = 0; i < methods.length; i++) { + for (let i = 0; i < methods.length; i++) { // make upper case methods[i] = methods[i].toUpperCase() } @@ -92,15 +95,20 @@ Route.prototype._methods = function _methods() { * @private */ -Route.prototype.dispatch = function dispatch(req, res, done) { - var idx = 0 - var stack = this.stack +Route.prototype.dispatch = function dispatch (req, res, done) { + let idx = 0 + const stack = this.stack + let sync = 0 + if (stack.length === 0) { return done() } - var method = req.method.toLowerCase() - if (method === 'head' && !this.methods['head']) { + let method = typeof req.method === 'string' + ? req.method.toLowerCase() + : req.method + + if (method === 'head' && !this.methods.head) { method = 'get' } @@ -108,7 +116,7 @@ Route.prototype.dispatch = function dispatch(req, res, done) { next() - function next(err) { + function next (err) { // signal to exit route if (err && err === 'route') { return done() @@ -124,8 +132,13 @@ Route.prototype.dispatch = function dispatch(req, res, done) { return done(err) } - var layer - var match + // max sync stack + if (++sync > 100) { + return setImmediate(next, err) + } + + let layer + let match // find next matching layer while (match !== true && idx < stack.length) { @@ -139,10 +152,12 @@ Route.prototype.dispatch = function dispatch(req, res, done) { } if (err) { - layer.handle_error(err, req, res, next) + layer.handleError(err, req, res, next) } else { - layer.handle_request(req, res, next) + layer.handleRequest(req, res, next) } + + sync = 0 } } @@ -174,21 +189,21 @@ Route.prototype.dispatch = function dispatch(req, res, done) { * @api public */ -Route.prototype.all = function all(handler) { - var callbacks = flatten(slice.call(arguments)) +Route.prototype.all = function all (handler) { + const callbacks = flatten.call(slice.call(arguments), Infinity) if (callbacks.length === 0) { throw new TypeError('argument handler is required') } - for (var i = 0; i < callbacks.length; i++) { - var fn = callbacks[i] + for (let i = 0; i < callbacks.length; i++) { + const fn = callbacks[i] if (typeof fn !== 'function') { throw new TypeError('argument handler must be a function') } - var layer = Layer('/', {}, fn) + const layer = Layer('/', {}, fn) layer.method = undefined this.methods._all = true @@ -200,14 +215,14 @@ Route.prototype.all = function all(handler) { methods.forEach(function (method) { Route.prototype[method] = function (handler) { - var callbacks = flatten(slice.call(arguments)) + const callbacks = flatten.call(slice.call(arguments), Infinity) if (callbacks.length === 0) { throw new TypeError('argument handler is required') } - for (var i = 0; i < callbacks.length; i++) { - var fn = callbacks[i] + for (let i = 0; i < callbacks.length; i++) { + const fn = callbacks[i] if (typeof fn !== 'function') { throw new TypeError('argument handler must be a function') @@ -215,7 +230,7 @@ methods.forEach(function (method) { debug('%s %s', method, this.path) - var layer = Layer('/', {}, fn) + const layer = Layer('/', {}, fn) layer.method = method this.methods[method] = true diff --git a/package.json b/package.json index 8b25b421..e6be5570 100644 --- a/package.json +++ b/package.json @@ -1,30 +1,31 @@ { "name": "router", "description": "Simple middleware-style router", - "version": "1.3.3", + "version": "2.2.0", "author": "Douglas Christopher Wilson ", "contributors": [ "Blake Embrey " ], "license": "MIT", "repository": "pillarjs/router", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + }, "dependencies": { - "array-flatten": "2.1.2", - "debug": "2.6.9", - "methods": "~1.1.2", - "parseurl": "~1.3.2", - "path-to-regexp": "0.1.7", - "setprototypeof": "1.1.1", - "utils-merge": "1.0.1" + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" }, "devDependencies": { - "after": "0.8.2", - "eslint": "3.19.0", - "eslint-plugin-markdown": "1.0.0-beta.6", - "finalhandler": "1.1.2", - "istanbul": "0.4.5", - "mocha": "2.5.3", - "supertest": "1.1.0" + "finalhandler": "^2.1.0", + "mocha": "10.2.0", + "nyc": "15.1.0", + "run-series": "^1.1.9", + "standard": "^17.1.0", + "supertest": "6.3.3" }, "files": [ "lib/", @@ -34,12 +35,14 @@ "index.js" ], "engines": { - "node": ">= 0.8" + "node": ">= 18" }, "scripts": { - "lint": "eslint --plugin markdown --ext js,md .", - "test": "mocha --reporter spec --bail --check-leaks test/", - "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --reporter dot --check-leaks test/", - "test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --reporter spec --check-leaks test/" + "lint": "standard", + "test": "mocha --reporter spec --check-leaks test/", + "test:debug": "mocha --reporter spec --check-leaks test/ --inspect --inspect-brk", + "test-ci": "nyc --reporter=lcov --reporter=text npm test", + "test-cov": "nyc --reporter=text npm test", + "version": "node scripts/version-history.js && git add HISTORY.md" } } diff --git a/scripts/version-history.js b/scripts/version-history.js new file mode 100644 index 00000000..b62d4e16 --- /dev/null +++ b/scripts/version-history.js @@ -0,0 +1,63 @@ +'use strict' + +const fs = require('fs') +const path = require('path') + +const HISTORY_FILE_PATH = path.join(__dirname, '..', 'HISTORY.md') +const MD_HEADER_REGEXP = /^====*$/ +const VERSION = process.env.npm_package_version +const VERSION_PLACEHOLDER_REGEXP = /^(?:unreleased|(\d+\.)+x)$/ + +const historyFileLines = fs.readFileSync(HISTORY_FILE_PATH, 'utf-8').split('\n') + +if (!MD_HEADER_REGEXP.test(historyFileLines[1])) { + console.error('Missing header in HISTORY.md') + process.exit(1) +} + +if (!VERSION_PLACEHOLDER_REGEXP.test(historyFileLines[0])) { + console.error('Missing placeholder version in HISTORY.md') + process.exit(1) +} + +if (historyFileLines[0].indexOf('x') !== -1) { + const versionCheckRegExp = new RegExp('^' + historyFileLines[0].replace('x', '.+') + '$') + + if (!versionCheckRegExp.test(VERSION)) { + console.error('Version %s does not match placeholder %s', VERSION, historyFileLines[0]) + process.exit(1) + } +} + +historyFileLines[0] = VERSION + ' / ' + getLocaleDate() +historyFileLines[1] = repeat('=', historyFileLines[0].length) + +fs.writeFileSync(HISTORY_FILE_PATH, historyFileLines.join('\n')) + +function getLocaleDate () { + const now = new Date() + + return zeroPad(now.getFullYear(), 4) + '-' + + zeroPad(now.getMonth() + 1, 2) + '-' + + zeroPad(now.getDate(), 2) +} + +function repeat (str, length) { + let out = '' + + for (let i = 0; i < length; i++) { + out += str + } + + return out +} + +function zeroPad (number, length) { + let num = number.toString() + + while (num.length < length) { + num = '0' + num + } + + return num +} diff --git a/test/auto-head.js b/test/auto-head.js index f64a0fa3..e8fca8a6 100644 --- a/test/auto-head.js +++ b/test/auto-head.js @@ -1,49 +1,49 @@ +const { it, describe } = require('mocha') +const Router = require('..') +const utils = require('./support/utils') -var Router = require('..') -var utils = require('./support/utils') - -var createServer = utils.createServer -var request = utils.request +const createServer = utils.createServer +const request = utils.request describe('HEAD', function () { it('should invoke get without head', function (done) { - var router = Router() - var server = createServer(router) + const router = Router() + const server = createServer(router) router.get('/users', sethit(1), saw) request(server) - .head('/users') - .expect('Content-Type', 'text/plain') - .expect('x-fn-1', 'hit') - .expect(200, done) + .head('/users') + .expect('Content-Type', 'text/plain') + .expect('x-fn-1', 'hit') + .expect(200, done) }) it('should invoke head if prior to get', function (done) { - var router = Router() - var server = createServer(router) + const router = Router() + const server = createServer(router) router.head('/users', sethit(1), saw) router.get('/users', sethit(2), saw) request(server) - .head('/users') - .expect('Content-Type', 'text/plain') - .expect('x-fn-1', 'hit') - .expect(200, done) + .head('/users') + .expect('Content-Type', 'text/plain') + .expect('x-fn-1', 'hit') + .expect(200, done) }) }) -function saw(req, res) { - var msg = 'saw ' + req.method + ' ' + req.url +function saw (req, res) { + const msg = 'saw ' + req.method + ' ' + req.url res.statusCode = 200 res.setHeader('Content-Type', 'text/plain') res.end(msg) } -function sethit(num) { - var name = 'x-fn-' + String(num) - return function hit(req, res, next) { +function sethit (num) { + const name = 'x-fn-' + String(num) + return function hit (req, res, next) { res.setHeader(name, 'hit') next() } diff --git a/test/auto-options.js b/test/auto-options.js index cd7a0b40..b4bcfac0 100644 --- a/test/auto-options.js +++ b/test/auto-options.js @@ -1,14 +1,14 @@ +const { it, describe } = require('mocha') +const Router = require('..') +const utils = require('./support/utils') -var Router = require('..') -var utils = require('./support/utils') - -var createServer = utils.createServer -var request = utils.request +const createServer = utils.createServer +const request = utils.request describe('OPTIONS', function () { it('should respond with defined routes', function (done) { - var router = Router() - var server = createServer(router) + const router = Router() + const server = createServer(router) router.delete('/', saw) router.get('/users', saw) @@ -16,14 +16,14 @@ describe('OPTIONS', function () { router.put('/users', saw) request(server) - .options('/users') - .expect('Allow', 'GET, HEAD, POST, PUT') - .expect(200, 'GET, HEAD, POST, PUT', done) + .options('/users') + .expect('Allow', 'GET, HEAD, POST, PUT') + .expect(200, 'GET, HEAD, POST, PUT', done) }) it('should not contain methods multiple times', function (done) { - var router = Router() - var server = createServer(router) + const router = Router() + const server = createServer(router) router.delete('/', saw) router.get('/users', saw) @@ -31,14 +31,14 @@ describe('OPTIONS', function () { router.get('/users', saw) request(server) - .options('/users') - .expect('GET, HEAD, PUT') - .expect('Allow', 'GET, HEAD, PUT', done) + .options('/users') + .expect('GET, HEAD, PUT') + .expect('Allow', 'GET, HEAD, PUT', done) }) it('should not include "all" routes', function (done) { - var router = Router() - var server = createServer(router) + const router = Router() + const server = createServer(router) router.get('/', saw) router.get('/users', saw) @@ -46,39 +46,39 @@ describe('OPTIONS', function () { router.all('/users', sethit(1)) request(server) - .options('/users') - .expect('x-fn-1', 'hit') - .expect('Allow', 'GET, HEAD, PUT') - .expect(200, 'GET, HEAD, PUT', done) + .options('/users') + .expect('x-fn-1', 'hit') + .expect('Allow', 'GET, HEAD, PUT') + .expect(200, 'GET, HEAD, PUT', done) }) it('should not respond if no matching path', function (done) { - var router = Router() - var server = createServer(router) + const router = Router() + const server = createServer(router) router.get('/users', saw) request(server) - .options('/') - .expect(404, done) + .options('/') + .expect(404, done) }) it('should do nothing with explicit options route', function (done) { - var router = Router() - var server = createServer(router) + const router = Router() + const server = createServer(router) router.get('/users', saw) router.options('/users', saw) request(server) - .options('/users') - .expect(200, 'saw OPTIONS /users', done) + .options('/users') + .expect(200, 'saw OPTIONS /users', done) }) describe('when error occurs in respone handler', function () { it('should pass error to callback', function (done) { - var router = Router() - var server = createServer(function hander(req, res, next) { + const router = Router() + const server = createServer(function hander (req, res, next) { res.writeHead(200) router(req, res, function (err) { res.end(String(Boolean(err))) @@ -88,22 +88,22 @@ describe('OPTIONS', function () { router.get('/users', saw) request(server) - .options('/users') - .expect(200, 'true', done) + .options('/users') + .expect(200, 'true', done) }) }) }) -function saw(req, res) { - var msg = 'saw ' + req.method + ' ' + req.url +function saw (req, res) { + const msg = 'saw ' + req.method + ' ' + req.url res.statusCode = 200 res.setHeader('Content-Type', 'text/plain') res.end(msg) } -function sethit(num) { - var name = 'x-fn-' + String(num) - return function hit(req, res, next) { +function sethit (num) { + const name = 'x-fn-' + String(num) + return function hit (req, res, next) { res.setHeader(name, 'hit') next() } diff --git a/test/fqdn-url.js b/test/fqdn-url.js index 54115009..8db59c3b 100644 --- a/test/fqdn-url.js +++ b/test/fqdn-url.js @@ -1,72 +1,72 @@ +const { it, describe } = require('mocha') +const Router = require('..') +const utils = require('./support/utils') -var Router = require('..') -var utils = require('./support/utils') - -var createServer = utils.createServer -var rawrequest = utils.rawrequest +const createServer = utils.createServer +const rawrequest = utils.rawrequest describe('FQDN url', function () { it('should not obscure FQDNs', function (done) { - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) router.use(saw) rawrequest(server) - .get('http://example.com/foo') - .expect(200, 'saw GET http://example.com/foo', done) + .get('http://example.com/foo') + .expect(200, 'saw GET http://example.com/foo', done) }) it('should strip/restore FQDN req.url', function (done) { - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) router.use('/blog', setsaw(1)) router.use(saw) rawrequest(server) - .get('http://example.com/blog/post/1') - .expect('x-saw-1', 'GET http://example.com/post/1') - .expect(200, 'saw GET http://example.com/blog/post/1', done) + .get('http://example.com/blog/post/1') + .expect('x-saw-1', 'GET http://example.com/post/1') + .expect(200, 'saw GET http://example.com/blog/post/1', done) }) it('should ignore FQDN in search', function (done) { - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) router.use('/proxy', setsaw(1)) router.use(saw) rawrequest(server) - .get('/proxy?url=http://example.com/blog/post/1') - .expect('x-saw-1', 'GET /?url=http://example.com/blog/post/1') - .expect(200, 'saw GET /proxy?url=http://example.com/blog/post/1', done) + .get('/proxy?url=http://example.com/blog/post/1') + .expect('x-saw-1', 'GET /?url=http://example.com/blog/post/1') + .expect(200, 'saw GET /proxy?url=http://example.com/blog/post/1', done) }) it('should ignore FQDN in path', function (done) { - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) router.use('/proxy', setsaw(1)) router.use(saw) rawrequest(server) - .get('/proxy/http://example.com/blog/post/1') - .expect('x-saw-1', 'GET /http://example.com/blog/post/1') - .expect(200, 'saw GET /proxy/http://example.com/blog/post/1', done) + .get('/proxy/http://example.com/blog/post/1') + .expect('x-saw-1', 'GET /http://example.com/blog/post/1') + .expect(200, 'saw GET /proxy/http://example.com/blog/post/1', done) }) }) -function setsaw(num) { - var name = 'x-saw-' + String(num) - return function hit(req, res, next) { +function setsaw (num) { + const name = 'x-saw-' + String(num) + return function hit (req, res, next) { res.setHeader(name, req.method + ' ' + req.url) next() } } -function saw(req, res) { - var msg = 'saw ' + req.method + ' ' + req.url +function saw (req, res) { + const msg = 'saw ' + req.method + ' ' + req.url res.statusCode = 200 res.setHeader('Content-Type', 'text/plain') res.end(msg) diff --git a/test/param.js b/test/param.js index e2a5aad8..fe7019d4 100644 --- a/test/param.js +++ b/test/param.js @@ -1,43 +1,42 @@ - -var after = require('after') -var Router = require('..') -var utils = require('./support/utils') - -var assert = utils.assert -var createHitHandle = utils.createHitHandle -var shouldHitHandle = utils.shouldHitHandle -var shouldNotHitHandle = utils.shouldNotHitHandle -var createServer = utils.createServer -var request = utils.request +const { it, describe } = require('mocha') +const series = require('run-series') +const Router = require('..') +const utils = require('./support/utils') + +const assert = utils.assert +const createHitHandle = utils.createHitHandle +const shouldHitHandle = utils.shouldHitHandle +const shouldNotHitHandle = utils.shouldNotHitHandle +const createServer = utils.createServer +const request = utils.request describe('Router', function () { describe('.param(name, fn)', function () { it('should reject missing name', function () { - var router = new Router() + const router = new Router() assert.throws(router.param.bind(router), /argument name is required/) }) it('should reject bad name', function () { - var router = new Router() + const router = new Router() assert.throws(router.param.bind(router, 42), /argument name must be a string/) }) it('should reject missing fn', function () { - var router = new Router() + const router = new Router() assert.throws(router.param.bind(router, 'id'), /argument fn is required/) }) it('should reject bad fn', function () { - var router = new Router() + const router = new Router() assert.throws(router.param.bind(router, 'id', 42), /argument fn must be a function/) }) it('should map logic for a path param', function (done) { - var cb = after(2, done) - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) - router.param('id', function parseId(req, res, next, val) { + router.param('id', function parseId (req, res, next, val) { req.params.id = Number(val) next() }) @@ -47,25 +46,30 @@ describe('Router', function () { res.end('get user ' + req.params.id) }) - request(server) - .get('/user/2') - .expect(200, 'get user 2', cb) - - request(server) - .get('/user/bob') - .expect(200, 'get user NaN', cb) + series([ + function (cb) { + request(server) + .get('/user/2') + .expect(200, 'get user 2', cb) + }, + function (cb) { + request(server) + .get('/user/bob') + .expect(200, 'get user NaN', cb) + } + ], done) }) it('should allow chaining', function (done) { - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) - router.param('id', function parseId(req, res, next, val) { + router.param('id', function parseId (req, res, next, val) { req.params.id = Number(val) next() }) - router.param('id', function parseId(req, res, next, val) { + router.param('id', function parseId (req, res, next, val) { req.itemId = Number(val) next() }) @@ -76,15 +80,15 @@ describe('Router', function () { }) request(server) - .get('/user/2') - .expect(200, 'get user 2 (2)', done) + .get('/user/2') + .expect(200, 'get user 2 (2)', done) }) it('should automatically decode path value', function (done) { - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) - router.param('user', function parseUser(req, res, next, user) { + router.param('user', function parseUser (req, res, next, user) { req.user = user next() }) @@ -95,15 +99,15 @@ describe('Router', function () { }) request(server) - .get('/user/%22bob%2Frobert%22') - .expect('get user "bob/robert"', done) + .get('/user/%22bob%2Frobert%22') + .expect('get user "bob/robert"', done) }) it('should 400 on invalid path value', function (done) { - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) - router.param('user', function parseUser(req, res, next, user) { + router.param('user', function parseUser (req, res, next, user) { req.user = user next() }) @@ -114,42 +118,46 @@ describe('Router', function () { }) request(server) - .get('/user/%bob') - .expect(400, /URIError: Failed to decode param/, done) + .get('/user/%bob') + .expect(400, /URIError: Failed to decode param/, done) }) it('should only invoke fn when necessary', function (done) { - var cb = after(2, done) - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) - router.param('id', function parseId(req, res, next, val) { + router.param('id', function parseId (req, res, next, val) { res.setHeader('x-id', val) next() }) - router.param('user', function parseUser(req, res, next, user) { + router.param('user', function parseUser (req, res, next, user) { throw new Error('boom') }) router.get('/user/:user', saw) router.put('/user/:id', saw) - request(server) - .get('/user/bob') - .expect(500, /Error: boom/, cb) - - request(server) - .put('/user/bob') - .expect('x-id', 'bob') - .expect(200, 'saw PUT /user/bob', cb) + series([ + function (cb) { + request(server) + .get('/user/bob') + .expect(500, /Error: boom/, cb) + }, + function (cb) { + request(server) + .put('/user/bob') + .expect('x-id', 'bob') + .expect(200, 'saw PUT /user/bob', cb) + } + ], done) }) it('should only invoke fn once per request', function (done) { - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) - router.param('user', function parseUser(req, res, next, user) { + router.param('user', function parseUser (req, res, next, user) { req.count = (req.count || 0) + 1 req.user = user next() @@ -163,15 +171,15 @@ describe('Router', function () { }) request(server) - .get('/user/bob') - .expect('get user bob 1 times', done) + .get('/user/bob') + .expect('get user bob 1 times', done) }) it('should keep changes to req.params value', function (done) { - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) - router.param('id', function parseUser(req, res, next, val) { + router.param('id', function parseUser (req, res, next, val) { req.count = (req.count || 0) + 1 req.params.id = Number(val) next() @@ -187,15 +195,15 @@ describe('Router', function () { }) request(server) - .get('/user/01') - .expect('get user 1 1 times', done) + .get('/user/01') + .expect('get user 1 1 times', done) }) it('should invoke fn if path value differs', function (done) { - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) - router.param('user', function parseUser(req, res, next, user) { + router.param('user', function parseUser (req, res, next, user) { req.count = (req.count || 0) + 1 req.user = user req.vals = (req.vals || []).concat(user) @@ -210,15 +218,15 @@ describe('Router', function () { }) request(server) - .get('/user/bob') - .expect('get user bob 2 times: user, bob', done) + .get('/user/bob') + .expect('get user bob 2 times: user, bob', done) }) it('should catch exception in fn', function (done) { - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) - router.param('user', function parseUser(req, res, next, user) { + router.param('user', function parseUser (req, res, next, user) { throw new Error('boom') }) @@ -228,19 +236,19 @@ describe('Router', function () { }) request(server) - .get('/user/bob') - .expect(500, /Error: boom/, done) + .get('/user/bob') + .expect(500, /Error: boom/, done) }) it('should catch exception in chained fn', function (done) { - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) - router.param('user', function parseUser(req, res, next, user) { + router.param('user', function parseUser (req, res, next, user) { process.nextTick(next) }) - router.param('user', function parseUser(req, res, next, user) { + router.param('user', function parseUser (req, res, next, user) { throw new Error('boom') }) @@ -250,18 +258,59 @@ describe('Router', function () { }) request(server) - .get('/user/bob') - .expect(500, /Error: boom/, done) + .get('/user/bob') + .expect(500, /Error: boom/, done) + }) + + describe('promise support', function () { + it('should pass rejected promise value', function (done) { + const router = new Router() + const server = createServer(router) + + router.param('user', function parseUser (req, res, next, user) { + return Promise.reject(new Error('boom')) + }) + + router.get('/user/:user', function (req, res) { + res.setHeader('Content-Type', 'text/plain') + res.end('get user ' + req.params.id) + }) + + request(server) + .get('/user/bob') + .expect(500, /Error: boom/, done) + }) + + it('should pass rejected promise without value', function (done) { + const router = new Router() + const server = createServer(router) + + router.use(function createError (req, res, next) { + return Promise.reject() // eslint-disable-line prefer-promise-reject-errors + }) + + router.param('user', function parseUser (req, res, next, user) { + return Promise.reject() // eslint-disable-line prefer-promise-reject-errors + }) + + router.get('/user/:user', function (req, res) { + res.setHeader('Content-Type', 'text/plain') + res.end('get user ' + req.params.id) + }) + + request(server) + .get('/user/bob') + .expect(500, /Error: Rejected promise/, done) + }) }) describe('next("route")', function () { it('should cause route with param to be skipped', function (done) { - var cb = after(3, done) - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) - router.param('id', function parseId(req, res, next, val) { - var id = Number(val) + router.param('id', function parseId (req, res, next, val) { + const id = Number(val) if (isNaN(id)) { return next('route') @@ -282,24 +331,30 @@ describe('Router', function () { res.end('cannot get a new user') }) - request(server) - .get('/user/2') - .expect(200, 'get user 2', cb) - - request(server) - .get('/user/bob') - .expect(404, cb) - - request(server) - .get('/user/new') - .expect(400, 'cannot get a new user', cb) + series([ + function (cb) { + request(server) + .get('/user/2') + .expect(200, 'get user 2', cb) + }, + function (cb) { + request(server) + .get('/user/bob') + .expect(404, cb) + }, + function (cb) { + request(server) + .get('/user/new') + .expect(400, 'cannot get a new user', cb) + } + ], done) }) it('should invoke fn if path value differs', function (done) { - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) - router.param('user', function parseUser(req, res, next, user) { + router.param('user', function parseUser (req, res, next, user) { req.count = (req.count || 0) + 1 req.user = user req.vals = (req.vals || []).concat(user) @@ -314,25 +369,25 @@ describe('Router', function () { }) request(server) - .get('/user/bob') - .expect(shouldNotHitHandle(1)) - .expect(shouldHitHandle(2)) - .expect('get user bob 2 times: user, bob', done) + .get('/user/bob') + .expect(shouldNotHitHandle(1)) + .expect(shouldHitHandle(2)) + .expect('get user bob 2 times: user, bob', done) }) }) }) }) -function sethit(num) { - var name = 'x-fn-' + String(num) - return function hit(req, res, next) { +function sethit (num) { + const name = 'x-fn-' + String(num) + return function hit (req, res, next) { res.setHeader(name, 'hit') next() } } -function saw(req, res) { - var msg = 'saw ' + req.method + ' ' + req.url +function saw (req, res) { + const msg = 'saw ' + req.method + ' ' + req.url res.statusCode = 200 res.setHeader('Content-Type', 'text/plain') res.end(msg) diff --git a/test/req.params.js b/test/req.params.js index c7dbba22..8eb4f63b 100644 --- a/test/req.params.js +++ b/test/req.params.js @@ -1,25 +1,25 @@ +const { it, describe } = require('mocha') +const Router = require('..') +const utils = require('./support/utils') -var Router = require('..') -var utils = require('./support/utils') - -var createServer = utils.createServer -var request = utils.request +const createServer = utils.createServer +const request = utils.request describe('req.params', function () { it('should default to empty object', function (done) { - var router = Router() - var server = createServer(router) + const router = Router() + const server = createServer(router) router.get('/', sawParams) request(server) - .get('/') - .expect(200, '{}', done) + .get('/') + .expect(200, '{}', done) }) it('should not exist outside the router', function (done) { - var router = Router() - var server = createServer(function (req, res, next) { + const router = Router() + const server = createServer(function (req, res, next) { router(req, res, function (err) { if (err) return next(err) sawParams(req, res) @@ -29,29 +29,29 @@ describe('req.params', function () { router.get('/', hitParams(1)) request(server) - .get('/') - .expect('x-params-1', '{}') - .expect(200, '', done) + .get('/') + .expect('x-params-1', '{}') + .expect(200, '', done) }) it('should overwrite value outside the router', function (done) { - var router = Router() - var server = createServer(function (req, res, next) { - req.params = {'foo': 'bar'} + const router = Router() + const server = createServer(function (req, res, next) { + req.params = { foo: 'bar' } router(req, res, done) }) router.get('/', sawParams) request(server) - .get('/') - .expect(200, '{}', done) + .get('/') + .expect(200, '{}', done) }) it('should restore previous value outside the router', function (done) { - var router = Router() - var server = createServer(function (req, res, next) { - req.params = {'foo': 'bar'} + const router = Router() + const server = createServer(function (req, res, next) { + req.params = { foo: 'bar' } router(req, res, function (err) { if (err) return next(err) @@ -62,16 +62,16 @@ describe('req.params', function () { router.get('/', hitParams(1)) request(server) - .get('/') - .expect('x-params-1', '{}') - .expect(200, '{"foo":"bar"}', done) + .get('/') + .expect('x-params-1', '{}') + .expect(200, '{"foo":"bar"}', done) }) describe('when "mergeParams: true"', function () { - it('should merge outsite object with params', function (done) { - var router = Router({ mergeParams: true }) - var server = createServer(function (req, res, next) { - req.params = {'foo': 'bar'} + it('should merge outside object with params', function (done) { + const router = Router({ mergeParams: true }) + const server = createServer(function (req, res, next) { + req.params = { foo: 'bar' } router(req, res, function (err) { if (err) return next(err) @@ -82,14 +82,14 @@ describe('req.params', function () { router.get('/:fizz', hitParams(1)) request(server) - .get('/buzz') - .expect('x-params-1', '{"foo":"bar","fizz":"buzz"}') - .expect(200, '{"foo":"bar"}', done) + .get('/buzz') + .expect('x-params-1', '{"foo":"bar","fizz":"buzz"}') + .expect(200, '{"foo":"bar"}', done) }) - it('should ignore non-object outsite object', function (done) { - var router = Router({ mergeParams: true }) - var server = createServer(function (req, res, next) { + it('should ignore non-object outside object', function (done) { + const router = Router({ mergeParams: true }) + const server = createServer(function (req, res, next) { req.params = 42 router(req, res, function (err) { @@ -101,15 +101,15 @@ describe('req.params', function () { router.get('/:fizz', hitParams(1)) request(server) - .get('/buzz') - .expect('x-params-1', '{"fizz":"buzz"}') - .expect(200, '42', done) + .get('/buzz') + .expect('x-params-1', '{"fizz":"buzz"}') + .expect(200, '42', done) }) it('should overwrite outside keys that are the same', function (done) { - var router = Router({ mergeParams: true }) - var server = createServer(function (req, res, next) { - req.params = {'foo': 'bar'} + const router = Router({ mergeParams: true }) + const server = createServer(function (req, res, next) { + req.params = { foo: 'bar' } router(req, res, function (err) { if (err) return next(err) @@ -120,16 +120,16 @@ describe('req.params', function () { router.get('/:foo', hitParams(1)) request(server) - .get('/buzz') - .expect('x-params-1', '{"foo":"buzz"}') - .expect(200, '{"foo":"bar"}', done) + .get('/buzz') + .expect('x-params-1', '{"foo":"buzz"}') + .expect(200, '{"foo":"bar"}', done) }) describe('with numeric properties in req.params', function () { - it('should merge numeric properies by offsetting', function (done) { - var router = Router({ mergeParams: true }) - var server = createServer(function (req, res, next) { - req.params = {'0': 'foo', '1': 'bar'} + it('should merge numeric properties by offsetting', function (done) { + const router = Router({ mergeParams: true }) + const server = createServer(function (req, res, next) { + req.params = { 0: 'foo', 1: 'bar' } router(req, res, function (err) { if (err) return next(err) @@ -137,18 +137,18 @@ describe('req.params', function () { }) }) - router.get('/*', hitParams(1)) + router.get(/\/([^/]*)/, hitParams(1)) request(server) - .get('/buzz') - .expect('x-params-1', '{"0":"foo","1":"bar","2":"buzz"}') - .expect(200, '{"0":"foo","1":"bar"}', done) + .get('/buzz') + .expect('x-params-1', '{"0":"foo","1":"bar","2":"buzz"}') + .expect(200, '{"0":"foo","1":"bar"}', done) }) it('should merge with same numeric properties', function (done) { - var router = Router({ mergeParams: true }) - var server = createServer(function (req, res, next) { - req.params = {'0': 'foo'} + const router = Router({ mergeParams: true }) + const server = createServer(function (req, res, next) { + req.params = { 0: 'foo' } router(req, res, function (err) { if (err) return next(err) @@ -156,26 +156,26 @@ describe('req.params', function () { }) }) - router.get('/*', hitParams(1)) + router.get(/\/([^/]*)/, hitParams(1)) request(server) - .get('/bar') - .expect('x-params-1', '{"0":"foo","1":"bar"}') - .expect(200, '{"0":"foo"}', done) + .get('/bar') + .expect('x-params-1', '{"0":"foo","1":"bar"}') + .expect(200, '{"0":"foo"}', done) }) }) }) }) -function hitParams(num) { - var name = 'x-params-' + String(num) - return function hit(req, res, next) { +function hitParams (num) { + const name = 'x-params-' + String(num) + return function hit (req, res, next) { res.setHeader(name, JSON.stringify(req.params)) next() } } -function sawParams(req, res) { +function sawParams (req, res) { res.statusCode = 200 res.setHeader('Content-Type', 'application/json') res.end(JSON.stringify(req.params)) diff --git a/test/route.js b/test/route.js index 47c8ff5c..dd51298b 100644 --- a/test/route.js +++ b/test/route.js @@ -1,51 +1,81 @@ - -var after = require('after') -var methods = require('methods') -var Router = require('..') -var utils = require('./support/utils') - -var assert = utils.assert -var createHitHandle = utils.createHitHandle -var createServer = utils.createServer -var request = utils.request -var shouldHitHandle = utils.shouldHitHandle -var shouldNotHitHandle = utils.shouldNotHitHandle +const { it, describe } = require('mocha') +const series = require('run-series') +const Router = require('..') +const utils = require('./support/utils') + +const assert = utils.assert +const createHitHandle = utils.createHitHandle +const createServer = utils.createServer +const request = utils.request +const shouldHaveBody = utils.shouldHaveBody +const shouldHitHandle = utils.shouldHitHandle +const shouldNotHaveBody = utils.shouldNotHaveBody +const shouldNotHitHandle = utils.shouldNotHitHandle +const methods = utils.methods describe('Router', function () { describe('.route(path)', function () { it('should return a new route', function () { - var router = new Router() - var route = router.route('/foo') + const router = new Router() + const route = router.route('/foo') assert.equal(route.path, '/foo') }) it('should respond to multiple methods', function (done) { - var cb = after(3, done) - var router = new Router() - var route = router.route('/foo') - var server = createServer(router) + const router = new Router() + const route = router.route('/foo') + const server = createServer(router) route.get(saw) route.post(saw) - request(server) - .get('/foo') - .expect(200, 'saw GET /foo', cb) + series([ + function (cb) { + request(server) + .get('/foo') + .expect(200, 'saw GET /foo', cb) + }, + function (cb) { + request(server) + .post('/foo') + .expect(200, 'saw POST /foo', cb) + }, + function (cb) { + request(server) + .put('/foo') + .expect(404, cb) + } + ], done) + }) - request(server) - .post('/foo') - .expect(200, 'saw POST /foo', cb) + it('should route without method', function (done) { + const router = new Router() + const route = router.route('/foo') + const server = createServer(function (req, res, next) { + req.method = undefined + router(req, res, next) + }) + + route.post(createHitHandle(1)) + route.all(createHitHandle(2)) + route.get(createHitHandle(3)) + + router.get('/foo', createHitHandle(4)) + router.use(saw) request(server) - .put('/foo') - .expect(404, cb) + .get('/foo') + .expect(shouldNotHitHandle(1)) + .expect(shouldHitHandle(2)) + .expect(shouldNotHitHandle(3)) + .expect(shouldNotHitHandle(4)) + .expect(200, 'saw undefined /foo', done) }) it('should stack', function (done) { - var cb = after(3, done) - var router = new Router() - var route = router.route('/foo') - var server = createServer(router) + const router = new Router() + const route = router.route('/foo') + const server = createServer(router) route.post(createHitHandle(1)) route.all(createHitHandle(2)) @@ -53,134 +83,169 @@ describe('Router', function () { router.use(saw) - request(server) - .get('/foo') - .expect('x-fn-2', 'hit') - .expect('x-fn-3', 'hit') - .expect(200, 'saw GET /foo', cb) - - request(server) - .post('/foo') - .expect('x-fn-1', 'hit') - .expect('x-fn-2', 'hit') - .expect(200, 'saw POST /foo', cb) - - request(server) - .put('/foo') - .expect('x-fn-2', 'hit') - .expect(200, 'saw PUT /foo', cb) + series([ + function (cb) { + request(server) + .get('/foo') + .expect('x-fn-2', 'hit') + .expect('x-fn-3', 'hit') + .expect(200, 'saw GET /foo', cb) + }, + function (cb) { + request(server) + .post('/foo') + .expect('x-fn-1', 'hit') + .expect('x-fn-2', 'hit') + .expect(200, 'saw POST /foo', cb) + }, + function (cb) { + request(server) + .put('/foo') + .expect('x-fn-2', 'hit') + .expect(200, 'saw PUT /foo', cb) + } + ], done) }) it('should not error on empty route', function (done) { - var cb = after(2, done) - var router = new Router() - var route = router.route('/foo') - var server = createServer(router) + const router = new Router() + const route = router.route('/foo') + const server = createServer(router) - request(server) - .get('/foo') - .expect(404, cb) + assert.ok(route) - request(server) - .head('/foo') - .expect(404, cb) + series([ + function (cb) { + request(server) + .get('/foo') + .expect(404, cb) + }, + function (cb) { + request(server) + .head('/foo') + .expect(404, cb) + } + ], done) }) it('should not invoke singular error route', function (done) { - var router = new Router() - var route = router.route('/foo') - var server = createServer(router) + const router = new Router() + const route = router.route('/foo') + const server = createServer(router) - route.all(function handleError(err, req, res, next) { - throw new Error('boom!') + route.all(function handleError (err, req, res, next) { + throw err || new Error('boom!') }) request(server) - .get('/foo') - .expect(404, done) + .get('/foo') + .expect(404, done) + }) + + it('should not stack overflow with a large sync stack', function (done) { + this.timeout(5000) // long-running test + + const router = new Router() + const route = router.route('/foo') + const server = createServer(router) + + for (let i = 0; i < 6000; i++) { + route.all(function (req, res, next) { next() }) + } + + route.get(helloWorld) + + request(server) + .get('/foo') + .expect(200, 'hello, world', done) }) describe('.all(...fn)', function () { it('should reject no arguments', function () { - var router = new Router() - var route = router.route('/') + const router = new Router() + const route = router.route('/') assert.throws(route.all.bind(route), /argument handler is required/) }) it('should reject empty array', function () { - var router = new Router() - var route = router.route('/') + const router = new Router() + const route = router.route('/') assert.throws(route.all.bind(route, []), /argument handler is required/) }) it('should reject invalid fn', function () { - var router = new Router() - var route = router.route('/') + const router = new Router() + const route = router.route('/') assert.throws(route.all.bind(route, 2), /argument handler must be a function/) }) it('should respond to all methods', function (done) { - var cb = after(3, done) - var router = new Router() - var route = router.route('/foo') - var server = createServer(router) + const router = new Router() + const route = router.route('/foo') + const server = createServer(router) route.all(saw) - request(server) - .get('/foo') - .expect(200, 'saw GET /foo', cb) - - request(server) - .post('/foo') - .expect(200, 'saw POST /foo', cb) - - request(server) - .put('/foo') - .expect(200, 'saw PUT /foo', cb) + series([ + function (cb) { + request(server) + .get('/foo') + .expect(200, 'saw GET /foo', cb) + }, + function (cb) { + request(server) + .post('/foo') + .expect(200, 'saw POST /foo', cb) + }, + function (cb) { + request(server) + .put('/foo') + .expect(200, 'saw PUT /foo', cb) + } + ], done) }) it('should accept multiple arguments', function (done) { - var router = new Router() - var route = router.route('/foo') - var server = createServer(router) + const router = new Router() + const route = router.route('/foo') + const server = createServer(router) route.all(createHitHandle(1), createHitHandle(2), helloWorld) request(server) - .get('/foo') - .expect('x-fn-1', 'hit') - .expect('x-fn-2', 'hit') - .expect(200, 'hello, world', done) + .get('/foo') + .expect('x-fn-1', 'hit') + .expect('x-fn-2', 'hit') + .expect(200, 'hello, world', done) }) it('should accept single array of handlers', function (done) { - var router = new Router() - var route = router.route('/foo') - var server = createServer(router) + const router = new Router() + const route = router.route('/foo') + const server = createServer(router) route.all([createHitHandle(1), createHitHandle(2), helloWorld]) request(server) - .get('/foo') - .expect('x-fn-1', 'hit') - .expect('x-fn-2', 'hit') - .expect(200, 'hello, world', done) + .get('/foo') + .expect('x-fn-1', 'hit') + .expect('x-fn-2', 'hit') + .expect(200, 'hello, world', done) }) it('should accept nested arrays of handlers', function (done) { - var router = new Router() - var route = router.route('/foo') - var server = createServer(router) + const router = new Router() + const route = router.route('/foo') + const server = createServer(router) route.all([[createHitHandle(1), createHitHandle(2)], createHitHandle(3)], helloWorld) request(server) - .get('/foo') - .expect('x-fn-1', 'hit') - .expect('x-fn-2', 'hit') - .expect('x-fn-3', 'hit') - .expect(200, 'hello, world', done) + .get('/foo') + .expect('x-fn-1', 'hit') + .expect('x-fn-2', 'hit') + .expect('x-fn-3', 'hit') + .expect(200, 'hello, world', done) }) }) @@ -189,159 +254,166 @@ describe('Router', function () { // CONNECT is tricky and supertest doesn't support it return } + if (method === 'query' && process.version.startsWith('v21')) { + return + } - var body = method !== 'head' - ? 'hello, world' - : '' + const body = method !== 'head' + ? shouldHaveBody(Buffer.from('hello, world')) + : shouldNotHaveBody() describe('.' + method + '(...fn)', function () { it('should respond to a ' + method.toUpperCase() + ' request', function (done) { - var router = new Router() - var route = router.route('/') - var server = createServer(router) + const router = new Router() + const route = router.route('/') + const server = createServer(router) route[method](helloWorld) - request(server) - [method]('/') - .expect(200, body, done) + request(server)[method]('/') + .expect(200) + .expect(body) + .end(done) }) it('should reject no arguments', function () { - var router = new Router() - var route = router.route('/') + const router = new Router() + const route = router.route('/') assert.throws(route[method].bind(route), /argument handler is required/) }) it('should reject empty array', function () { - var router = new Router() - var route = router.route('/') + const router = new Router() + const route = router.route('/') assert.throws(route[method].bind(route, []), /argument handler is required/) }) it('should reject invalid fn', function () { - var router = new Router() - var route = router.route('/') + const router = new Router() + const route = router.route('/') assert.throws(route[method].bind(route, 2), /argument handler must be a function/) }) it('should accept multiple arguments', function (done) { - var router = new Router() - var route = router.route('/foo') - var server = createServer(router) + const router = new Router() + const route = router.route('/foo') + const server = createServer(router) route[method](createHitHandle(1), createHitHandle(2), helloWorld) - request(server) - [method]('/foo') - .expect('x-fn-1', 'hit') - .expect('x-fn-2', 'hit') - .expect(200, body, done) + request(server)[method]('/foo') + .expect(200) + .expect('x-fn-1', 'hit') + .expect('x-fn-2', 'hit') + .expect(body) + .end(done) }) it('should accept single array of handlers', function (done) { - var router = new Router() - var route = router.route('/foo') - var server = createServer(router) + const router = new Router() + const route = router.route('/foo') + const server = createServer(router) route[method]([createHitHandle(1), createHitHandle(2), helloWorld]) - request(server) - [method]('/foo') - .expect('x-fn-1', 'hit') - .expect('x-fn-2', 'hit') - .expect(200, body, done) + request(server)[method]('/foo') + .expect(200) + .expect('x-fn-1', 'hit') + .expect('x-fn-2', 'hit') + .expect(body) + .end(done) }) it('should accept nested arrays of handlers', function (done) { - var router = new Router() - var route = router.route('/foo') - var server = createServer(router) + const router = new Router() + const route = router.route('/foo') + const server = createServer(router) route[method]([[createHitHandle(1), createHitHandle(2)], createHitHandle(3)], helloWorld) - request(server) - [method]('/foo') - .expect('x-fn-1', 'hit') - .expect('x-fn-2', 'hit') - .expect('x-fn-3', 'hit') - .expect(200, body, done) + request(server)[method]('/foo') + .expect(200) + .expect('x-fn-1', 'hit') + .expect('x-fn-2', 'hit') + .expect('x-fn-3', 'hit') + .expect(body) + .end(done) }) }) }) describe('error handling', function () { it('should handle errors from next(err)', function (done) { - var router = new Router() - var route = router.route('/foo') - var server = createServer(router) + const router = new Router() + const route = router.route('/foo') + const server = createServer(router) - route.all(function createError(req, res, next) { + route.all(function createError (req, res, next) { next(new Error('boom!')) }) route.all(helloWorld) - route.all(function handleError(err, req, res, next) { + route.all(function handleError (err, req, res, next) { res.statusCode = 500 res.end('caught: ' + err.message) }) request(server) - .get('/foo') - .expect(500, 'caught: boom!', done) + .get('/foo') + .expect(500, 'caught: boom!', done) }) it('should handle errors thrown', function (done) { - var router = new Router() - var route = router.route('/foo') - var server = createServer(router) + const router = new Router() + const route = router.route('/foo') + const server = createServer(router) - route.all(function createError(req, res, next) { + route.all(function createError (req, res, next) { throw new Error('boom!') }) route.all(helloWorld) - route.all(function handleError(err, req, res, next) { + route.all(function handleError (err, req, res, next) { res.statusCode = 500 res.end('caught: ' + err.message) }) request(server) - .get('/foo') - .expect(500, 'caught: boom!', done) + .get('/foo') + .expect(500, 'caught: boom!', done) }) it('should handle errors thrown in error handlers', function (done) { - var router = new Router() - var route = router.route('/foo') - var server = createServer(router) + const router = new Router() + const route = router.route('/foo') + const server = createServer(router) - route.all(function createError(req, res, next) { + route.all(function createError (req, res, next) { throw new Error('boom!') }) - route.all(function handleError(err, req, res, next) { - throw new Error('oh, no!') + route.all(function handleError (err, req, res, next) { + throw new Error('ouch: ' + err.message) }) - route.all(function handleError(err, req, res, next) { + route.all(function handleError (err, req, res, next) { res.statusCode = 500 res.end('caught: ' + err.message) }) request(server) - .get('/foo') - .expect(500, 'caught: oh, no!', done) + .get('/foo') + .expect(500, 'caught: ouch: boom!', done) }) }) describe('next("route")', function () { it('should invoke next handler', function (done) { - var router = new Router() - var route = router.route('/foo') - var server = createServer(router) + const router = new Router() + const route = router.route('/foo') + const server = createServer(router) route.get(function handle (req, res, next) { res.setHeader('x-next', 'route') @@ -351,15 +423,15 @@ describe('Router', function () { router.use(saw) request(server) - .get('/foo') - .expect('x-next', 'route') - .expect(200, 'saw GET /foo', done) + .get('/foo') + .expect('x-next', 'route') + .expect(200, 'saw GET /foo', done) }) it('should invoke next route', function (done) { - var router = new Router() - var route = router.route('/foo') - var server = createServer(router) + const router = new Router() + const route = router.route('/foo') + const server = createServer(router) route.get(function handle (req, res, next) { res.setHeader('x-next', 'route') @@ -369,15 +441,15 @@ describe('Router', function () { router.route('/foo').all(saw) request(server) - .get('/foo') - .expect('x-next', 'route') - .expect(200, 'saw GET /foo', done) + .get('/foo') + .expect('x-next', 'route') + .expect(200, 'saw GET /foo', done) }) it('should skip next handlers in route', function (done) { - var router = new Router() - var route = router.route('/foo') - var server = createServer(router) + const router = new Router() + const route = router.route('/foo') + const server = createServer(router) route.all(createHitHandle(1)) route.get(function goNext (req, res, next) { @@ -389,40 +461,40 @@ describe('Router', function () { router.use(saw) request(server) - .get('/foo') - .expect(shouldHitHandle(1)) - .expect('x-next', 'route') - .expect(shouldNotHitHandle(2)) - .expect(200, 'saw GET /foo', done) + .get('/foo') + .expect(shouldHitHandle(1)) + .expect('x-next', 'route') + .expect(shouldNotHitHandle(2)) + .expect(200, 'saw GET /foo', done) }) it('should not invoke error handlers', function (done) { - var router = new Router() - var route = router.route('/foo') - var server = createServer(router) + const router = new Router() + const route = router.route('/foo') + const server = createServer(router) route.all(function goNext (req, res, next) { res.setHeader('x-next', 'route') next('route') }) - route.all(function handleError(err, req, res, next) { + route.all(function handleError (err, req, res, next) { res.statusCode = 500 res.end('caught: ' + err.message) }) request(server) - .get('/foo') - .expect('x-next', 'route') - .expect(404, done) + .get('/foo') + .expect('x-next', 'route') + .expect(404, done) }) }) describe('next("router")', function () { it('should exit the router', function (done) { - var router = new Router() - var route = router.route('/foo') - var server = createServer(router) + const router = new Router() + const route = router.route('/foo') + const server = createServer(router) function handle (req, res, next) { res.setHeader('x-next', 'router') @@ -434,16 +506,16 @@ describe('Router', function () { router.use(saw) request(server) - .get('/foo') - .expect('x-next', 'router') - .expect(shouldNotHitHandle(1)) - .expect(404, done) + .get('/foo') + .expect('x-next', 'router') + .expect(shouldNotHitHandle(1)) + .expect(404, done) }) it('should not invoke error handlers', function (done) { - var router = new Router() - var route = router.route('/foo') - var server = createServer(router) + const router = new Router() + const route = router.route('/foo') + const server = createServer(router) route.all(function goNext (req, res, next) { res.setHeader('x-next', 'router') @@ -461,251 +533,382 @@ describe('Router', function () { }) request(server) - .get('/foo') - .expect('x-next', 'router') - .expect(404, done) + .get('/foo') + .expect('x-next', 'router') + .expect(404, done) }) }) - describe('path', function () { - describe('using ":name"', function () { - it('should name a capture group', function (done) { - var router = new Router() - var route = router.route('/:foo') - var server = createServer(router) + describe('promise support', function () { + it('should pass rejected promise value', function (done) { + const router = new Router() + const route = router.route('/foo') + const server = createServer(router) - route.all(sendParams) + route.all(function createError (req, res, next) { + return Promise.reject(new Error('boom!')) + }) - request(server) - .get('/bar') - .expect(200, {'foo': 'bar'}, done) + route.all(helloWorld) + + route.all(function handleError (err, req, res, next) { + res.statusCode = 500 + res.end('caught: ' + err.message) }) - it('should match single path segment', function (done) { - var router = new Router() - var route = router.route('/:foo') - var server = createServer(router) + request(server) + .get('/foo') + .expect(500, 'caught: boom!', done) + }) - route.all(sendParams) + it('should pass rejected promise without value', function (done) { + const router = new Router() + const route = router.route('/foo') + const server = createServer(router) - request(server) - .get('/bar/bar') - .expect(404, done) + route.all(function createError (req, res, next) { + return Promise.reject() // eslint-disable-line prefer-promise-reject-errors }) - it('should work multiple times', function (done) { - var router = new Router() - var route = router.route('/:foo/:bar') - var server = createServer(router) - - route.all(sendParams) + route.all(helloWorld) - request(server) - .get('/fizz/buzz') - .expect(200, {'foo': 'fizz', 'bar': 'buzz'}, done) + route.all(function handleError (err, req, res, next) { + res.statusCode = 500 + res.end('caught: ' + err.message) }) - it('should work following a partial capture group', function (done) { - var cb = after(2, done) - var router = new Router() - var route = router.route('/user(s)?/:user/:op') - var server = createServer(router) + request(server) + .get('/foo') + .expect(500, 'caught: Rejected promise', done) + }) - route.all(sendParams) + it('should ignore resolved promise', function (done) { + const router = new Router() + const route = router.route('/foo') + const server = createServer(router) - request(server) - .get('/user/tj/edit') - .expect(200, {'user': 'tj', 'op': 'edit'}, cb) + route.all(function createError (req, res, next) { + saw(req, res) + return Promise.resolve('foo') + }) - request(server) - .get('/users/tj/edit') - .expect(200, {'0': 's', 'user': 'tj', 'op': 'edit'}, cb) + route.all(function () { + done(new Error('Unexpected route invoke')) }) - it('should work inside literal paranthesis', function (done) { - var router = new Router() - var route = router.route('/:user\\(:op\\)') - var server = createServer(router) + request(server) + .get('/foo') + .expect(200, 'saw GET /foo', done) + }) - route.all(sendParams) + describe('error handling', function () { + it('should pass rejected promise value', function (done) { + const router = new Router() + const route = router.route('/foo') + const server = createServer(router) + + route.all(function createError (req, res, next) { + return Promise.reject(new Error('boom!')) + }) + + route.all(function handleError (err, req, res, next) { + return Promise.reject(new Error('caught: ' + err.message)) + }) + + route.all(function handleError (err, req, res, next) { + res.statusCode = 500 + res.end('caught again: ' + err.message) + }) request(server) - .get('/tj(edit)') - .expect(200, {'user': 'tj', 'op': 'edit'}, done) + .get('/foo') + .expect(500, 'caught again: caught: boom!', done) }) - it('should work within arrays', function (done) { - var cb = after(2, done) - var router = new Router() - var route = router.route(['/user/:user/poke', '/user/:user/pokes']) - var server = createServer(router) + it('should pass rejected promise without value', function (done) { + const router = new Router() + const route = router.route('/foo') + const server = createServer(router) - route.all(sendParams) + route.all(function createError (req, res, next) { + return Promise.reject(new Error('boom!')) + }) - request(server) - .get('/user/tj/poke') - .expect(200, {'user': 'tj'}, cb) + route.all(function handleError (err, req, res, next) { + assert.equal(err.message, 'boom!') + return Promise.reject() // eslint-disable-line prefer-promise-reject-errors + }) + + route.all(function handleError (err, req, res, next) { + res.statusCode = 500 + res.end('caught again: ' + err.message) + }) request(server) - .get('/user/tj/pokes') - .expect(200, {'user': 'tj'}, cb) + .get('/foo') + .expect(500, 'caught again: Rejected promise', done) }) - }) - describe('using "*"', function () { - it('should capture everything', function (done) { - var router = new Router() - var route = router.route('*') - var server = createServer(router) + it('should ignore resolved promise', function (done) { + const router = new Router() + const route = router.route('/foo') + const server = createServer(router) - route.all(sendParams) + route.all(function createError (req, res, next) { + return Promise.reject(new Error('boom!')) + }) + + route.all(function handleError (err, req, res, next) { + res.statusCode = 500 + res.end('caught: ' + err.message) + return Promise.resolve('foo') + }) + + route.all(function () { + done(new Error('Unexpected route invoke')) + }) request(server) - .get('/foo/bar/baz') - .expect(200, {'0': '/foo/bar/baz'}, done) + .get('/foo') + .expect(500, 'caught: boom!', done) }) + }) + }) - it('should decode the capture', function (done) { - var router = new Router() - var route = router.route('*') - var server = createServer(router) + describe('path', function () { + describe('using ":name"', function () { + it('should name a capture group', function (done) { + const router = new Router() + const route = router.route('/:foo') + const server = createServer(router) route.all(sendParams) request(server) - .get('/foo/%20/baz') - .expect(200, {'0': '/foo/ /baz'}, done) + .get('/bar') + .expect(200, { foo: 'bar' }, done) }) - it('should capture everything with pre- and post-fixes', function (done) { - var router = new Router() - var route = router.route('/foo/*/bar') - var server = createServer(router) + it('should match single path segment', function (done) { + const router = new Router() + const route = router.route('/:foo') + const server = createServer(router) route.all(sendParams) request(server) - .get('/foo/1/2/3/bar') - .expect(200, {'0': '1/2/3'}, done) + .get('/bar/bar') + .expect(404, done) }) - it('should capture greedly', function (done) { - var router = new Router() - var route = router.route('/foo/*/bar') - var server = createServer(router) + it('should work multiple times', function (done) { + const router = new Router() + const route = router.route('/:foo/:bar') + const server = createServer(router) route.all(sendParams) request(server) - .get('/foo/bar/bar/bar') - .expect(200, {'0': 'bar/bar'}, done) + .get('/fizz/buzz') + .expect(200, { foo: 'fizz', bar: 'buzz' }, done) }) - it('should be an optional capture', function (done) { - var router = new Router() - var route = router.route('/foo*') - var server = createServer(router) + it('should work inside literal parentheses', function (done) { + const router = new Router() + const route = router.route('/:user\\(:op\\)') + const server = createServer(router) route.all(sendParams) request(server) - .get('/foo') - .expect(200, {'0': ''}, done) + .get('/tj(edit)') + .expect(200, { user: 'tj', op: 'edit' }, done) }) - it('should require preceeding /', function (done) { - var cb = after(2, done) - var router = new Router() - var route = router.route('/foo/*') - var server = createServer(router) + it('should work within arrays', function (done) { + const router = new Router() + const route = router.route(['/user/:user/poke', '/user/:user/pokes']) + const server = createServer(router) route.all(sendParams) - - request(server) - .get('/foo') - .expect(404, cb) - - request(server) - .get('/foo/') - .expect(200, cb) + series([ + function (cb) { + request(server) + .get('/user/tj/poke') + .expect(200, { user: 'tj' }, cb) + }, + function (cb) { + request(server) + .get('/user/tj/pokes') + .expect(200, { user: 'tj' }, cb) + } + ], done) }) + }) - it('should work in a named parameter', function (done) { - var cb = after(2, done) - var router = new Router() - var route = router.route('/:foo(*)') - var server = createServer(router) + describe('using "{:name}"', function () { + it('should name an optional parameter', function (done) { + const router = new Router() + const route = router.route('{/:foo}') + const server = createServer(router) route.all(sendParams) + series([ + function (cb) { + request(server) + .get('/bar') + .expect(200, { foo: 'bar' }, cb) + }, + function (cb) { + request(server) + .get('/') + .expect(200, {}, cb) + } + ], done) + }) + + it('should work in any segment', function (done) { + const router = new Router() + const route = router.route('/user{/:foo}/delete') + const server = createServer(router) - request(server) - .get('/bar') - .expect(200, {'0': 'bar', 'foo': 'bar'}, cb) - - request(server) - .get('/fizz/buzz') - .expect(200, {'0': 'fizz/buzz', 'foo': 'fizz/buzz'}, cb) + route.all(sendParams) + series([ + function (cb) { + request(server) + .get('/user/bar/delete') + .expect(200, { foo: 'bar' }, cb) + }, + function (cb) { + request(server) + .get('/user/delete') + .expect(200, {}, cb) + } + ], done) }) + }) - it('should work before a named parameter', function (done) { - var router = new Router() - var route = router.route('/*/user/:id') - var server = createServer(router) + describe('using "*name"', function () { + it('should name a zero-or-more repeated parameter', function (done) { + const router = new Router() + const route = router.route('{/*foo}') + const server = createServer(router) route.all(sendParams) + series([ + function (cb) { + request(server) + .get('/') + .expect(200, {}, cb) + }, + function (cb) { + request(server) + .get('/bar') + .expect(200, { foo: ['bar'] }, cb) + }, + function (cb) { + request(server) + .get('/fizz/buzz') + .expect(200, { foo: ['fizz', 'buzz'] }, cb) + } + ], done) + }) + + it('should work in any segment', function (done) { + const router = new Router() + const route = router.route('/user{/*foo}/delete') + const server = createServer(router) - request(server) - .get('/poke/user/42') - .expect(200, {'0': 'poke', 'id': '42'}, done) + route.all(sendParams) + series([ + function (cb) { + request(server) + .get('/user/delete') + .expect(200, {}, cb) + }, + function (cb) { + request(server) + .get('/user/bar/delete') + .expect(200, { foo: ['bar'] }, cb) + }, + function (cb) { + request(server) + .get('/user/fizz/buzz/delete') + .expect(200, { foo: ['fizz', 'buzz'] }, cb) + } + ], done) }) + }) - it('should work within arrays', function (done) { - var cb = after(3, done) - var router = new Router() - var route = router.route(['/user/:id', '/foo/*', '/:action']) - var server = createServer(router) + describe('using regular expression with param name "(?pattern)"', function () { + it('should limit capture group to regexp match', function (done) { + const router = new Router() + const route = router.route(/\/(?[0-9]+)/) + const server = createServer(router) route.all(sendParams) - request(server) - .get('/user/42') - .expect(200, {'id': '42'}, cb) + series([ + function (cb) { + request(server) + .get('/foo') + .expect(404, cb) + }, + function (cb) { + request(server) + .get('/42') + .expect(200, { foo: '42' }, cb) + } + ], done) + }) + }) - request(server) - .get('/foo/bar') - .expect(200, {'0': 'bar'}, cb) + describe('using "(regexp)"', function () { + it('should add capture group using regexp', function (done) { + const router = new Router() + const route = router.route(/\/page_([0-9]+)/) + const server = createServer(router) - request(server) - .get('/poke') - .expect(200, {'action': 'poke'}, cb) + route.all(sendParams) + series([ + function (cb) { + request(server) + .get('/page_foo') + .expect(404, cb) + }, + function (cb) { + request(server) + .get('/page_42') + .expect(200, { 0: '42' }, cb) + } + ], done) + }) + + it('should not treat regexp as literal regexp', function () { + const router = new Router() + assert.throws(function () { + router.route('/([a-z]+:n[0-9]+)') + }, /TypeError: Unexpected \( at/) }) }) }) }) }) -function helloWorld(req, res) { +function helloWorld (req, res) { res.statusCode = 200 res.setHeader('Content-Type', 'text/plain') res.end('hello, world') } -function setsaw(num) { - var name = 'x-saw-' + String(num) - return function hit(req, res, next) { - res.setHeader(name, req.method + ' ' + req.url) - next() - } -} - -function saw(req, res) { - var msg = 'saw ' + req.method + ' ' + req.url +function saw (req, res) { + const msg = 'saw ' + req.method + ' ' + req.url res.statusCode = 200 res.setHeader('Content-Type', 'text/plain') res.end(msg) } -function sendParams(req, res) { +function sendParams (req, res) { res.statusCode = 200 res.setHeader('Content-Type', 'application/json') res.end(JSON.stringify(req.params)) diff --git a/test/router.js b/test/router.js index ea3d73c3..b440e404 100644 --- a/test/router.js +++ b/test/router.js @@ -1,16 +1,18 @@ - -var after = require('after') -var methods = require('methods') -var Router = require('..') -var utils = require('./support/utils') - -var assert = utils.assert -var createHitHandle = utils.createHitHandle -var createServer = utils.createServer -var rawrequest = utils.rawrequest -var request = utils.request -var shouldHitHandle = utils.shouldHitHandle -var shouldNotHitHandle = utils.shouldNotHitHandle +const { it, describe } = require('mocha') +const series = require('run-series') +const Router = require('..') +const utils = require('./support/utils') + +const assert = utils.assert +const createHitHandle = utils.createHitHandle +const createServer = utils.createServer +const rawrequest = utils.rawrequest +const request = utils.request +const shouldHaveBody = utils.shouldHaveBody +const shouldHitHandle = utils.shouldHitHandle +const shouldNotHaveBody = utils.shouldNotHaveBody +const shouldNotHitHandle = utils.shouldNotHitHandle +const methods = utils.methods describe('Router', function () { it('should return a function', function () { @@ -22,232 +24,290 @@ describe('Router', function () { }) it('should reject missing callback', function () { - var router = new Router() + const router = new Router() assert.throws(function () { router({}, {}) }, /argument callback is required/) }) it('should invoke callback without "req.url"', function (done) { - var router = new Router() + const router = new Router() router.use(saw) router({}, {}, done) }) describe('.all(path, fn)', function () { it('should be chainable', function () { - var router = new Router() + const router = new Router() assert.equal(router.all('/', helloWorld), router) }) it('should respond to all methods', function (done) { - var cb = after(methods.length, done) - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) router.all('/', helloWorld) - methods.forEach(function (method) { - if (method === 'connect') { - // CONNECT is tricky and supertest doesn't support it - return cb() + series(methods.map(function (method) { + return function (cb) { + if (method === 'connect') { + // CONNECT is tricky and supertest doesn't support it + return cb() + } + if (method === 'query' && process.version.startsWith('v21')) { + return cb() + } + + const body = method !== 'head' + ? shouldHaveBody(Buffer.from('hello, world')) + : shouldNotHaveBody() + + request(server)[method]('/') + .expect(200) + .expect(body) + .end(cb) } - - var body = method !== 'head' - ? 'hello, world' - : '' - - request(server) - [method]('/') - .expect(200, body, cb) - }) + }), done) }) it('should support array of paths', function (done) { - var cb = after(3, done) - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) router.all(['/foo', '/bar'], saw) - - request(server) - .get('/') - .expect(404, cb) - - request(server) - .get('/foo') - .expect(200, 'saw GET /foo', cb) - - request(server) - .get('/bar') - .expect(200, 'saw GET /bar', cb) + series([ + function (cb) { + request(server) + .get('/') + .expect(404, cb) + }, + function (cb) { + request(server) + .get('/foo') + .expect(200, 'saw GET /foo', cb) + }, + function (cb) { + request(server) + .get('/bar') + .expect(200, 'saw GET /bar', cb) + } + ], done) }) it('should support regexp path', function (done) { - var cb = after(3, done) - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) router.all(/^\/[a-z]oo$/, saw) - - request(server) - .get('/') - .expect(404, cb) - - request(server) - .get('/foo') - .expect(200, 'saw GET /foo', cb) - - request(server) - .get('/zoo') - .expect(200, 'saw GET /zoo', cb) + series([ + function (cb) { + request(server) + .get('/') + .expect(404, cb) + }, + function (cb) { + request(server) + .get('/foo') + .expect(200, 'saw GET /foo', cb) + }, + function (cb) { + request(server) + .get('/zoo') + .expect(200, 'saw GET /zoo', cb) + } + ], done) }) it('should support parameterized path', function (done) { - var cb = after(4, done) - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) router.all('/:thing', saw) + series([ + function (cb) { + request(server) + .get('/') + .expect(404, cb) + }, + function (cb) { + request(server) + .get('/foo') + .expect(200, 'saw GET /foo', cb) + }, + function (cb) { + request(server) + .get('/bar') + .expect(200, 'saw GET /bar', cb) + }, + function (cb) { + request(server) + .get('/foo/bar') + .expect(404, cb) + } + ], done) + }) - request(server) - .get('/') - .expect(404, cb) + it('should not stack overflow with many registered routes', function (done) { + this.timeout(5000) // long-running test - request(server) - .get('/foo') - .expect(200, 'saw GET /foo', cb) + const router = new Router() + const server = createServer(router) - request(server) - .get('/bar') - .expect(200, 'saw GET /bar', cb) + for (let i = 0; i < 6000; i++) { + router.get('/thing' + i, helloWorld) + } + + router.get('/', helloWorld) request(server) - .get('/foo/bar') - .expect(404, cb) + .get('/') + .expect(200, 'hello, world', done) }) - it('should not stack overflow with many registered routes', function (done) { - var router = new Router() - var server = createServer(router) + it('should not stack overflow with a large sync stack', function (done) { + this.timeout(5000) // long-running test - for (var i = 0; i < 6000; i++) { - router.get('/thing' + i, helloWorld) + const router = new Router() + const server = createServer(router) + + for (let i = 0; i < 6000; i++) { + router.get('/foo', function (req, res, next) { next() }) } - router.get('/', helloWorld) + router.get('/foo', helloWorld) request(server) - .get('/') - .expect(200, 'hello, world', done) + .get('/foo') + .expect(200, 'hello, world', done) }) describe('with "caseSensitive" option', function () { it('should not match paths case-sensitively by default', function (done) { - var cb = after(3, done) - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) router.all('/foo/bar', saw) - - request(server) - .get('/foo/bar') - .expect(200, 'saw GET /foo/bar', cb) - - request(server) - .get('/FOO/bar') - .expect(200, 'saw GET /FOO/bar', cb) - - request(server) - .get('/FOO/BAR') - .expect(200, 'saw GET /FOO/BAR', cb) + series([ + function (cb) { + request(server) + .get('/foo/bar') + .expect(200, 'saw GET /foo/bar', cb) + }, + function (cb) { + request(server) + .get('/FOO/bar') + .expect(200, 'saw GET /FOO/bar', cb) + }, + function (cb) { + request(server) + .get('/FOO/BAR') + .expect(200, 'saw GET /FOO/BAR', cb) + } + ], done) }) it('should not match paths case-sensitively when false', function (done) { - var cb = after(3, done) - var router = new Router({ caseSensitive: false }) - var server = createServer(router) + const router = new Router({ caseSensitive: false }) + const server = createServer(router) router.all('/foo/bar', saw) - - request(server) - .get('/foo/bar') - .expect(200, 'saw GET /foo/bar', cb) - - request(server) - .get('/FOO/bar') - .expect(200, 'saw GET /FOO/bar', cb) - - request(server) - .get('/FOO/BAR') - .expect(200, 'saw GET /FOO/BAR', cb) + series([ + function (cb) { + request(server) + .get('/foo/bar') + .expect(200, 'saw GET /foo/bar', cb) + }, + function (cb) { + request(server) + .get('/FOO/bar') + .expect(200, 'saw GET /FOO/bar', cb) + }, + function (cb) { + request(server) + .get('/FOO/BAR') + .expect(200, 'saw GET /FOO/BAR', cb) + } + ], done) }) it('should match paths case-sensitively when true', function (done) { - var cb = after(3, done) - var router = new Router({ caseSensitive: true }) - var server = createServer(router) + const router = new Router({ caseSensitive: true }) + const server = createServer(router) router.all('/foo/bar', saw) - - request(server) - .get('/foo/bar') - .expect(200, 'saw GET /foo/bar', cb) - - request(server) - .get('/FOO/bar') - .expect(404, cb) - - request(server) - .get('/FOO/BAR') - .expect(404, cb) + series([ + function (cb) { + request(server) + .get('/foo/bar') + .expect(200, 'saw GET /foo/bar', cb) + }, + function (cb) { + request(server) + .get('/FOO/bar') + .expect(404, cb) + }, + function (cb) { + request(server) + .get('/FOO/BAR') + .expect(404, cb) + } + ], done) }) }) describe('with "strict" option', function () { it('should accept optional trailing slashes by default', function (done) { - var cb = after(2, done) - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) router.all('/foo', saw) - - request(server) - .get('/foo') - .expect(200, 'saw GET /foo', cb) - - request(server) - .get('/foo/') - .expect(200, 'saw GET /foo/', cb) + series([ + function (cb) { + request(server) + .get('/foo') + .expect(200, 'saw GET /foo', cb) + }, + function (cb) { + request(server) + .get('/foo/') + .expect(200, 'saw GET /foo/', cb) + } + ], done) }) it('should accept optional trailing slashes when false', function (done) { - var cb = after(2, done) - var router = new Router({ strict: false }) - var server = createServer(router) + const router = new Router({ strict: false }) + const server = createServer(router) router.all('/foo', saw) - - request(server) - .get('/foo') - .expect(200, 'saw GET /foo', cb) - - request(server) - .get('/foo/') - .expect(200, 'saw GET /foo/', cb) + series([ + function (cb) { + request(server) + .get('/foo') + .expect(200, 'saw GET /foo', cb) + }, + function (cb) { + request(server) + .get('/foo/') + .expect(200, 'saw GET /foo/', cb) + } + ], done) }) it('should not accept optional trailing slashes when true', function (done) { - var cb = after(2, done) - var router = new Router({ strict: true }) - var server = createServer(router) + const router = new Router({ strict: true }) + const server = createServer(router) router.all('/foo', saw) - - request(server) - .get('/foo') - .expect(200, 'saw GET /foo', cb) - - request(server) - .get('/foo/') - .expect(404, cb) + series([ + function (cb) { + request(server) + .get('/foo') + .expect(200, 'saw GET /foo', cb) + }, + function (cb) { + request(server) + .get('/foo/') + .expect(404, cb) + } + ], done) }) }) }) @@ -257,166 +317,160 @@ describe('Router', function () { // CONNECT is tricky and supertest doesn't support it return } + if (method === 'query' && process.version.startsWith('v21')) { + return + } - var body = method !== 'head' - ? 'hello, world' - : '' + const body = method !== 'head' + ? shouldHaveBody(Buffer.from('hello, world')) + : shouldNotHaveBody() describe('.' + method + '(path, ...fn)', function () { it('should be chainable', function () { - var router = new Router() + const router = new Router() assert.equal(router[method]('/', helloWorld), router) }) it('should respond to a ' + method.toUpperCase() + ' request', function (done) { - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) router[method]('/', helloWorld) - request(server) - [method]('/') - .expect(200, body, done) + request(server)[method]('/') + .expect(200) + .expect(body) + .end(done) }) it('should reject invalid fn', function () { - var router = new Router() + const router = new Router() assert.throws(router[method].bind(router, '/', 2), /argument handler must be a function/) }) it('should support array of paths', function (done) { - var cb = after(3, done) - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) router[method](['/foo', '/bar'], createHitHandle(1), helloWorld) - request(server) - [method]('/') - .expect(shouldNotHitHandle(1)) - .expect(404, cb) - - request(server) - [method]('/foo') - .expect(shouldHitHandle(1)) - .expect(200, body, cb) - - request(server) - [method]('/bar') - .expect(shouldHitHandle(1)) - .expect(200, body, cb) - }) - - it('should support regexp path', function (done) { - var cb = after(3, done) - var router = new Router() - var server = createServer(router) - - router[method](/^\/[a-z]oo$/, createHitHandle(1), helloWorld) - - request(server) - [method]('/') - .expect(shouldNotHitHandle(1)) - .expect(404, cb) - - request(server) - [method]('/foo') - .expect(shouldHitHandle(1)) - .expect(200, body, cb) - - request(server) - [method]('/zoo') - .expect(shouldHitHandle(1)) - .expect(200, body, cb) + series([ + function (cb) { + request(server)[method]('/') + .expect(404) + .expect(shouldNotHitHandle(1)) + .end(cb) + }, + function (cb) { + request(server)[method]('/foo') + .expect(200) + .expect(shouldHitHandle(1)) + .expect(body) + .end(cb) + }, + function (cb) { + request(server)[method]('/bar') + .expect(200) + .expect(shouldHitHandle(1)) + .expect(body) + .end(cb) + } + ], done) }) it('should support parameterized path', function (done) { - var cb = after(4, done) - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) router[method]('/:thing', createHitHandle(1), helloWorld) - request(server) - [method]('/') - .expect(shouldNotHitHandle(1)) - .expect(404, cb) - - request(server) - [method]('/foo') - .expect(shouldHitHandle(1)) - .expect(200, body, cb) - - request(server) - [method]('/bar') - .expect(shouldHitHandle(1)) - .expect(200, body, cb) - - request(server) - [method]('/foo/bar') - .expect(shouldNotHitHandle(1)) - .expect(404, cb) + series([ + function (cb) { + request(server)[method]('/') + .expect(404) + .expect(shouldNotHitHandle(1)) + .end(cb) + }, + function (cb) { + request(server)[method]('/foo') + .expect(200) + .expect(shouldHitHandle(1)) + .expect(body) + .end(cb) + }, + function (cb) { + request(server)[method]('/bar') + .expect(200) + .expect(shouldHitHandle(1)) + .expect(body) + .end(cb) + }, + function (cb) { + request(server)[method]('/foo/bar') + .expect(404) + .expect(shouldNotHitHandle(1)) + .end(cb) + } + ], done) }) it('should accept multiple arguments', function (done) { - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) router[method]('/', createHitHandle(1), createHitHandle(2), helloWorld) - request(server) - [method]('/') - .expect(shouldHitHandle(1)) - .expect(shouldHitHandle(2)) - .expect(200, body, done) + request(server)[method]('/') + .expect(200) + .expect(shouldHitHandle(1)) + .expect(shouldHitHandle(2)) + .expect(body) + .end(done) }) describe('req.baseUrl', function () { it('should be empty', function (done) { - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) - router[method]('/foo', function handle(req, res) { + router[method]('/foo', function handle (req, res) { res.setHeader('x-url-base', JSON.stringify(req.baseUrl)) res.end() }) - request(server) - [method]('/foo') - .expect('x-url-base', '""') - .expect(200, done) + request(server)[method]('/foo') + .expect('x-url-base', '""') + .expect(200, done) }) }) describe('req.route', function () { it('should be a Route', function (done) { - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) - router[method]('/foo', function handle(req, res) { + router[method]('/foo', function handle (req, res) { res.setHeader('x-is-route', String(req.route instanceof Router.Route)) res.end() }) - request(server) - [method]('/foo') - .expect('x-is-route', 'true') - .expect(200, done) + request(server)[method]('/foo') + .expect('x-is-route', 'true') + .expect(200, done) }) it('should be the matched route', function (done) { - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) - router[method]('/foo', function handle(req, res) { + router[method]('/foo', function handle (req, res) { res.setHeader('x-is-route', String(req.route.path === '/foo')) res.end() }) - request(server) - [method]('/foo') - .expect('x-is-route', 'true') - .expect(200, done) + request(server)[method]('/foo') + .expect('x-is-route', 'true') + .expect(200, done) }) }) }) @@ -424,17 +478,17 @@ describe('Router', function () { describe('.use(...fn)', function () { it('should reject missing functions', function () { - var router = new Router() + const router = new Router() assert.throws(router.use.bind(router), /argument handler is required/) }) it('should reject empty array', function () { - var router = new Router() + const router = new Router() assert.throws(router.use.bind(router, []), /argument handler is required/) }) it('should reject non-functions', function () { - var router = new Router() + const router = new Router() assert.throws(router.use.bind(router, '/', 'hello'), /argument handler must be a function/) assert.throws(router.use.bind(router, '/', 5), /argument handler must be a function/) assert.throws(router.use.bind(router, '/', null), /argument handler must be a function/) @@ -442,37 +496,43 @@ describe('Router', function () { }) it('should be chainable', function () { - var router = new Router() + const router = new Router() assert.equal(router.use(helloWorld), router) }) it('should invoke function for all requests', function (done) { - var cb = after(4, done) - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) router.use(saw) - request(server) - .get('/') - .expect(200, 'saw GET /', cb) - - request(server) - .put('/') - .expect(200, 'saw PUT /', cb) - - request(server) - .post('/foo') - .expect(200, 'saw POST /foo', cb) - - rawrequest(server) - .options('*') - .expect(200, 'saw OPTIONS *', cb) + series([ + function (cb) { + request(server) + .get('/') + .expect(200, 'saw GET /', cb) + }, + function (cb) { + request(server) + .put('/') + .expect(200, 'saw PUT /', cb) + }, + function (cb) { + request(server) + .post('/foo') + .expect(200, 'saw POST /foo', cb) + }, + function (cb) { + rawrequest(server) + .options('*') + .expect(200, 'saw OPTIONS *', cb) + } + ], done) }) it('should not invoke for blank URLs', function (done) { - var router = new Router() - var server = createServer(function hander(req, res, next) { + const router = new Router() + const server = createServer(function hander (req, res, next) { req.url = '' router(req, res, next) }) @@ -480,127 +540,144 @@ describe('Router', function () { router.use(saw) request(server) - .get('/') - .expect(404, done) + .get('/') + .expect(404, done) }) it('should support another router', function (done) { - var inner = new Router() - var router = new Router() - var server = createServer(router) + const inner = new Router() + const router = new Router() + const server = createServer(router) inner.use(saw) router.use(inner) request(server) - .get('/') - .expect(200, 'saw GET /', done) + .get('/') + .expect(200, 'saw GET /', done) }) it('should accept multiple arguments', function (done) { - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) router.use(createHitHandle(1), createHitHandle(2), helloWorld) request(server) - .get('/') - .expect(shouldHitHandle(1)) - .expect(shouldHitHandle(2)) - .expect(200, 'hello, world', done) + .get('/') + .expect(shouldHitHandle(1)) + .expect(shouldHitHandle(2)) + .expect(200, 'hello, world', done) }) it('should accept single array of middleware', function (done) { - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) router.use([createHitHandle(1), createHitHandle(2), helloWorld]) request(server) - .get('/') - .expect(shouldHitHandle(1)) - .expect(shouldHitHandle(2)) - .expect(200, 'hello, world', done) + .get('/') + .expect(shouldHitHandle(1)) + .expect(shouldHitHandle(2)) + .expect(200, 'hello, world', done) }) it('should accept nested arrays of middleware', function (done) { - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) router.use([[createHitHandle(1), createHitHandle(2)], createHitHandle(3)], helloWorld) request(server) - .get('/') - .expect(shouldHitHandle(1)) - .expect(shouldHitHandle(2)) - .expect(shouldHitHandle(3)) - .expect(200, 'hello, world', done) + .get('/') + .expect(shouldHitHandle(1)) + .expect(shouldHitHandle(2)) + .expect(shouldHitHandle(3)) + .expect(200, 'hello, world', done) }) it('should not invoke singular error function', function (done) { - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) - router.use(function handleError(err, req, res, next) { - throw new Error('boom!') + router.use(function handleError (err, req, res, next) { + throw err || new Error('boom!') }) request(server) - .get('/') - .expect(404, done) + .get('/') + .expect(404, done) + }) + + it('should not stack overflow with a large sync stack', function (done) { + this.timeout(5000) // long-running test + + const router = new Router() + const server = createServer(router) + + for (let i = 0; i < 6000; i++) { + router.use(function (req, res, next) { next() }) + } + + router.use(helloWorld) + + request(server) + .get('/') + .expect(200, 'hello, world', done) }) describe('error handling', function () { it('should invoke error function after next(err)', function (done) { - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) - router.use(function handle(req, res, next) { + router.use(function handle (req, res, next) { next(new Error('boom!')) }) router.use(sawError) request(server) - .get('/') - .expect(200, 'saw Error: boom!', done) + .get('/') + .expect(200, 'saw Error: boom!', done) }) it('should invoke error function after throw err', function (done) { - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) - router.use(function handle(req, res, next) { + router.use(function handle (req, res, next) { throw new Error('boom!') }) router.use(sawError) request(server) - .get('/') - .expect(200, 'saw Error: boom!', done) + .get('/') + .expect(200, 'saw Error: boom!', done) }) it('should not invoke error functions above function', function (done) { - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) router.use(sawError) - router.use(function handle(req, res, next) { + router.use(function handle (req, res, next) { throw new Error('boom!') }) request(server) - .get('/') - .expect(500, done) + .get('/') + .expect(500, done) }) }) describe('next("route")', function () { it('should invoke next handler', function (done) { - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) router.use(function handle (req, res, next) { res.setHeader('x-next', 'route') @@ -610,14 +687,14 @@ describe('Router', function () { router.use(saw) request(server) - .get('/') - .expect('x-next', 'route') - .expect(200, 'saw GET /', done) + .get('/') + .expect('x-next', 'route') + .expect(200, 'saw GET /', done) }) it('should invoke next function', function (done) { - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) function goNext (req, res, next) { res.setHeader('x-next', 'route') @@ -627,16 +704,16 @@ describe('Router', function () { router.use(createHitHandle(1), goNext, createHitHandle(2), saw) request(server) - .get('/') - .expect(shouldHitHandle(1)) - .expect('x-next', 'route') - .expect(shouldHitHandle(2)) - .expect(200, 'saw GET /', done) + .get('/') + .expect(shouldHitHandle(1)) + .expect('x-next', 'route') + .expect(shouldHitHandle(2)) + .expect(200, 'saw GET /', done) }) it('should not invoke error handlers', function (done) { - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) router.use(function handle (req, res, next) { res.setHeader('x-next', 'route') @@ -646,16 +723,16 @@ describe('Router', function () { router.use(sawError) request(server) - .get('/') - .expect('x-next', 'route') - .expect(404, done) + .get('/') + .expect('x-next', 'route') + .expect(404, done) }) }) describe('next("router")', function () { it('should exit the router', function (done) { - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) function handle (req, res, next) { res.setHeader('x-next', 'router') @@ -666,15 +743,15 @@ describe('Router', function () { router.use(saw) request(server) - .get('/') - .expect('x-next', 'router') - .expect(shouldNotHitHandle(1)) - .expect(404, done) + .get('/') + .expect('x-next', 'router') + .expect(shouldNotHitHandle(1)) + .expect(404, done) }) it('should not invoke error handlers', function (done) { - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) router.use(function handle (req, res, next) { res.setHeader('x-next', 'router') @@ -684,273 +761,497 @@ describe('Router', function () { router.use(sawError) request(server) - .get('/') - .expect('x-next', 'router') - .expect(404, done) + .get('/') + .expect('x-next', 'router') + .expect(404, done) + }) + }) + + describe('promise support', function () { + it('should pass rejected promise value', function (done) { + const router = new Router() + const server = createServer(router) + + router.use(function createError (req, res, next) { + return Promise.reject(new Error('boom!')) + }) + + router.use(sawError) + + request(server) + .get('/') + .expect(200, 'saw Error: boom!', done) + }) + + it('should pass rejected promise without value', function (done) { + const router = new Router() + const server = createServer(router) + + router.use(function createError (req, res, next) { + return Promise.reject() // eslint-disable-line prefer-promise-reject-errors + }) + + router.use(sawError) + + request(server) + .get('/') + .expect(200, 'saw Error: Rejected promise', done) + }) + + it('should ignore resolved promise', function (done) { + const router = new Router() + const server = createServer(router) + + router.use(function createError (req, res, next) { + saw(req, res) + return Promise.resolve('foo') + }) + + router.use(function () { + done(new Error('Unexpected middleware invoke')) + }) + + request(server) + .get('/foo') + .expect(200, 'saw GET /foo', done) + }) + + describe('error handling', function () { + it('should pass rejected promise value', function (done) { + const router = new Router() + const server = createServer(router) + + router.use(function createError (req, res, next) { + return Promise.reject(new Error('boom!')) + }) + + router.use(function handleError (err, req, res, next) { + return Promise.reject(new Error('caught: ' + err.message)) + }) + + router.use(sawError) + + request(server) + .get('/') + .expect(200, 'saw Error: caught: boom!', done) + }) + + it('should pass rejected promise without value', function (done) { + const router = new Router() + const server = createServer(router) + + router.use(function createError (req, res, next) { + return Promise.reject() // eslint-disable-line prefer-promise-reject-errors + }) + + router.use(function handleError (err, req, res, next) { + return Promise.reject(new Error('caught: ' + err.message)) + }) + + router.use(sawError) + + request(server) + .get('/') + .expect(200, 'saw Error: caught: Rejected promise', done) + }) + + it('should ignore resolved promise', function (done) { + const router = new Router() + const server = createServer(router) + + router.use(function createError (req, res, next) { + return Promise.reject(new Error('boom!')) + }) + + router.use(function handleError (err, req, res, next) { + sawError(err, req, res, next) + return Promise.resolve('foo') + }) + + router.use(function () { + done(new Error('Unexpected middleware invoke')) + }) + + request(server) + .get('/foo') + .expect(200, 'saw Error: boom!', done) + }) }) }) describe('req.baseUrl', function () { it('should be empty', function (done) { - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) router.use(sawBase) request(server) - .get('/foo/bar') - .expect(200, 'saw ', done) + .get('/foo/bar') + .expect(200, 'saw ', done) }) }) }) describe('.use(path, ...fn)', function () { it('should be chainable', function () { - var router = new Router() + const router = new Router() assert.equal(router.use('/', helloWorld), router) }) it('should invoke when req.url starts with path', function (done) { - var cb = after(3, done) - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) router.use('/foo', saw) - - request(server) - .get('/') - .expect(404, cb) - - request(server) - .post('/foo') - .expect(200, 'saw POST /', cb) - - request(server) - .post('/foo/bar') - .expect(200, 'saw POST /bar', cb) + series([ + function (cb) { + request(server) + .get('/') + .expect(404, cb) + }, + function (cb) { + request(server) + .post('/foo') + .expect(200, 'saw POST /', cb) + }, + function (cb) { + request(server) + .post('/foo/bar') + .expect(200, 'saw POST /bar', cb) + } + ], done) }) it('should match if path has trailing slash', function (done) { - var cb = after(3, done) - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) router.use('/foo/', saw) - request(server) - .get('/') - .expect(404, cb) - - request(server) - .post('/foo') - .expect(200, 'saw POST /', cb) - - request(server) - .post('/foo/bar') - .expect(200, 'saw POST /bar', cb) + series([ + function (cb) { + request(server) + .get('/') + .expect(404, cb) + }, + function (cb) { + request(server) + .post('/foo') + .expect(200, 'saw POST /', cb) + }, + function (cb) { + request(server) + .post('/foo/bar') + .expect(200, 'saw POST /bar', cb) + } + ], done) }) it('should support array of paths', function (done) { - var cb = after(3, done) - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) router.use(['/foo/', '/bar'], saw) - request(server) - .get('/') - .expect(404, cb) + series([ + function (cb) { + request(server) + .get('/') + .expect(404, cb) + }, + function (cb) { + request(server) + .get('/foo') + .expect(200, 'saw GET /', cb) + }, + function (cb) { + request(server) + .get('/bar') + .expect(200, 'saw GET /', cb) + } + ], done) + }) - request(server) - .get('/foo') - .expect(200, 'saw GET /', cb) + it('should support regexp path', function (done) { + const router = new Router() + const server = createServer(router) - request(server) - .get('/bar') - .expect(200, 'saw GET /', cb) + router.use(/^\/[a-z]oo$/, saw) + series([ + function (cb) { + request(server) + .get('/') + .expect(404, cb) + }, + function (cb) { + request(server) + .get('/foo') + .expect(200, 'saw GET /', cb) + }, + function (cb) { + request(server) + .get('/fooo') + .expect(404, cb) + }, + function (cb) { + request(server) + .get('/zoo/bear') + .expect(404, cb) + }, + function (cb) { + request(server) + .get('/get/zoo') + .expect(404, cb) + } + ], done) }) - it('should support regexp path', function (done) { - var cb = after(5, done) - var router = new Router() - var server = createServer(router) + it('should support regexp path with params', function (done) { + const router = new Router() + const server = createServer(router) - router.use(/^\/[a-z]oo/, saw) + router.use(/^\/([a-z]oo)$/, function (req, res, next) { + createHitHandle(req.params[0])(req, res, next) + }, saw) - request(server) - .get('/') - .expect(404, cb) + router.use(/^\/([a-z]oo)\/(?bear)$/, function (req, res, next) { + createHitHandle(req.params[0] + req.params.animal)(req, res, next) + }, saw) - request(server) - .get('/foo') - .expect(200, 'saw GET /', cb) + series([ + function (cb) { + request(server) + .get('/') + .expect(404, cb) + }, + function (cb) { + request(server) + .get('/foo') + .expect(shouldHitHandle('foo')) + .expect(200, 'saw GET /', cb) + }, + function (cb) { + request(server) + .get('/zoo') + .expect(shouldHitHandle('zoo')) + .expect(200, 'saw GET /', cb) + }, + function (cb) { + request(server) + .get('/fooo') + .expect(404, cb) + }, + function (cb) { + request(server) + .get('/zoo/bear') + .expect(shouldHitHandle('zoobear')) + .expect(200, cb) + }, + function (cb) { + request(server) + .get('/get/zoo') + .expect(404, cb) + } + ], done) + }) - request(server) - .get('/fooo') - .expect(404, cb) + it('should ensure regexp matches path prefix', function (done) { + const router = new Router() + const server = createServer(router) - request(server) - .get('/zoo/bear') - .expect(200, 'saw GET /bear', cb) + router.use(/\/api.*/, createHitHandle(1)) + router.use(/api/, createHitHandle(2)) + router.use(/\/test/, createHitHandle(3)) + router.use(helloWorld) request(server) - .get('/get/zoo') - .expect(404, cb) + .get('/test/api/1234') + .expect(shouldNotHitHandle(1)) + .expect(shouldNotHitHandle(2)) + .expect(shouldHitHandle(3)) + .expect(200, done) }) it('should support parameterized path', function (done) { - var cb = after(4, done) - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) router.use('/:thing', saw) - - request(server) - .get('/') - .expect(404, cb) - - request(server) - .get('/foo') - .expect(200, 'saw GET /', cb) - - request(server) - .get('/bar') - .expect(200, 'saw GET /', cb) - - request(server) - .get('/foo/bar') - .expect(200, 'saw GET /bar', cb) + series([ + function (cb) { + request(server) + .get('/') + .expect(404, cb) + }, + function (cb) { + request(server) + .get('/foo') + .expect(200, 'saw GET /', cb) + }, + function (cb) { + request(server) + .get('/bar') + .expect(200, 'saw GET /', cb) + }, + function (cb) { + request(server) + .get('/foo/bar') + .expect(200, 'saw GET /bar', cb) + } + ], done) }) it('should accept multiple arguments', function (done) { - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) router.use('/foo', createHitHandle(1), createHitHandle(2), helloWorld) request(server) - .get('/foo') - .expect(shouldHitHandle(1)) - .expect(shouldHitHandle(2)) - .expect(200, 'hello, world', done) + .get('/foo') + .expect(shouldHitHandle(1)) + .expect(shouldHitHandle(2)) + .expect(200, 'hello, world', done) }) describe('with "caseSensitive" option', function () { it('should not match paths case-sensitively by default', function (done) { - var cb = after(3, done) - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) router.use('/foo', saw) - - request(server) - .get('/foo/bar') - .expect(200, 'saw GET /bar', cb) - - request(server) - .get('/FOO/bar') - .expect(200, 'saw GET /bar', cb) - - request(server) - .get('/FOO/BAR') - .expect(200, 'saw GET /BAR', cb) + series([ + function (cb) { + request(server) + .get('/foo/bar') + .expect(200, 'saw GET /bar', cb) + }, + function (cb) { + request(server) + .get('/FOO/bar') + .expect(200, 'saw GET /bar', cb) + }, + function (cb) { + request(server) + .get('/FOO/BAR') + .expect(200, 'saw GET /BAR', cb) + } + ], done) }) it('should not match paths case-sensitively when false', function (done) { - var cb = after(3, done) - var router = new Router({ caseSensitive: false }) - var server = createServer(router) + const router = new Router({ caseSensitive: false }) + const server = createServer(router) router.use('/foo', saw) - - request(server) - .get('/foo/bar') - .expect(200, 'saw GET /bar', cb) - - request(server) - .get('/FOO/bar') - .expect(200, 'saw GET /bar', cb) - - request(server) - .get('/FOO/BAR') - .expect(200, 'saw GET /BAR', cb) + series([ + function (cb) { + request(server) + .get('/foo/bar') + .expect(200, 'saw GET /bar', cb) + }, + function (cb) { + request(server) + .get('/FOO/bar') + .expect(200, 'saw GET /bar', cb) + }, + function (cb) { + request(server) + .get('/FOO/BAR') + .expect(200, 'saw GET /BAR', cb) + } + ], done) }) it('should match paths case-sensitively when true', function (done) { - var cb = after(3, done) - var router = new Router({ caseSensitive: true }) - var server = createServer(router) + const router = new Router({ caseSensitive: true }) + const server = createServer(router) router.use('/foo', saw) - - request(server) - .get('/foo/bar') - .expect(200, 'saw GET /bar', cb) - - request(server) - .get('/FOO/bar') - .expect(404, cb) - - request(server) - .get('/FOO/BAR') - .expect(404, cb) + series([ + function (cb) { + request(server) + .get('/foo/bar') + .expect(200, 'saw GET /bar', cb) + }, + function (cb) { + request(server) + .get('/FOO/bar') + .expect(404, cb) + }, + function (cb) { + request(server) + .get('/FOO/BAR') + .expect(404, cb) + } + ], done) }) }) describe('with "strict" option', function () { it('should accept optional trailing slashes by default', function (done) { - var cb = after(2, done) - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) router.use('/foo', saw) - - request(server) - .get('/foo') - .expect(200, 'saw GET /', cb) - - request(server) - .get('/foo/') - .expect(200, 'saw GET /', cb) + series([ + function (cb) { + request(server) + .get('/foo') + .expect(200, 'saw GET /', cb) + }, + function (cb) { + request(server) + .get('/foo/') + .expect(200, 'saw GET /', cb) + } + ], done) }) it('should accept optional trailing slashes when false', function (done) { - var cb = after(2, done) - var router = new Router({ strict: false }) - var server = createServer(router) + const router = new Router({ strict: false }) + const server = createServer(router) router.use('/foo', saw) - - request(server) - .get('/foo') - .expect(200, 'saw GET /', cb) - - request(server) - .get('/foo/') - .expect(200, 'saw GET /', cb) + series([ + function (cb) { + request(server) + .get('/foo') + .expect(200, 'saw GET /', cb) + }, + function (cb) { + request(server) + .get('/foo/') + .expect(200, 'saw GET /', cb) + } + ], done) }) it('should accept optional trailing slashes when true', function (done) { - var cb = after(2, done) - var router = new Router({ strict: true }) - var server = createServer(router) + const router = new Router({ strict: true }) + const server = createServer(router) router.use('/foo', saw) - - request(server) - .get('/foo') - .expect(200, 'saw GET /', cb) - - request(server) - .get('/foo/') - .expect(200, 'saw GET /', cb) + series([ + function (cb) { + request(server) + .get('/foo') + .expect(200, 'saw GET /', cb) + }, + function (cb) { + request(server) + .get('/foo/') + .expect(200, 'saw GET /', cb) + } + ], done) }) }) describe('next("route")', function () { it('should invoke next handler', function (done) { - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) router.use('/foo', function handle (req, res, next) { res.setHeader('x-next', 'route') @@ -960,14 +1261,14 @@ describe('Router', function () { router.use('/foo', saw) request(server) - .get('/foo') - .expect('x-next', 'route') - .expect(200, 'saw GET /', done) + .get('/foo') + .expect('x-next', 'route') + .expect(200, 'saw GET /', done) }) it('should invoke next function', function (done) { - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) function goNext (req, res, next) { res.setHeader('x-next', 'route') @@ -977,45 +1278,58 @@ describe('Router', function () { router.use('/foo', createHitHandle(1), goNext, createHitHandle(2), saw) request(server) - .get('/foo') - .expect(shouldHitHandle(1)) - .expect('x-next', 'route') - .expect(shouldHitHandle(2)) - .expect(200, 'saw GET /', done) + .get('/foo') + .expect(shouldHitHandle(1)) + .expect('x-next', 'route') + .expect(shouldHitHandle(2)) + .expect(200, 'saw GET /', done) }) }) describe('req.baseUrl', function () { it('should contain the stripped path', function (done) { - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) router.use('/foo', sawBase) request(server) - .get('/foo/bar') - .expect(200, 'saw /foo', done) + .get('/foo/bar') + .expect(200, 'saw /foo', done) }) it('should contain the stripped path for multiple levels', function (done) { - var router1 = new Router() - var router2 = new Router() - var server = createServer(router1) + const router1 = new Router() + const router2 = new Router() + const server = createServer(router1) router1.use('/foo', router2) router2.use('/bar', sawBase) request(server) - .get('/foo/bar/baz') - .expect(200, 'saw /foo/bar', done) + .get('/foo/bar/baz') + .expect(200, 'saw /foo/bar', done) }) - it('should be altered correctly', function(done){ - var router = new Router() - var server = createServer(router) - var sub1 = new Router() - var sub2 = new Router() - var sub3 = new Router() + it('should contain the stripped path for multiple levels with regular expressions', function (done) { + const router1 = new Router() + const router2 = new Router() + const server = createServer(router1) + + router1.use(/^\/foo/, router2) + router2.use(/^\/bar/, sawBase) + + request(server) + .get('/foo/bar/baz') + .expect(200, 'saw /foo/bar', done) + }) + + it('should be altered correctly', function (done) { + const router = new Router() + const server = createServer(router) + const sub1 = new Router() + const sub2 = new Router() + const sub3 = new Router() sub3.get('/zed', setsawBase(1)) @@ -1032,60 +1346,60 @@ describe('Router', function () { router.use(helloWorld) request(server) - .get('/foo/bar/baz/zed') - .expect('x-saw-base-1', '/foo/bar/baz') - .expect('x-saw-base-2', '/foo') - .expect('x-saw-base-3', '/foo/bar') - .expect('x-saw-base-4', '') - .expect('x-saw-base-5', '') - .expect(200, done) + .get('/foo/bar/baz/zed') + .expect('x-saw-base-1', '/foo/bar/baz') + .expect('x-saw-base-2', '/foo') + .expect('x-saw-base-3', '/foo/bar') + .expect('x-saw-base-4', '') + .expect('x-saw-base-5', '') + .expect(200, done) }) }) describe('req.url', function () { it('should strip path from req.url', function (done) { - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) router.use('/foo', saw) request(server) - .get('/foo/bar') - .expect(200, 'saw GET /bar', done) + .get('/foo/bar') + .expect(200, 'saw GET /bar', done) }) it('should restore req.url after stripping', function (done) { - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) router.use('/foo', setsaw(1)) router.use(saw) request(server) - .get('/foo/bar') - .expect('x-saw-1', 'GET /bar') - .expect(200, 'saw GET /foo/bar', done) + .get('/foo/bar') + .expect('x-saw-1', 'GET /bar') + .expect(200, 'saw GET /foo/bar', done) }) it('should strip/restore with trailing stash', function (done) { - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) router.use('/foo', setsaw(1)) router.use(saw) request(server) - .get('/foo/') - .expect('x-saw-1', 'GET /') - .expect(200, 'saw GET /foo/', done) + .get('/foo/') + .expect('x-saw-1', 'GET /') + .expect(200, 'saw GET /foo/', done) }) }) }) describe('request rewriting', function () { it('should support altering req.method', function (done) { - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) router.put('/foo', createHitHandle(1)) router.post('/foo', createHitHandle(2), function (req, res, next) { @@ -1098,17 +1412,17 @@ describe('Router', function () { router.use(saw) request(server) - .post('/foo') - .expect(shouldNotHitHandle(1)) - .expect(shouldHitHandle(2)) - .expect(shouldNotHitHandle(3)) - .expect(shouldHitHandle(4)) - .expect(200, 'saw PUT /foo', done) + .post('/foo') + .expect(shouldNotHitHandle(1)) + .expect(shouldHitHandle(2)) + .expect(shouldNotHitHandle(3)) + .expect(shouldHitHandle(4)) + .expect(200, 'saw PUT /foo', done) }) it('should support altering req.url', function (done) { - var router = new Router() - var server = createServer(router) + const router = new Router() + const server = createServer(router) router.get('/bar', createHitHandle(1)) router.get('/foo', createHitHandle(2), function (req, res, next) { @@ -1121,54 +1435,54 @@ describe('Router', function () { router.use(saw) request(server) - .get('/foo') - .expect(shouldNotHitHandle(1)) - .expect(shouldHitHandle(2)) - .expect(shouldNotHitHandle(3)) - .expect(shouldHitHandle(4)) - .expect(200, 'saw GET /bar', done) + .get('/foo') + .expect(shouldNotHitHandle(1)) + .expect(shouldHitHandle(2)) + .expect(shouldNotHitHandle(3)) + .expect(shouldHitHandle(4)) + .expect(200, 'saw GET /bar', done) }) }) }) -function helloWorld(req, res) { +function helloWorld (req, res) { res.statusCode = 200 res.setHeader('Content-Type', 'text/plain') res.end('hello, world') } -function setsaw(num) { - var name = 'x-saw-' + String(num) - return function saw(req, res, next) { +function setsaw (num) { + const name = 'x-saw-' + String(num) + return function saw (req, res, next) { res.setHeader(name, req.method + ' ' + req.url) next() } } -function setsawBase(num) { - var name = 'x-saw-base-' + String(num) - return function sawBase(req, res, next) { +function setsawBase (num) { + const name = 'x-saw-base-' + String(num) + return function sawBase (req, res, next) { res.setHeader(name, String(req.baseUrl)) next() } } -function saw(req, res) { - var msg = 'saw ' + req.method + ' ' + req.url +function saw (req, res) { + const msg = 'saw ' + req.method + ' ' + req.url res.statusCode = 200 res.setHeader('Content-Type', 'text/plain') res.end(msg) } -function sawError(err, req, res, next) { - var msg = 'saw ' + err.name + ': ' + err.message +function sawError (err, req, res, next) { + const msg = 'saw ' + err.name + ': ' + err.message res.statusCode = 200 res.setHeader('Content-Type', 'text/plain') res.end(msg) } -function sawBase(req, res) { - var msg = 'saw ' + req.baseUrl +function sawBase (req, res) { + const msg = 'saw ' + req.baseUrl res.statusCode = 200 res.setHeader('Content-Type', 'text/plain') res.end(msg) diff --git a/test/support/utils.js b/test/support/utils.js index 8841e418..2fab2fbb 100644 --- a/test/support/utils.js +++ b/test/support/utils.js @@ -1,49 +1,53 @@ +const assert = require('assert') +const finalhandler = require('finalhandler') +const http = require('http') +const { METHODS } = require('node:http') +const request = require('supertest') -var assert = require('assert') -var finalhandler = require('finalhandler') -var http = require('http') -var methods = require('methods') -var request = require('supertest') +const methods = METHODS.map((method) => method.toLowerCase()) exports.assert = assert exports.createHitHandle = createHitHandle exports.createServer = createServer exports.rawrequest = rawrequest exports.request = request +exports.shouldHaveBody = shouldHaveBody +exports.shouldNotHaveBody = shouldNotHaveBody exports.shouldHitHandle = shouldHitHandle exports.shouldNotHitHandle = shouldNotHitHandle +exports.methods = methods -function createHitHandle(num) { - var name = 'x-fn-' + String(num) - return function hit(req, res, next) { +function createHitHandle (num) { + const name = 'x-fn-' + String(num) + return function hit (req, res, next) { res.setHeader(name, 'hit') next() } } -function createServer(router) { - return http.createServer(function onRequest(req, res) { +function createServer (router) { + return http.createServer(function onRequest (req, res) { router(req, res, finalhandler(req, res)) }) } -function rawrequest(server) { - var _headers = {} - var _method - var _path - var _test = {} +function rawrequest (server) { + const _headers = {} + let _method + let _path + const _test = {} methods.forEach(function (method) { _test[method] = go.bind(null, method) }) - function expect(status, body, callback) { + function expect (status, body, callback) { if (arguments.length === 2) { _headers[status.toLowerCase()] = body return this } - var _server + let _server if (!server.address()) { _server = server.listen(0, onListening) @@ -53,25 +57,25 @@ function rawrequest(server) { onListening.call(server) function onListening () { - var addr = this.address() - var port = addr.port + const addr = this.address() + const port = addr.port - var req = http.request({ + const req = http.request({ host: '127.0.0.1', method: _method, path: _path, - port: port + port }) - req.on('response', function(res){ - var buf = '' + req.on('response', function (res) { + let buf = '' res.setEncoding('utf8') - res.on('data', function(s){ buf += s }) - res.on('end', function(){ - var err = null + res.on('data', function (s) { buf += s }) + res.on('end', function () { + let err = null try { - for (var key in _headers) { + for (const key in _headers) { assert.equal(res.headers[key], _headers[key]) } @@ -97,25 +101,41 @@ function rawrequest(server) { _path = path return { - expect: expect + expect } } return _test } -function shouldHitHandle(num) { - var header = 'x-fn-' + String(num) +function shouldHaveBody (buf) { + return function (res) { + const 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')) + } +} + +function shouldHitHandle (num) { + const header = 'x-fn-' + String(num) return function (res) { assert.equal(res.headers[header], 'hit', 'should hit handle ' + num) } } -function shouldNotHitHandle(num) { +function shouldNotHaveBody () { + return function (res) { + assert.ok(res.text === '' || res.text === undefined) + } +} + +function shouldNotHitHandle (num) { return shouldNotHaveHeader('x-fn-' + String(num)) } -function shouldNotHaveHeader(header) { +function shouldNotHaveHeader (header) { return function (res) { assert.ok(!(header.toLowerCase() in res.headers), 'should not have header ' + header) }