diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 86d2df3..14fc857 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,9 +10,8 @@ jobs: os: - ubuntu-latest node_version: - - 14 - - 16 - - 18 + - 20 + - 22 name: Node ${{ matrix.node_version }} on ${{ matrix.os }} steps: - uses: actions/checkout@v3 diff --git a/.xo-config.js b/.xo-config.js index 6163131..2bcc6f1 100644 --- a/.xo-config.js +++ b/.xo-config.js @@ -32,6 +32,8 @@ module.exports = { 'unicorn/explicit-length-check': 'warn', 'unicorn/no-array-reduce': 'warn', 'unicorn/prefer-spread': 'warn', - 'unicorn/prefer-node-protocol': 'off' + 'unicorn/prefer-node-protocol': 'off', + 'unicorn/expiring-todo-comments': 'off', + 'max-nested-callbacks': 'off' } }; diff --git a/README.md b/README.md index b1a6cdd..329e7ec 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ See [API Reference](./API.md) for more documentation. | ---------------- | | **Alex Mingoia** | | **@koajs** | +| **Imed Jaberi** | ## License diff --git a/bench/server.js b/bench/server.js index ae4b5af..dd73a88 100644 --- a/bench/server.js +++ b/bench/server.js @@ -1,15 +1,20 @@ -const Koa = require('koa'); -const Router = require('../'); +const process = require('node:process'); const env = require('@ladjs/env')({ path: '../.env', includeProcessEnv: true, assignToProcessEnv: true }); +const Koa = require('koa'); + +const Router = require('../'); const app = new Koa(); const router = new Router(); -const ok = (ctx) => (ctx.status = 200); +const ok = (ctx) => { + ctx.status = 200; +}; + const n = Number.parseInt(env.FACTOR || '10', 10); const useMiddleware = env.USE_MIDDLEWARE === 'true'; diff --git a/lib/layer.js b/lib/layer.js index 7cddcbe..06f2ed6 100644 --- a/lib/layer.js +++ b/lib/layer.js @@ -1,229 +1,223 @@ -const { parse: parseUrl, format: formatUrl } = require('url'); +const { parse: parseUrl, format: formatUrl } = require('node:url'); + const { pathToRegexp, compile, parse } = require('path-to-regexp'); -module.exports = Layer; +module.exports = class Layer { + /** + * Initialize a new routing Layer with given `method`, `path`, and `middleware`. + * + * @param {String|RegExp} path Path string or regular expression. + * @param {Array} methods Array of HTTP verbs. + * @param {Array} middleware Layer callback/middleware or series of. + * @param {Object=} opts + * @param {String=} opts.name route name + * @param {String=} opts.sensitive case sensitive (default: false) + * @param {String=} opts.strict require the trailing slash (default: false) + * @returns {Layer} + * @private + */ + constructor(path, methods, middleware, opts = {}) { + this.opts = opts; + this.name = this.opts.name || null; + this.methods = []; + this.paramNames = []; + this.stack = Array.isArray(middleware) ? middleware : [middleware]; -/** - * Initialize a new routing Layer with given `method`, `path`, and `middleware`. - * - * @param {String|RegExp} path Path string or regular expression. - * @param {Array} methods Array of HTTP verbs. - * @param {Array} middleware Layer callback/middleware or series of. - * @param {Object=} opts - * @param {String=} opts.name route name - * @param {String=} opts.sensitive case sensitive (default: false) - * @param {String=} opts.strict require the trailing slash (default: false) - * @returns {Layer} - * @private - */ + for (const method of methods) { + const l = this.methods.push(method.toUpperCase()); + if (this.methods[l - 1] === 'GET') this.methods.unshift('HEAD'); + } -function Layer(path, methods, middleware, opts = {}) { - this.opts = opts; - this.name = this.opts.name || null; - this.methods = []; - this.paramNames = []; - this.stack = Array.isArray(middleware) ? middleware : [middleware]; + // ensure middleware is a function + for (let i = 0; i < this.stack.length; i++) { + const fn = this.stack[i]; + const type = typeof fn; + if (type !== 'function') + throw new Error( + `${methods.toString()} \`${ + this.opts.name || path + }\`: \`middleware\` must be a function, not \`${type}\`` + ); + } - for (const method of methods) { - const l = this.methods.push(method.toUpperCase()); - if (this.methods[l - 1] === 'GET') this.methods.unshift('HEAD'); + this.path = path; + this.regexp = pathToRegexp(path, this.paramNames, this.opts); } - // ensure middleware is a function - for (let i = 0; i < this.stack.length; i++) { - const fn = this.stack[i]; - const type = typeof fn; - if (type !== 'function') - throw new Error( - `${methods.toString()} \`${ - this.opts.name || path - }\`: \`middleware\` must be a function, not \`${type}\`` - ); + /** + * Returns whether request `path` matches route. + * + * @param {String} path + * @returns {Boolean} + * @private + */ + match(path) { + return this.regexp.test(path); } - this.path = path; - this.regexp = pathToRegexp(path, this.paramNames, this.opts); -} - -/** - * Returns whether request `path` matches route. - * - * @param {String} path - * @returns {Boolean} - * @private - */ - -Layer.prototype.match = function (path) { - return this.regexp.test(path); -}; - -/** - * Returns map of URL parameters for given `path` and `paramNames`. - * - * @param {String} path - * @param {Array.} captures - * @param {Object=} params - * @returns {Object} - * @private - */ - -Layer.prototype.params = function (path, captures, params = {}) { - for (let len = captures.length, i = 0; i < len; i++) { - if (this.paramNames[i]) { - const c = captures[i]; - if (c && c.length > 0) - params[this.paramNames[i].name] = c ? safeDecodeURIComponent(c) : c; + /** + * Returns map of URL parameters for given `path` and `paramNames`. + * + * @param {String} path + * @param {Array.} captures + * @param {Object=} params + * @returns {Object} + * @private + */ + params(path, captures, params = {}) { + for (let len = captures.length, i = 0; i < len; i++) { + if (this.paramNames[i]) { + const c = captures[i]; + if (c && c.length > 0) + params[this.paramNames[i].name] = c ? safeDecodeURIComponent(c) : c; + } } - } - - return params; -}; -/** - * Returns array of regexp url path captures. - * - * @param {String} path - * @returns {Array.} - * @private - */ - -Layer.prototype.captures = function (path) { - return this.opts.ignoreCaptures ? [] : path.match(this.regexp).slice(1); -}; - -/** - * Generate URL for route using given `params`. - * - * @example - * - * ```javascript - * const route = new Layer('/users/:id', ['GET'], fn); - * - * route.url({ id: 123 }); // => "/users/123" - * ``` - * - * @param {Object} params url parameters - * @returns {String} - * @private - */ + return params; + } -Layer.prototype.url = function (params, options) { - let args = params; - const url = this.path.replace(/\(\.\*\)/g, ''); + /** + * Returns array of regexp url path captures. + * + * @param {String} path + * @returns {Array.} + * @private + */ + captures(path) { + return this.opts.ignoreCaptures ? [] : path.match(this.regexp).slice(1); + } - if (typeof params !== 'object') { - args = Array.prototype.slice.call(arguments); - if (typeof args[args.length - 1] === 'object') { - options = args[args.length - 1]; - args = args.slice(0, -1); + /** + * Generate URL for route using given `params`. + * + * @example + * + * ```javascript + * const route = new Layer('/users/:id', ['GET'], fn); + * + * route.url({ id: 123 }); // => "/users/123" + * ``` + * + * @param {Object} params url parameters + * @returns {String} + * @private + */ + url(params, options) { + let args = params; + const url = this.path.replace(/\(\.\*\)/g, ''); + + if (typeof params !== 'object') { + args = Array.prototype.slice.call(arguments); + if (typeof args[args.length - 1] === 'object') { + options = args[args.length - 1]; + args = args.slice(0, -1); + } } - } - const toPath = compile(url, { encode: encodeURIComponent, ...options }); - let replaced; + const toPath = compile(url, { encode: encodeURIComponent, ...options }); + let replaced; - const tokens = parse(url); - let replace = {}; + const tokens = parse(url); + let replace = {}; - if (Array.isArray(args)) { - for (let len = tokens.length, i = 0, j = 0; i < len; i++) { - if (tokens[i].name) replace[tokens[i].name] = args[j++]; + if (Array.isArray(args)) { + for (let len = tokens.length, i = 0, j = 0; i < len; i++) { + if (tokens[i].name) replace[tokens[i].name] = args[j++]; + } + } else if (tokens.some((token) => token.name)) { + replace = params; + } else if (!options) { + options = params; } - } else if (tokens.some((token) => token.name)) { - replace = params; - } else if (!options) { - options = params; - } - replaced = toPath(replace); + replaced = toPath(replace); + + if (options && options.query) { + replaced = parseUrl(replaced); + if (typeof options.query === 'string') { + replaced.search = options.query; + } else { + replaced.search = undefined; + replaced.query = options.query; + } - if (options && options.query) { - replaced = parseUrl(replaced); - if (typeof options.query === 'string') { - replaced.search = options.query; - } else { - replaced.search = undefined; - replaced.query = options.query; + return formatUrl(replaced); } - return formatUrl(replaced); + return replaced; } - return replaced; -}; + /** + * Run validations on route named parameters. + * + * @example + * + * ```javascript + * router + * .param('user', function (id, ctx, next) { + * ctx.user = users[id]; + * if (!ctx.user) return ctx.status = 404; + * next(); + * }) + * .get('/users/:user', function (ctx, next) { + * ctx.body = ctx.user; + * }); + * ``` + * + * @param {String} param + * @param {Function} middleware + * @returns {Layer} + * @private + */ + param(param, fn) { + const { stack } = this; + const params = this.paramNames; + const middleware = function (ctx, next) { + return fn.call(this, ctx.params[param], ctx, next); + }; + + middleware.param = param; + + const names = params.map(function (p) { + return p.name; + }); -/** - * Run validations on route named parameters. - * - * @example - * - * ```javascript - * router - * .param('user', function (id, ctx, next) { - * ctx.user = users[id]; - * if (!ctx.user) return ctx.status = 404; - * next(); - * }) - * .get('/users/:user', function (ctx, next) { - * ctx.body = ctx.user; - * }); - * ``` - * - * @param {String} param - * @param {Function} middleware - * @returns {Layer} - * @private - */ + const x = names.indexOf(param); + if (x > -1) { + // iterate through the stack, to figure out where to place the handler fn + stack.some((fn, i) => { + // param handlers are always first, so when we find an fn w/o a param property, stop here + // if the param handler at this part of the stack comes after the one we are adding, stop here + if (!fn.param || names.indexOf(fn.param) > x) { + // inject this param handler right before the current item + stack.splice(i, 0, middleware); + return true; // then break the loop + } + }); + } -Layer.prototype.param = function (param, fn) { - const { stack } = this; - const params = this.paramNames; - const middleware = function (ctx, next) { - return fn.call(this, ctx.params[param], ctx, next); - }; - - middleware.param = param; - - const names = params.map(function (p) { - return p.name; - }); - - const x = names.indexOf(param); - if (x > -1) { - // iterate through the stack, to figure out where to place the handler fn - stack.some(function (fn, i) { - // param handlers are always first, so when we find an fn w/o a param property, stop here - // if the param handler at this part of the stack comes after the one we are adding, stop here - if (!fn.param || names.indexOf(fn.param) > x) { - // inject this param handler right before the current item - stack.splice(i, 0, middleware); - return true; // then break the loop - } - }); + return this; } - return this; -}; - -/** - * Prefix route path. - * - * @param {String} prefix - * @returns {Layer} - * @private - */ + /** + * Prefix route path. + * + * @param {String} prefix + * @returns {Layer} + * @private + */ + setPrefix(prefix) { + if (this.path) { + this.path = + this.path !== '/' || this.opts.strict === true + ? `${prefix}${this.path}` + : prefix; + this.paramNames = []; + this.regexp = pathToRegexp(this.path, this.paramNames, this.opts); + } -Layer.prototype.setPrefix = function (prefix) { - if (this.path) { - this.path = - this.path !== '/' || this.opts.strict === true - ? `${prefix}${this.path}` - : prefix; - this.paramNames = []; - this.regexp = pathToRegexp(this.path, this.paramNames, this.opts); + return this; } - - return this; }; /** @@ -237,7 +231,8 @@ Layer.prototype.setPrefix = function (prefix) { function safeDecodeURIComponent(text) { try { - return decodeURIComponent(text); + // @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#decoding_query_parameters_from_a_url + return decodeURIComponent(text.replace(/\+/g, ' ')); } catch { return text; } diff --git a/lib/router.js b/lib/router.js index ece7ac5..748520b 100644 --- a/lib/router.js +++ b/lib/router.js @@ -4,69 +4,652 @@ * @author Alex Mingoia * @link https://github.com/alexmingoia/koa-router */ +const http = require('node:http'); const compose = require('koa-compose'); const HttpError = require('http-errors'); -const methods = require('methods'); +const debug = require('debug')('koa-router'); const { pathToRegexp } = require('path-to-regexp'); + const Layer = require('./layer'); -const debug = require('debug')('koa-router'); + +const methods = http.METHODS.map((method) => method.toLowerCase()); /** * @module koa-router */ +class Router { + /** + * Create a new router. + * + * @example + * + * Basic usage: + * + * ```javascript + * const Koa = require('koa'); + * const Router = require('@koa/router'); + * + * const app = new Koa(); + * const router = new Router(); + * + * router.get('/', (ctx, next) => { + * // ctx.router available + * }); + * + * app + * .use(router.routes()) + * .use(router.allowedMethods()); + * ``` + * + * @alias module:koa-router + * @param {Object=} opts + * @param {Boolean=false} opts.exclusive only run last matched route's controller when there are multiple matches + * @param {String=} opts.prefix prefix router paths + * @param {String|RegExp=} opts.host host for router match + * @constructor + */ + constructor(opts = {}) { + if (!(this instanceof Router)) return new Router(opts); // eslint-disable-line no-constructor-return + + this.opts = opts; + this.methods = this.opts.methods || [ + 'HEAD', + 'OPTIONS', + 'GET', + 'PUT', + 'PATCH', + 'POST', + 'DELETE' + ]; + this.exclusive = Boolean(this.opts.exclusive); + + this.params = {}; + this.stack = []; + this.host = this.opts.host; + } -module.exports = Router; + /** + * Generate URL from url pattern and given `params`. + * + * @example + * + * ```javascript + * const url = Router.url('/users/:id', {id: 1}); + * // => "/users/1" + * ``` + * + * @param {String} path url pattern + * @param {Object} params url parameters + * @returns {String} + */ + static url(path, ...args) { + return Layer.prototype.url.apply({ path }, args); + } -/** - * Create a new router. - * - * @example - * - * Basic usage: - * - * ```javascript - * const Koa = require('koa'); - * const Router = require('@koa/router'); - * - * const app = new Koa(); - * const router = new Router(); - * - * router.get('/', (ctx, next) => { - * // ctx.router available - * }); - * - * app - * .use(router.routes()) - * .use(router.allowedMethods()); - * ``` - * - * @alias module:koa-router - * @param {Object=} opts - * @param {Boolean=false} opts.exclusive only run last matched route's controller when there are multiple matches - * @param {String=} opts.prefix prefix router paths - * @param {String|RegExp=} opts.host host for router match - * @constructor - */ + /** + * Use given middleware. + * + * Middleware run in the order they are defined by `.use()`. They are invoked + * sequentially, requests start at the first middleware and work their way + * "down" the middleware stack. + * + * @example + * + * ```javascript + * // session middleware will run before authorize + * router + * .use(session()) + * .use(authorize()); + * + * // use middleware only with given path + * router.use('/users', userAuth()); + * + * // or with an array of paths + * router.use(['/users', '/admin'], userAuth()); + * + * app.use(router.routes()); + * ``` + * + * @param {String=} path + * @param {Function} middleware + * @param {Function=} ... + * @returns {Router} + */ + use(...middleware) { + const router = this; + let path; + + // support array of paths + if (Array.isArray(middleware[0]) && typeof middleware[0][0] === 'string') { + const arrPaths = middleware[0]; + for (const p of arrPaths) { + router.use.apply(router, [p, ...middleware.slice(1)]); + } + + return this; + } + + const hasPath = typeof middleware[0] === 'string'; + if (hasPath) path = middleware.shift(); + + for (const m of middleware) { + if (m.router) { + const cloneRouter = Object.assign( + Object.create(Router.prototype), + m.router, + { + stack: [...m.router.stack] + } + ); + + for (let j = 0; j < cloneRouter.stack.length; j++) { + const nestedLayer = cloneRouter.stack[j]; + const cloneLayer = Object.assign( + Object.create(Layer.prototype), + nestedLayer + ); + + if (path) cloneLayer.setPrefix(path); + if (router.opts.prefix) cloneLayer.setPrefix(router.opts.prefix); + router.stack.push(cloneLayer); + cloneRouter.stack[j] = cloneLayer; + } + + if (router.params) { + const routerParams = Object.keys(router.params); + for (const key of routerParams) { + cloneRouter.param(key, router.params[key]); + } + } + } else { + const keys = []; + pathToRegexp(router.opts.prefix || '', keys); + const routerPrefixHasParam = Boolean( + router.opts.prefix && keys.length > 0 + ); + router.register(path || '([^/]*)', [], m, { + end: false, + ignoreCaptures: !hasPath && !routerPrefixHasParam + }); + } + } + + return this; + } + + /** + * Set the path prefix for a Router instance that was already initialized. + * + * @example + * + * ```javascript + * router.prefix('/things/:thing_id') + * ``` + * + * @param {String} prefix + * @returns {Router} + */ + prefix(prefix) { + prefix = prefix.replace(/\/$/, ''); + + this.opts.prefix = prefix; + + for (let i = 0; i < this.stack.length; i++) { + const route = this.stack[i]; + route.setPrefix(prefix); + } + + return this; + } + + /** + * Returns router middleware which dispatches a route matching the request. + * + * @returns {Function} + */ + middleware() { + const router = this; + const dispatch = (ctx, next) => { + debug('%s %s', ctx.method, ctx.path); + + const hostMatched = router.matchHost(ctx.host); + + if (!hostMatched) { + return next(); + } + + const path = + router.opts.routerPath || + ctx.newRouterPath || + ctx.path || + ctx.routerPath; + const matched = router.match(path, ctx.method); + if (ctx.matched) { + ctx.matched.push(matched.path); + } else { + ctx.matched = matched.path; + } + + ctx.router = router; + + if (!matched.route) return next(); + + const matchedLayers = matched.pathAndMethod; + const mostSpecificLayer = matchedLayers[matchedLayers.length - 1]; + ctx._matchedRoute = mostSpecificLayer.path; + if (mostSpecificLayer.name) { + ctx._matchedRouteName = mostSpecificLayer.name; + } + + const layerChain = ( + router.exclusive ? [mostSpecificLayer] : matchedLayers + ).reduce((memo, layer) => { + memo.push((ctx, next) => { + ctx.captures = layer.captures(path, ctx.captures); + ctx.request.params = layer.params(path, ctx.captures, ctx.params); + ctx.params = ctx.request.params; + ctx.routerPath = layer.path; + ctx.routerName = layer.name; + ctx._matchedRoute = layer.path; + if (layer.name) { + ctx._matchedRouteName = layer.name; + } + + return next(); + }); + return [...memo, ...layer.stack]; + }, []); + + return compose(layerChain)(ctx, next); + }; + + dispatch.router = this; + + return dispatch; + } + + routes() { + return this.middleware(); + } + + /** + * Returns separate middleware for responding to `OPTIONS` requests with + * an `Allow` header containing the allowed methods, as well as responding + * with `405 Method Not Allowed` and `501 Not Implemented` as appropriate. + * + * @example + * + * ```javascript + * const Koa = require('koa'); + * const Router = require('@koa/router'); + * + * const app = new Koa(); + * const router = new Router(); + * + * app.use(router.routes()); + * app.use(router.allowedMethods()); + * ``` + * + * **Example with [Boom](https://github.com/hapijs/boom)** + * + * ```javascript + * const Koa = require('koa'); + * const Router = require('@koa/router'); + * const Boom = require('boom'); + * + * const app = new Koa(); + * const router = new Router(); + * + * app.use(router.routes()); + * app.use(router.allowedMethods({ + * throw: true, + * notImplemented: () => new Boom.notImplemented(), + * methodNotAllowed: () => new Boom.methodNotAllowed() + * })); + * ``` + * + * @param {Object=} options + * @param {Boolean=} options.throw throw error instead of setting status and header + * @param {Function=} options.notImplemented throw the returned value in place of the default NotImplemented error + * @param {Function=} options.methodNotAllowed throw the returned value in place of the default MethodNotAllowed error + * @returns {Function} + */ + allowedMethods(options = {}) { + const implemented = this.methods; + + return (ctx, next) => { + return next().then(() => { + const allowed = {}; + + if (ctx.matched && (!ctx.status || ctx.status === 404)) { + for (let i = 0; i < ctx.matched.length; i++) { + const route = ctx.matched[i]; + for (let j = 0; j < route.methods.length; j++) { + const method = route.methods[j]; + allowed[method] = method; + } + } + + const allowedArr = Object.keys(allowed); + if (!implemented.includes(ctx.method)) { + if (options.throw) { + const notImplementedThrowable = + typeof options.notImplemented === 'function' + ? options.notImplemented() // set whatever the user returns from their function + : new HttpError.NotImplemented(); + + throw notImplementedThrowable; + } else { + ctx.status = 501; + ctx.set('Allow', allowedArr.join(', ')); + } + } else if (allowedArr.length > 0) { + if (ctx.method === 'OPTIONS') { + ctx.status = 200; + ctx.body = ''; + ctx.set('Allow', allowedArr.join(', ')); + } else if (!allowed[ctx.method]) { + if (options.throw) { + const notAllowedThrowable = + typeof options.methodNotAllowed === 'function' + ? options.methodNotAllowed() // set whatever the user returns from their function + : new HttpError.MethodNotAllowed(); + + throw notAllowedThrowable; + } else { + ctx.status = 405; + ctx.set('Allow', allowedArr.join(', ')); + } + } + } + } + }); + }; + } + + /** + * Register route with all methods. + * + * @param {String} name Optional. + * @param {String} path + * @param {Function=} middleware You may also pass multiple middleware. + * @param {Function} callback + * @returns {Router} + */ + all(name, path, middleware) { + if (typeof path === 'string') { + middleware = Array.prototype.slice.call(arguments, 2); + } else { + middleware = Array.prototype.slice.call(arguments, 1); + path = name; + name = null; + } + + // Sanity check to ensure we have a viable path candidate (eg: string|regex|non-empty array) + if ( + typeof path !== 'string' && + !(path instanceof RegExp) && + (!Array.isArray(path) || path.length === 0) + ) + throw new Error('You have to provide a path when adding an all handler'); + + this.register(path, methods, middleware, { name }); + + return this; + } + + /** + * Redirect `source` to `destination` URL with optional 30x status `code`. + * + * Both `source` and `destination` can be route names. + * + * ```javascript + * router.redirect('/login', 'sign-in'); + * ``` + * + * This is equivalent to: + * + * ```javascript + * router.all('/login', ctx => { + * ctx.redirect('/sign-in'); + * ctx.status = 301; + * }); + * ``` + * + * @param {String} source URL or route name. + * @param {String} destination URL or route name. + * @param {Number=} code HTTP status code (default: 301). + * @returns {Router} + */ + redirect(source, destination, code) { + // lookup source route by name + if (typeof source === 'symbol' || source[0] !== '/') { + source = this.url(source); + if (source instanceof Error) throw source; + } + + // lookup destination route by name + if ( + typeof destination === 'symbol' || + (destination[0] !== '/' && !destination.includes('://')) + ) { + destination = this.url(destination); + if (destination instanceof Error) throw destination; + } + + return this.all(source, (ctx) => { + ctx.redirect(destination); + ctx.status = code || 301; + }); + } + + /** + * Create and register a route. + * + * @param {String} path Path string. + * @param {Array.} methods Array of HTTP verbs. + * @param {Function} middleware Multiple middleware also accepted. + * @returns {Layer} + * @private + */ + register(path, methods, middleware, opts = {}) { + const router = this; + const { stack } = this; + + // support array of paths + if (Array.isArray(path)) { + for (const curPath of path) { + router.register.call(router, curPath, methods, middleware, opts); + } -function Router(opts = {}) { - if (!(this instanceof Router)) return new Router(opts); - - this.opts = opts; - this.methods = this.opts.methods || [ - 'HEAD', - 'OPTIONS', - 'GET', - 'PUT', - 'PATCH', - 'POST', - 'DELETE' - ]; - this.exclusive = Boolean(this.opts.exclusive); - - this.params = {}; - this.stack = []; - this.host = this.opts.host; + return this; + } + + // create route + const route = new Layer(path, methods, middleware, { + end: opts.end === false ? opts.end : true, + name: opts.name, + sensitive: opts.sensitive || this.opts.sensitive || false, + strict: opts.strict || this.opts.strict || false, + prefix: opts.prefix || this.opts.prefix || '', + ignoreCaptures: opts.ignoreCaptures + }); + + if (this.opts.prefix) { + route.setPrefix(this.opts.prefix); + } + + // add parameter middleware + for (let i = 0; i < Object.keys(this.params).length; i++) { + const param = Object.keys(this.params)[i]; + route.param(param, this.params[param]); + } + + stack.push(route); + + debug('defined route %s %s', route.methods, route.path); + + return route; + } + + /** + * Lookup route with given `name`. + * + * @param {String} name + * @returns {Layer|false} + */ + route(name) { + const routes = this.stack; + + for (let len = routes.length, i = 0; i < len; i++) { + if (routes[i].name && routes[i].name === name) return routes[i]; + } + + return false; + } + + /** + * Generate URL for route. Takes a route name and map of named `params`. + * + * @example + * + * ```javascript + * router.get('user', '/users/:id', (ctx, next) => { + * // ... + * }); + * + * router.url('user', 3); + * // => "/users/3" + * + * router.url('user', { id: 3 }); + * // => "/users/3" + * + * router.use((ctx, next) => { + * // redirect to named route + * ctx.redirect(ctx.router.url('sign-in')); + * }) + * + * router.url('user', { id: 3 }, { query: { limit: 1 } }); + * // => "/users/3?limit=1" + * + * router.url('user', { id: 3 }, { query: "limit=1" }); + * // => "/users/3?limit=1" + * ``` + * + * @param {String} name route name + * @param {Object} params url parameters + * @param {Object} [options] options parameter + * @param {Object|String} [options.query] query options + * @returns {String|Error} + */ + url(name, ...args) { + const route = this.route(name); + if (route) return route.url.apply(route, args); + + return new Error(`No route found for name: ${String(name)}`); + } + + /** + * Match given `path` and return corresponding routes. + * + * @param {String} path + * @param {String} method + * @returns {Object.} returns layers that matched path and + * path and method. + * @private + */ + match(path, method) { + const layers = this.stack; + let layer; + const matched = { + path: [], + pathAndMethod: [], + route: false + }; + + for (let len = layers.length, i = 0; i < len; i++) { + layer = layers[i]; + + debug('test %s %s', layer.path, layer.regexp); + + // eslint-disable-next-line unicorn/prefer-regexp-test + if (layer.match(path)) { + matched.path.push(layer); + + if (layer.methods.length === 0 || layer.methods.includes(method)) { + matched.pathAndMethod.push(layer); + if (layer.methods.length > 0) matched.route = true; + } + } + } + + return matched; + } + + /** + * Match given `input` to allowed host + * @param {String} input + * @returns {boolean} + */ + matchHost(input) { + const { host } = this; + + if (!host) { + return true; + } + + if (!input) { + return false; + } + + if (typeof host === 'string') { + return input === host; + } + + if (typeof host === 'object' && host instanceof RegExp) { + return host.test(input); + } + } + + /** + * Run middleware for named route parameters. Useful for auto-loading or + * validation. + * + * @example + * + * ```javascript + * router + * .param('user', (id, ctx, next) => { + * ctx.user = users[id]; + * if (!ctx.user) return ctx.status = 404; + * return next(); + * }) + * .get('/users/:user', ctx => { + * ctx.body = ctx.user; + * }) + * .get('/users/:user/friends', ctx => { + * return ctx.user.getFriends().then(function(friends) { + * ctx.body = friends; + * }); + * }) + * // /users/3 => {"id": 3, "name": "Alex"} + * // /users/3/friends => [{"id": 4, "name": "TJ"}] + * ``` + * + * @param {String} param + * @param {Function} middleware + * @returns {Router} + */ + param(param, middleware) { + this.params[param] = middleware; + for (let i = 0; i < this.stack.length; i++) { + const route = this.stack[i]; + route.param(param, middleware); + } + + return this; + } } /** @@ -207,639 +790,34 @@ function Router(opts = {}) { * @param {Function} callback route callback * @returns {Router} */ - -for (const method_ of methods) { - function setMethodVerb(method) { - Router.prototype[method] = function (name, path, middleware) { - if (typeof path === 'string' || path instanceof RegExp) { - middleware = Array.prototype.slice.call(arguments, 2); - } else { - middleware = Array.prototype.slice.call(arguments, 1); - path = name; - name = null; - } - - // Sanity check to ensure we have a viable path candidate (eg: string|regex|non-empty array) - if ( - typeof path !== 'string' && - !(path instanceof RegExp) && - (!Array.isArray(path) || path.length === 0) - ) - throw new Error( - `You have to provide a path when adding a ${method} handler` - ); - - this.register(path, [method], middleware, { name }); - - return this; - }; - } - - setMethodVerb(method_); -} - -// Alias for `router.delete()` because delete is a reserved word -// eslint-disable-next-line dot-notation -Router.prototype.del = Router.prototype['delete']; - -/** - * Use given middleware. - * - * Middleware run in the order they are defined by `.use()`. They are invoked - * sequentially, requests start at the first middleware and work their way - * "down" the middleware stack. - * - * @example - * - * ```javascript - * // session middleware will run before authorize - * router - * .use(session()) - * .use(authorize()); - * - * // use middleware only with given path - * router.use('/users', userAuth()); - * - * // or with an array of paths - * router.use(['/users', '/admin'], userAuth()); - * - * app.use(router.routes()); - * ``` - * - * @param {String=} path - * @param {Function} middleware - * @param {Function=} ... - * @returns {Router} - */ - -Router.prototype.use = function () { - const router = this; - const middleware = Array.prototype.slice.call(arguments); - let path; - - // support array of paths - if (Array.isArray(middleware[0]) && typeof middleware[0][0] === 'string') { - const arrPaths = middleware[0]; - for (const p of arrPaths) { - router.use.apply(router, [p].concat(middleware.slice(1))); - } - - return this; - } - - const hasPath = typeof middleware[0] === 'string'; - if (hasPath) path = middleware.shift(); - - for (const m of middleware) { - if (m.router) { - const cloneRouter = Object.assign( - Object.create(Router.prototype), - m.router, - { - stack: [...m.router.stack] - } - ); - - for (let j = 0; j < cloneRouter.stack.length; j++) { - const nestedLayer = cloneRouter.stack[j]; - const cloneLayer = Object.assign( - Object.create(Layer.prototype), - nestedLayer - ); - - if (path) cloneLayer.setPrefix(path); - if (router.opts.prefix) cloneLayer.setPrefix(router.opts.prefix); - router.stack.push(cloneLayer); - cloneRouter.stack[j] = cloneLayer; - } - - if (router.params) { - function setRouterParams(paramArr) { - const routerParams = paramArr; - for (const key of routerParams) { - cloneRouter.param(key, router.params[key]); - } - } - - setRouterParams(Object.keys(router.params)); - } +for (const method of methods) { + Router.prototype[method] = function (name, path, middleware) { + if (typeof path === 'string' || path instanceof RegExp) { + middleware = Array.prototype.slice.call(arguments, 2); } else { - const keys = []; - pathToRegexp(router.opts.prefix || '', keys); - const routerPrefixHasParam = router.opts.prefix && keys.length; - router.register(path || '([^/]*)', [], m, { - end: false, - ignoreCaptures: !hasPath && !routerPrefixHasParam - }); + middleware = Array.prototype.slice.call(arguments, 1); + path = name; + name = null; } - } - return this; -}; - -/** - * Set the path prefix for a Router instance that was already initialized. - * - * @example - * - * ```javascript - * router.prefix('/things/:thing_id') - * ``` - * - * @param {String} prefix - * @returns {Router} - */ - -Router.prototype.prefix = function (prefix) { - prefix = prefix.replace(/\/$/, ''); - - this.opts.prefix = prefix; - - for (let i = 0; i < this.stack.length; i++) { - const route = this.stack[i]; - route.setPrefix(prefix); - } - - return this; -}; - -/** - * Returns router middleware which dispatches a route matching the request. - * - * @returns {Function} - */ - -Router.prototype.routes = Router.prototype.middleware = function () { - const router = this; - - const dispatch = function dispatch(ctx, next) { - debug('%s %s', ctx.method, ctx.path); - - const hostMatched = router.matchHost(ctx.host); - - if (!hostMatched) { - return next(); - } - - const path = - router.opts.routerPath || ctx.newRouterPath || ctx.path || ctx.routerPath; - const matched = router.match(path, ctx.method); - let layerChain; - - if (ctx.matched) { - ctx.matched.push.apply(ctx.matched, matched.path); - } else { - ctx.matched = matched.path; - } - - ctx.router = router; - - if (!matched.route) return next(); - - const matchedLayers = matched.pathAndMethod; - const mostSpecificLayer = matchedLayers[matchedLayers.length - 1]; - ctx._matchedRoute = mostSpecificLayer.path; - if (mostSpecificLayer.name) { - ctx._matchedRouteName = mostSpecificLayer.name; - } - - layerChain = ( - router.exclusive ? [mostSpecificLayer] : matchedLayers - ).reduce(function (memo, layer) { - memo.push(function (ctx, next) { - ctx.captures = layer.captures(path, ctx.captures); - ctx.params = ctx.request.params = layer.params( - path, - ctx.captures, - ctx.params - ); - ctx.routerPath = layer.path; - ctx.routerName = layer.name; - ctx._matchedRoute = layer.path; - if (layer.name) { - ctx._matchedRouteName = layer.name; - } - - return next(); - }); - return memo.concat(layer.stack); - }, []); - - return compose(layerChain)(ctx, next); - }; - - dispatch.router = this; - - return dispatch; -}; - -/** - * Returns separate middleware for responding to `OPTIONS` requests with - * an `Allow` header containing the allowed methods, as well as responding - * with `405 Method Not Allowed` and `501 Not Implemented` as appropriate. - * - * @example - * - * ```javascript - * const Koa = require('koa'); - * const Router = require('@koa/router'); - * - * const app = new Koa(); - * const router = new Router(); - * - * app.use(router.routes()); - * app.use(router.allowedMethods()); - * ``` - * - * **Example with [Boom](https://github.com/hapijs/boom)** - * - * ```javascript - * const Koa = require('koa'); - * const Router = require('@koa/router'); - * const Boom = require('boom'); - * - * const app = new Koa(); - * const router = new Router(); - * - * app.use(router.routes()); - * app.use(router.allowedMethods({ - * throw: true, - * notImplemented: () => new Boom.notImplemented(), - * methodNotAllowed: () => new Boom.methodNotAllowed() - * })); - * ``` - * - * @param {Object=} options - * @param {Boolean=} options.throw throw error instead of setting status and header - * @param {Function=} options.notImplemented throw the returned value in place of the default NotImplemented error - * @param {Function=} options.methodNotAllowed throw the returned value in place of the default MethodNotAllowed error - * @returns {Function} - */ - -Router.prototype.allowedMethods = function (options = {}) { - const implemented = this.methods; - - return function allowedMethods(ctx, next) { - return next().then(function () { - const allowed = {}; - - if (ctx.matched && (!ctx.status || ctx.status === 404)) { - for (let i = 0; i < ctx.matched.length; i++) { - const route = ctx.matched[i]; - for (let j = 0; j < route.methods.length; j++) { - const method = route.methods[j]; - allowed[method] = method; - } - } - - const allowedArr = Object.keys(allowed); - - if (!~implemented.indexOf(ctx.method)) { - if (options.throw) { - const notImplementedThrowable = - typeof options.notImplemented === 'function' - ? options.notImplemented() // set whatever the user returns from their function - : new HttpError.NotImplemented(); - - throw notImplementedThrowable; - } else { - ctx.status = 501; - ctx.set('Allow', allowedArr.join(', ')); - } - } else if (allowedArr.length > 0) { - if (ctx.method === 'OPTIONS') { - ctx.status = 200; - ctx.body = ''; - ctx.set('Allow', allowedArr.join(', ')); - } else if (!allowed[ctx.method]) { - if (options.throw) { - const notAllowedThrowable = - typeof options.methodNotAllowed === 'function' - ? options.methodNotAllowed() // set whatever the user returns from their function - : new HttpError.MethodNotAllowed(); - - throw notAllowedThrowable; - } else { - ctx.status = 405; - ctx.set('Allow', allowedArr.join(', ')); - } - } - } - } - }); - }; -}; - -/** - * Register route with all methods. - * - * @param {String} name Optional. - * @param {String} path - * @param {Function=} middleware You may also pass multiple middleware. - * @param {Function} callback - * @returns {Router} - */ - -Router.prototype.all = function (name, path, middleware) { - if (typeof path === 'string') { - middleware = Array.prototype.slice.call(arguments, 2); - } else { - middleware = Array.prototype.slice.call(arguments, 1); - path = name; - name = null; - } - - // Sanity check to ensure we have a viable path candidate (eg: string|regex|non-empty array) - if ( - typeof path !== 'string' && - !(path instanceof RegExp) && - (!Array.isArray(path) || path.length === 0) - ) - throw new Error('You have to provide a path when adding an all handler'); - - this.register(path, methods, middleware, { name }); - - return this; -}; - -/** - * Redirect `source` to `destination` URL with optional 30x status `code`. - * - * Both `source` and `destination` can be route names. - * - * ```javascript - * router.redirect('/login', 'sign-in'); - * ``` - * - * This is equivalent to: - * - * ```javascript - * router.all('/login', ctx => { - * ctx.redirect('/sign-in'); - * ctx.status = 301; - * }); - * ``` - * - * @param {String} source URL or route name. - * @param {String} destination URL or route name. - * @param {Number=} code HTTP status code (default: 301). - * @returns {Router} - */ - -Router.prototype.redirect = function (source, destination, code) { - // lookup source route by name - if (typeof source === 'symbol' || source[0] !== '/') { - source = this.url(source); - if (source instanceof Error) throw source; - } - - // lookup destination route by name - if ( - typeof destination === 'symbol' || - (destination[0] !== '/' && !destination.includes('://')) - ) { - destination = this.url(destination); - if (destination instanceof Error) throw destination; - } - - return this.all(source, (ctx) => { - ctx.redirect(destination); - ctx.status = code || 301; - }); -}; - -/** - * Create and register a route. - * - * @param {String} path Path string. - * @param {Array.} methods Array of HTTP verbs. - * @param {Function} middleware Multiple middleware also accepted. - * @returns {Layer} - * @private - */ - -Router.prototype.register = function (path, methods, middleware, opts = {}) { - const router = this; - const { stack } = this; + // Sanity check to ensure we have a viable path candidate (eg: string|regex|non-empty array) + if ( + typeof path !== 'string' && + !(path instanceof RegExp) && + (!Array.isArray(path) || path.length === 0) + ) + throw new Error( + `You have to provide a path when adding a ${method} handler` + ); - // support array of paths - if (Array.isArray(path)) { - for (const curPath of path) { - router.register.call(router, curPath, methods, middleware, opts); - } + this.register(path, [method], middleware, { name }); return this; - } - - // create route - const route = new Layer(path, methods, middleware, { - end: opts.end === false ? opts.end : true, - name: opts.name, - sensitive: opts.sensitive || this.opts.sensitive || false, - strict: opts.strict || this.opts.strict || false, - prefix: opts.prefix || this.opts.prefix || '', - ignoreCaptures: opts.ignoreCaptures - }); - - if (this.opts.prefix) { - route.setPrefix(this.opts.prefix); - } - - // add parameter middleware - for (let i = 0; i < Object.keys(this.params).length; i++) { - const param = Object.keys(this.params)[i]; - route.param(param, this.params[param]); - } - - stack.push(route); - - debug('defined route %s %s', route.methods, route.path); - - return route; -}; - -/** - * Lookup route with given `name`. - * - * @param {String} name - * @returns {Layer|false} - */ - -Router.prototype.route = function (name) { - const routes = this.stack; - - for (let len = routes.length, i = 0; i < len; i++) { - if (routes[i].name && routes[i].name === name) return routes[i]; - } - - return false; -}; - -/** - * Generate URL for route. Takes a route name and map of named `params`. - * - * @example - * - * ```javascript - * router.get('user', '/users/:id', (ctx, next) => { - * // ... - * }); - * - * router.url('user', 3); - * // => "/users/3" - * - * router.url('user', { id: 3 }); - * // => "/users/3" - * - * router.use((ctx, next) => { - * // redirect to named route - * ctx.redirect(ctx.router.url('sign-in')); - * }) - * - * router.url('user', { id: 3 }, { query: { limit: 1 } }); - * // => "/users/3?limit=1" - * - * router.url('user', { id: 3 }, { query: "limit=1" }); - * // => "/users/3?limit=1" - * ``` - * - * @param {String} name route name - * @param {Object} params url parameters - * @param {Object} [options] options parameter - * @param {Object|String} [options.query] query options - * @returns {String|Error} - */ - -Router.prototype.url = function (name, params) { - const route = this.route(name); - - if (route) { - const args = Array.prototype.slice.call(arguments, 1); - return route.url.apply(route, args); - } - - return new Error(`No route found for name: ${String(name)}`); -}; - -/** - * Match given `path` and return corresponding routes. - * - * @param {String} path - * @param {String} method - * @returns {Object.} returns layers that matched path and - * path and method. - * @private - */ - -Router.prototype.match = function (path, method) { - const layers = this.stack; - let layer; - const matched = { - path: [], - pathAndMethod: [], - route: false }; +} - for (let len = layers.length, i = 0; i < len; i++) { - layer = layers[i]; - - debug('test %s %s', layer.path, layer.regexp); - - // eslint-disable-next-line unicorn/prefer-regexp-test - if (layer.match(path)) { - matched.path.push(layer); - - if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) { - matched.pathAndMethod.push(layer); - if (layer.methods.length > 0) matched.route = true; - } - } - } - - return matched; -}; - -/** - * Match given `input` to allowed host - * @param {String} input - * @returns {boolean} - */ - -Router.prototype.matchHost = function (input) { - const { host } = this; - - if (!host) { - return true; - } - - if (!input) { - return false; - } - - if (typeof host === 'string') { - return input === host; - } - - if (typeof host === 'object' && host instanceof RegExp) { - return host.test(input); - } -}; - -/** - * Run middleware for named route parameters. Useful for auto-loading or - * validation. - * - * @example - * - * ```javascript - * router - * .param('user', (id, ctx, next) => { - * ctx.user = users[id]; - * if (!ctx.user) return ctx.status = 404; - * return next(); - * }) - * .get('/users/:user', ctx => { - * ctx.body = ctx.user; - * }) - * .get('/users/:user/friends', ctx => { - * return ctx.user.getFriends().then(function(friends) { - * ctx.body = friends; - * }); - * }) - * // /users/3 => {"id": 3, "name": "Alex"} - * // /users/3/friends => [{"id": 4, "name": "TJ"}] - * ``` - * - * @param {String} param - * @param {Function} middleware - * @returns {Router} - */ - -Router.prototype.param = function (param, middleware) { - this.params[param] = middleware; - for (let i = 0; i < this.stack.length; i++) { - const route = this.stack[i]; - route.param(param, middleware); - } - - return this; -}; +// Alias for `router.delete()` because delete is a reserved word +// eslint-disable-next-line dot-notation +Router.prototype.del = Router.prototype['delete']; -/** - * Generate URL from url pattern and given `params`. - * - * @example - * - * ```javascript - * const url = Router.url('/users/:id', {id: 1}); - * // => "/users/1" - * ``` - * - * @param {String} path url pattern - * @param {Object} params url parameters - * @returns {String} - */ -Router.url = function (path) { - const args = Array.prototype.slice.call(arguments, 1); - return Layer.prototype.url.apply({ path }, args); -}; +module.exports = Router; diff --git a/package.json b/package.json index f1a2ab8..17e494c 100644 --- a/package.json +++ b/package.json @@ -8,41 +8,44 @@ "email": "niftylettuce@gmail.com" }, "contributors": [ - "Alex Mingoia ", - "@koajs" + { + "name": "Alex Mingoia", + "email": "talk@alexmingoia.com" + }, + { + "name": "@koajs" + }, + { + "name": "Imed Jaberi", + "email": "imed-jaberi@outlook.com" + } ], "dependencies": { - "debug": "^4.3.4", + "debug": "^4.3.6", "http-errors": "^2.0.0", "koa-compose": "^4.1.0", - "methods": "^1.1.2", - "path-to-regexp": "^6.2.1" + "path-to-regexp": "^6.2.2" }, "devDependencies": { "@commitlint/cli": "^17.7.2", "@commitlint/config-conventional": "^17.7.0", "@ladjs/env": "^4.0.0", - "ava": "^5.3.1", - "cross-env": "^7.0.3", - "eslint": "8.39.0", + "eslint": "^8.39.0", "eslint-config-xo-lass": "^2.0.1", - "expect.js": "^0.3.1", "fixpack": "^4.0.0", "husky": "^8.0.3", "jsdoc-to-markdown": "^8.0.0", - "koa": "^2.14.2", + "koa": "^2.15.3", "lint-staged": "^14.0.1", - "mocha": "^10.2.0", - "nyc": "^15.1.0", + "mocha": "^10.7.3", + "nyc": "^17.0.0", "remark-cli": "11", "remark-preset-github": "^4.0.4", - "should": "^13.2.3", - "supertest": "^6.3.3", - "wrk": "^1.2.1", + "supertest": "^7.0.0", "xo": "0.53.1" }, "engines": { - "node": ">= 12" + "node": ">= 20" }, "files": [ "lib" diff --git a/test/index.js b/test/index.js index 63cff94..fd70b5b 100644 --- a/test/index.js +++ b/test/index.js @@ -1,14 +1,12 @@ /** * Module tests */ +const assert = require('node:assert'); -const should = require('should'); - -describe('module', function () { - it('should expose Router', function (done) { +describe('module', () => { + it('should expose Router', () => { const Router = require('..'); - should.exist(Router); - Router.should.be.type('function'); - done(); + assert.strictEqual(Boolean(Router), true); + assert.strictEqual(typeof Router, 'function'); }); }); diff --git a/test/lib/layer.js b/test/lib/layer.js index 182c332..891eda7 100644 --- a/test/lib/layer.js +++ b/test/lib/layer.js @@ -1,346 +1,324 @@ /** * Route tests */ +const http = require('node:http'); +const assert = require('node:assert'); -const http = require('http'); const Koa = require('koa'); const request = require('supertest'); + const Router = require('../../lib/router'); const Layer = require('../../lib/layer'); -describe('Layer', function () { - it('composes multiple callbacks/middleware', function (done) { +describe('Layer', () => { + it('composes multiple callbacks/middleware', async () => { const app = new Koa(); const router = new Router(); app.use(router.routes()); router.get( '/:category/:title', - function (ctx, next) { + (ctx, next) => { ctx.status = 500; return next(); }, - function (ctx, next) { + (ctx, next) => { ctx.status = 204; return next(); } ); - request(http.createServer(app.callback())) + + await request(http.createServer(app.callback())) .get('/programming/how-to-node') - .expect(204) - .end(function (err) { - if (err) return done(err); - done(); - }); + .expect(204); }); - describe('Layer#match()', function () { - it('captures URL path parameters', function (done) { + describe('Layer#match()', () => { + it('captures URL path parameters', async () => { const app = new Koa(); const router = new Router(); app.use(router.routes()); - router.get('/:category/:title', function (ctx) { - ctx.should.have.property('params'); - ctx.params.should.be.type('object'); - ctx.params.should.have.property('category', 'match'); - ctx.params.should.have.property('title', 'this'); + router.get('/:category/:title', (ctx) => { + assert.strictEqual(typeof ctx.params, 'object'); + assert.strictEqual(ctx.params.category, 'match'); + assert.strictEqual(ctx.params.title, 'this'); ctx.status = 204; }); - request(http.createServer(app.callback())) + await request(http.createServer(app.callback())) .get('/match/this') - .expect(204) - .end(function (err) { - if (err) return done(err); - done(); - }); + .expect(204); }); - it('return original path parameters when decodeURIComponent throw error', function (done) { + it('return original path parameters when decodeURIComponent throw error', async () => { const app = new Koa(); const router = new Router(); app.use(router.routes()); - router.get('/:category/:title', function (ctx) { - ctx.should.have.property('params'); - ctx.params.should.be.type('object'); - ctx.params.should.have.property('category', '100%'); - ctx.params.should.have.property('title', '101%'); + router.get('/:category/:title', (ctx) => { + assert.strictEqual(typeof ctx.params, 'object'); + assert.strictEqual(ctx.params.category, '100%'); + assert.strictEqual(ctx.params.title, '101%'); ctx.status = 204; }); - request(http.createServer(app.callback())) + await request(http.createServer(app.callback())) .get('/100%/101%') - .expect(204) - .end(done); + .expect(204); }); - it('populates ctx.captures with regexp captures', function (done) { + it('populates ctx.captures with regexp captures', async () => { const app = new Koa(); const router = new Router(); app.use(router.routes()); router.get( /^\/api\/([^/]+)\/?/i, - function (ctx, next) { - ctx.should.have.property('captures'); - ctx.captures.should.be.instanceOf(Array); - ctx.captures.should.have.property(0, '1'); + (ctx, next) => { + assert.strictEqual(Array.isArray(ctx.captures), true); + assert.strictEqual(ctx.captures[0], '1'); return next(); }, - function (ctx) { - ctx.should.have.property('captures'); - ctx.captures.should.be.instanceOf(Array); - ctx.captures.should.have.property(0, '1'); + (ctx) => { + assert.strictEqual(Array.isArray(ctx.captures), true); + assert.strictEqual(ctx.captures[0], '1'); ctx.status = 204; } ); - request(http.createServer(app.callback())) + await request(http.createServer(app.callback())) .get('/api/1') - .expect(204) - .end(function (err) { - if (err) return done(err); - done(); - }); + .expect(204); }); - it('return original ctx.captures when decodeURIComponent throw error', function (done) { + it('return original ctx.captures when decodeURIComponent throw error', async () => { const app = new Koa(); const router = new Router(); app.use(router.routes()); router.get( /^\/api\/([^/]+)\/?/i, - function (ctx, next) { - ctx.should.have.property('captures'); - ctx.captures.should.be.type('object'); - ctx.captures.should.have.property(0, '101%'); + (ctx, next) => { + assert.strictEqual(typeof ctx.captures, 'object'); + assert.strictEqual(ctx.captures[0], '101%'); return next(); }, - function (ctx) { - ctx.should.have.property('captures'); - ctx.captures.should.be.type('object'); - ctx.captures.should.have.property(0, '101%'); + (ctx) => { + assert.strictEqual(typeof ctx.captures, 'object'); + assert.strictEqual(ctx.captures[0], '101%'); ctx.status = 204; } ); - request(http.createServer(app.callback())) + await request(http.createServer(app.callback())) .get('/api/101%') - .expect(204) - .end(function (err) { - if (err) return done(err); - done(); - }); + .expect(204); }); - it('populates ctx.captures with regexp captures include undefined', function (done) { + it('populates ctx.captures with regexp captures include undefined', async () => { const app = new Koa(); const router = new Router(); app.use(router.routes()); router.get( /^\/api(\/.+)?/i, - function (ctx, next) { - ctx.should.have.property('captures'); - ctx.captures.should.be.type('object'); - ctx.captures.should.have.property(0, undefined); + (ctx, next) => { + assert.strictEqual(typeof ctx.captures, 'object'); + assert.strictEqual(ctx.captures[0], undefined); return next(); }, - function (ctx) { - ctx.should.have.property('captures'); - ctx.captures.should.be.type('object'); - ctx.captures.should.have.property(0, undefined); + (ctx) => { + assert.strictEqual(typeof ctx.captures, 'object'); + assert.strictEqual(ctx.captures[0], undefined); ctx.status = 204; } ); - request(http.createServer(app.callback())) - .get('/api') - .expect(204) - .end(function (err) { - if (err) return done(err); - done(); - }); + await request(http.createServer(app.callback())).get('/api').expect(204); }); - it('should throw friendly error message when handle not exists', function () { + it('should throw friendly error message when handle not exists', () => { const app = new Koa(); const router = new Router(); app.use(router.routes()); const notexistHandle = undefined; - (function () { - router.get('/foo', notexistHandle); - }).should.throw( - 'get `/foo`: `middleware` must be a function, not `undefined`' + assert.throws( + () => router.get('/foo', notexistHandle), + new Error( + 'get `/foo`: `middleware` must be a function, not `undefined`' + ) ); - (function () { - router.get('foo router', '/foo', notexistHandle); - }).should.throw( - 'get `foo router`: `middleware` must be a function, not `undefined`' + assert.throws( + () => router.get('foo router', '/foo', notexistHandle), + new Error( + 'get `foo router`: `middleware` must be a function, not `undefined`' + ) ); - (function () { - router.post('/foo', function () {}, notexistHandle); - }).should.throw( - 'post `/foo`: `middleware` must be a function, not `undefined`' + assert.throws( + () => router.post('/foo', () => {}, notexistHandle), + new Error( + 'post `/foo`: `middleware` must be a function, not `undefined`' + ) ); }); }); - describe('Layer#param()', function () { - it('composes middleware for param fn', function (done) { + describe('Layer#param()', () => { + it('composes middleware for param fn', async () => { const app = new Koa(); const router = new Router(); const route = new Layer( '/users/:user', ['GET'], [ - function (ctx) { + (ctx) => { ctx.body = ctx.user; } ] ); - route.param('user', function (id, ctx, next) { + route.param('user', (id, ctx, next) => { ctx.user = { name: 'alex' }; - if (!id) return (ctx.status = 404); + if (!id) { + ctx.status = 404; + return; + } + return next(); }); router.stack.push(route); app.use(router.middleware()); - request(http.createServer(app.callback())) + const res = await request(http.createServer(app.callback())) .get('/users/3') - .expect(200) - .end(function (err, res) { - if (err) return done(err); - res.should.have.property('body'); - res.body.should.have.property('name', 'alex'); - done(); - }); + .expect(200); + assert.strictEqual(res.body.name, 'alex'); }); - it('ignores params which are not matched', function (done) { + it('ignores params which are not matched', async () => { const app = new Koa(); const router = new Router(); const route = new Layer( '/users/:user', ['GET'], [ - function (ctx) { + (ctx) => { ctx.body = ctx.user; } ] ); - route.param('user', function (id, ctx, next) { + route.param('user', (id, ctx, next) => { ctx.user = { name: 'alex' }; - if (!id) return (ctx.status = 404); + if (!id) { + ctx.status = 404; + return; + } + return next(); }); - route.param('title', function (id, ctx, next) { + route.param('title', (id, ctx, next) => { ctx.user = { name: 'mark' }; - if (!id) return (ctx.status = 404); + if (!id) { + ctx.status = 404; + return; + } + return next(); }); router.stack.push(route); app.use(router.middleware()); - request(http.createServer(app.callback())) + const res = await request(http.createServer(app.callback())) .get('/users/3') - .expect(200) - .end(function (err, res) { - if (err) return done(err); - res.should.have.property('body'); - res.body.should.have.property('name', 'alex'); - done(); - }); + .expect(200); + + assert.strictEqual(res.body.name, 'alex'); }); }); - describe('Layer#params()', function () { + describe('Layer#params()', () => { let route; - before(function () { - route = new Layer('/:category', ['GET'], [function () {}]); + before(() => { + route = new Layer('/:category', ['GET'], [() => {}]); }); - it('should return an empty object if params were not pass', function () { + it('should return an empty object if params were not pass', () => { const params = route.params('', []); - params.should.deepEqual({}); + assert.deepStrictEqual(params, {}); }); - it('should return empty object if params is empty string', function () { + it('should return empty object if params is empty string', () => { const params = route.params('', ['']); - params.should.deepEqual({}); + assert.deepStrictEqual(params, {}); }); - it('should return an object with escaped params', function () { + it('should return an object with escaped params', () => { const params = route.params('', ['how%20to%20node']); - params.should.deepEqual({ category: 'how to node' }); + assert.deepStrictEqual(params, { category: 'how to node' }); }); - it('should return an object with the same params if an error occurs', function () { + it('should return an object with the same params if an error occurs', () => { const params = route.params('', ['%E0%A4%A']); - params.should.deepEqual({ category: '%E0%A4%A' }); + assert.deepStrictEqual(params, { category: '%E0%A4%A' }); }); - it('should return an object with data if params were pass', function () { + it('should return an object with data if params were pass', () => { const params = route.params('', ['programming']); - params.should.deepEqual({ category: 'programming' }); + assert.deepStrictEqual(params, { category: 'programming' }); }); - it('should return empty object if params were not pass', function () { + it('should return empty object if params were not pass', () => { route.paramNames = []; const params = route.params('', ['programming']); - params.should.deepEqual({}); + assert.deepStrictEqual(params, {}); }); }); - describe('Layer#url()', function () { - it('generates route URL', function () { - const route = new Layer('/:category/:title', ['get'], [function () {}], { + describe('Layer#url()', () => { + it('generates route URL', () => { + const route = new Layer('/:category/:title', ['get'], [() => {}], { name: 'books' }); let url = route.url({ category: 'programming', title: 'how-to-node' }); - url.should.equal('/programming/how-to-node'); + assert.strictEqual(url, '/programming/how-to-node'); url = route.url('programming', 'how-to-node'); - url.should.equal('/programming/how-to-node'); + assert.strictEqual(url, '/programming/how-to-node'); }); - it('escapes using encodeURIComponent()', function () { - const route = new Layer('/:category/:title', ['get'], [function () {}], { + it('escapes using encodeURIComponent()', () => { + const route = new Layer('/:category/:title', ['get'], [() => {}], { name: 'books' }); const url = route.url({ category: 'programming', title: 'how to node & js/ts' }); - url.should.equal('/programming/how%20to%20node%20%26%20js%2Fts'); + assert.strictEqual(url, '/programming/how%20to%20node%20%26%20js%2Fts'); }); - it('setPrefix method checks Layer for path', function () { - const route = new Layer('/category', ['get'], [function () {}], { + it('setPrefix method checks Layer for path', () => { + const route = new Layer('/category', ['get'], [() => {}], { name: 'books' }); route.path = '/hunter2'; const prefix = route.setPrefix('TEST'); - prefix.path.should.equal('TEST/hunter2'); + assert.strictEqual(prefix.path, 'TEST/hunter2'); }); }); describe('Layer#prefix', () => { - it('setPrefix method passes check Layer for path', function () { - const route = new Layer('/category', ['get'], [function () {}], { + it('setPrefix method passes check Layer for path', () => { + const route = new Layer('/category', ['get'], [() => {}], { name: 'books' }); route.path = '/hunter2'; const prefix = route.setPrefix('/TEST'); - prefix.path.should.equal('/TEST/hunter2'); + assert.strictEqual(prefix.path, '/TEST/hunter2'); }); - it('setPrefix method fails check Layer for path', function () { - const route = new Layer(false, ['get'], [function () {}], { + it('setPrefix method fails check Layer for path', () => { + const route = new Layer(false, ['get'], [() => {}], { name: 'books' }); route.path = false; const prefix = route.setPrefix('/TEST'); - prefix.path.should.equal(false); + assert.strictEqual(prefix.path, false); }); }); }); diff --git a/test/lib/router.js b/test/lib/router.js index 065fe91..bf2846f 100644 --- a/test/lib/router.js +++ b/test/lib/router.js @@ -1,76 +1,69 @@ /** * Router tests */ +const fs = require('node:fs'); +const http = require('node:http'); +const path = require('node:path'); +const assert = require('node:assert'); -const fs = require('fs'); -const http = require('http'); -const path = require('path'); -const assert = require('assert'); const Koa = require('koa'); const methods = require('methods'); const request = require('supertest'); -const expect = require('expect.js'); -const should = require('should'); + const Router = require('../../lib/router'); const Layer = require('../../lib/layer'); -describe('Router', function () { - it('creates new router with koa app', function (done) { +describe('Router', () => { + it('creates new router with koa app', () => { const router = new Router(); - router.should.be.instanceOf(Router); - done(); + assert.strictEqual(router instanceof Router, true); }); - it('should', function (done) { + it('should', () => { const router = new Router(); console.info(router.params); - - done(); }); - it('shares context between routers (gh-205)', function (done) { + it('shares context between routers (gh-205)', async () => { const app = new Koa(); const router1 = new Router(); const router2 = new Router(); - router1.get('/', function (ctx, next) { + router1.get('/', (ctx, next) => { ctx.foo = 'bar'; return next(); }); - router2.get('/', function (ctx, next) { + router2.get('/', (ctx, next) => { ctx.baz = 'qux'; ctx.body = { foo: ctx.foo }; return next(); }); app.use(router1.routes()).use(router2.routes()); - request(http.createServer(app.callback())) + const res = await request(http.createServer(app.callback())) .get('/') - .expect(200) - .end(function (err, res) { - if (err) return done(err); - expect(res.body).to.have.property('foo', 'bar'); - done(); - }); + .expect(200); + + assert.strictEqual(res.body.foo, 'bar'); }); - it('does not register middleware more than once (gh-184)', function (done) { + it('does not register middleware more than once (gh-184)', async () => { const app = new Koa(); const parentRouter = new Router(); const nestedRouter = new Router(); nestedRouter - .get('/first-nested-route', function (ctx) { + .get('/first-nested-route', (ctx) => { ctx.body = { n: ctx.n }; }) - .get('/second-nested-route', function (ctx, next) { + .get('/second-nested-route', (ctx, next) => { return next(); }) - .get('/third-nested-route', function (ctx, next) { + .get('/third-nested-route', (ctx, next) => { return next(); }); parentRouter.use( '/parent-route', - function (ctx, next) { + (ctx, next) => { ctx.n = ctx.n ? ctx.n + 1 : 1; return next(); }, @@ -79,120 +72,109 @@ describe('Router', function () { app.use(parentRouter.routes()); - request(http.createServer(app.callback())) + const res = await request(http.createServer(app.callback())) .get('/parent-route/first-nested-route') - .expect(200) - .end(function (err, res) { - if (err) return done(err); - expect(res.body).to.have.property('n', 1); - done(); - }); + .expect(200); + + assert.strictEqual(res.body.n, 1); }); - it('router can be accecced with ctx', function (done) { + it('router can be accecced with ctx', async () => { const app = new Koa(); const router = new Router(); - router.get('home', '/', function (ctx) { + router.get('home', '/', (ctx) => { ctx.body = { url: ctx.router.url('home') }; }); app.use(router.routes()); - request(http.createServer(app.callback())) + const res = await request(http.createServer(app.callback())) .get('/') - .expect(200) - .end(function (err, res) { - if (err) return done(err); - expect(res.body.url).to.eql('/'); - done(); - }); + .expect(200); + + assert.strictEqual(res.body.url, '/'); }); - it('registers multiple middleware for one route', function (done) { + it('registers multiple middleware for one route', async () => { const app = new Koa(); const router = new Router(); router.get( '/double', - function (ctx, next) { - return new Promise(function (resolve) { - setTimeout(function () { + (ctx, next) => { + return new Promise((resolve) => { + setTimeout(() => { ctx.body = { message: 'Hello' }; resolve(next()); }, 1); }); }, - function (ctx, next) { - return new Promise(function (resolve) { - setTimeout(function () { + (ctx, next) => { + return new Promise((resolve) => { + setTimeout(() => { ctx.body.message += ' World'; resolve(next()); }, 1); }); }, - function (ctx) { + (ctx) => { ctx.body.message += '!'; } ); app.use(router.routes()); - request(http.createServer(app.callback())) + const res = await request(http.createServer(app.callback())) .get('/double') - .expect(200) - .end(function (err, res) { - if (err) return done(err); - expect(res.body.message).to.eql('Hello World!'); - done(); - }); + .expect(200); + + assert.strictEqual(res.body.message, 'Hello World!'); }); - it('does not break when nested-routes use regexp paths', function (done) { + it('does not break when nested-routes use regexp paths', async () => { const app = new Koa(); const parentRouter = new Router(); const nestedRouter = new Router(); nestedRouter - .get(/^\/\w$/i, function (ctx, next) { + .get(/^\/\w$/i, (ctx, next) => { return next(); }) - .get('/first-nested-route', function (ctx, next) { + .get('/first-nested-route', (ctx, next) => { return next(); }) - .get('/second-nested-route', function (ctx, next) { + .get('/second-nested-route', (ctx, next) => { return next(); }); parentRouter.use( '/parent-route', - function (ctx, next) { + (ctx, next) => { return next(); }, nestedRouter.routes() ); app.use(parentRouter.routes()); - app.should.be.ok(); - done(); + assert.strictEqual(Boolean(app), true); }); - it('exposes middleware factory', function (done) { + it('exposes middleware factory', async () => { const router = new Router(); - router.should.have.property('routes'); - router.routes.should.be.type('function'); + assert.strictEqual('routes' in router, true); + assert.strictEqual(typeof router.routes, 'function'); const middleware = router.routes(); - should.exist(middleware); - middleware.should.be.type('function'); - done(); + assert.strictEqual(Boolean(middleware), true); + assert.strictEqual(typeof middleware, 'function'); }); - it('supports promises for async/await', function (done) { + it('supports promises for async/await', async () => { const app = new Koa(); app.experimental = true; - const router = Router(); - router.get('/async', function (ctx) { - return new Promise(function (resolve) { - setTimeout(function () { + const router = new Router(); + router.get('/async', (ctx) => { + return new Promise((resolve) => { + setTimeout(() => { ctx.body = { msg: 'promises!' }; @@ -202,2337 +184,2017 @@ describe('Router', function () { }); app.use(router.routes()).use(router.allowedMethods()); - request(http.createServer(app.callback())) + const res = await request(http.createServer(app.callback())) .get('/async') - .expect(200) - .end(function (err, res) { - if (err) return done(err); - expect(res.body).to.have.property('msg', 'promises!'); - done(); - }); + .expect(200); + + assert.strictEqual(res.body.msg, 'promises!'); }); - it('matches middleware only if route was matched (gh-182)', function (done) { + it('matches middleware only if route was matched (gh-182)', async () => { const app = new Koa(); const router = new Router(); const otherRouter = new Router(); - router.use(function (ctx, next) { + router.use((ctx, next) => { ctx.body = { bar: 'baz' }; return next(); }); - otherRouter.get('/bar', function (ctx) { + otherRouter.get('/bar', (ctx) => { ctx.body = ctx.body || { foo: 'bar' }; }); app.use(router.routes()).use(otherRouter.routes()); - request(http.createServer(app.callback())) + const res = await request(http.createServer(app.callback())) .get('/bar') - .expect(200) - .end(function (err, res) { - if (err) return done(err); - expect(res.body).to.have.property('foo', 'bar'); - expect(res.body).to.not.have.property('bar'); - done(); - }); + .expect(200); + + assert.strictEqual(res.body.foo, 'bar'); + assert.strictEqual('bar' in res.body, false); }); - it('matches first to last', function (done) { + it('matches first to last', async () => { const app = new Koa(); const router = new Router(); router - .get('user_page', '/user/(.*).jsx', function (ctx) { + .get('user_page', '/user/(.*).jsx', (ctx) => { ctx.body = { order: 1 }; }) - .all('app', '/app/(.*).jsx', function (ctx) { + .all('app', '/app/(.*).jsx', (ctx) => { ctx.body = { order: 2 }; }) - .all('view', '(.*).jsx', function (ctx) { + .all('view', '(.*).jsx', (ctx) => { ctx.body = { order: 3 }; }); - request(http.createServer(app.use(router.routes()).callback())) + const res = await request( + http.createServer(app.use(router.routes()).callback()) + ) .get('/user/account.jsx') - .expect(200) - .end(function (err, res) { - if (err) return done(err); - expect(res.body).to.have.property('order', 1); - done(); - }); + .expect(200); + + assert.strictEqual(res.body.order, 1); }); - it('runs multiple controllers when there are multiple matches', function (done) { + it('runs multiple controllers when there are multiple matches', async () => { const app = new Koa(); const router = new Router(); router - .get('users_single', '/users/:id(.*)', function (ctx, next) { + .get('users_single', '/users/:id(.*)', (ctx, next) => { ctx.body = { single: true }; next(); }) - .get('users_all', '/users/all', function (ctx, next) { + .get('users_all', '/users/all', (ctx, next) => { ctx.body = { ...ctx.body, all: true }; next(); }); - request(http.createServer(app.use(router.routes()).callback())) + const res = await request( + http.createServer(app.use(router.routes()).callback()) + ) .get('/users/all') - .expect(200) - .end(function (err, res) { - if (err) return done(err); - expect(res.body).to.have.property('single', true); - expect(res.body).to.have.property('all', true); - done(); - }); + .expect(200); + + assert.strictEqual('single' in res.body, true); + assert.strictEqual('all' in res.body, true); }); - it("runs only the last match when the 'exclusive' option is enabled", function (done) { + it("runs only the last match when the 'exclusive' option is enabled", async () => { const app = new Koa(); const router = new Router({ exclusive: true }); router - .get('users_single', '/users/:id(.*)', function (ctx, next) { + .get('users_single', '/users/:id(.*)', (ctx, next) => { ctx.body = { single: true }; next(); }) - .get('users_all', '/users/all', function (ctx, next) { + .get('users_all', '/users/all', (ctx, next) => { ctx.body = { ...ctx.body, all: true }; next(); }); - request(http.createServer(app.use(router.routes()).callback())) + const res = await request( + http.createServer(app.use(router.routes()).callback()) + ) .get('/users/all') - .expect(200) - .end(function (err, res) { - if (err) return done(err); - expect(res.body).to.not.have.property('single'); - expect(res.body).to.have.property('all', true); - done(); - }); + .expect(200); + + assert.strictEqual('single' in res.body, false); + assert.strictEqual('all' in res.body, true); }); - it('does not run subsequent middleware without calling next', function (done) { + it('does not run subsequent middleware without calling next', async () => { const app = new Koa(); const router = new Router(); router.get( 'user_page', '/user/(.*).jsx', - function () { + () => { // no next() }, - function (ctx) { + (ctx) => { ctx.body = { order: 1 }; } ); - request(http.createServer(app.use(router.routes()).callback())) + await request(http.createServer(app.use(router.routes()).callback())) .get('/user/account.jsx') - .expect(404) - .end(done); + .expect(404); }); - it('nests routers with prefixes at root', function (done) { + it('nests routers with prefixes at root', async () => { const app = new Koa(); - const api = new Router(); const forums = new Router({ prefix: '/forums' }); const posts = new Router({ prefix: '/:fid/posts' }); - let server; posts - .get('/', function (ctx, next) { + .get('/', (ctx, next) => { ctx.status = 204; return next(); }) - .get('/:pid', function (ctx, next) { + .get('/:pid', (ctx, next) => { ctx.body = ctx.params; return next(); }); forums.use(posts.routes()); - server = http.createServer(app.use(forums.routes()).callback()); - - request(server) - .get('/forums/1/posts') - .expect(204) - .end(function (err) { - if (err) return done(err); - - request(server) - .get('/forums/1') - .expect(404) - .end(function (err) { - if (err) return done(err); - - request(server) - .get('/forums/1/posts/2') - .expect(200) - .end(function (err, res) { - if (err) return done(err); - - expect(res.body).to.have.property('fid', '1'); - expect(res.body).to.have.property('pid', '2'); - done(); - }); - }); - }); + const server = http.createServer(app.use(forums.routes()).callback()); + + await request(server).get('/forums/1/posts').expect(204); + await request(server).get('/forums/1').expect(404); + const res = await request(server).get('/forums/1/posts/2').expect(200); + + assert.strictEqual(res.body.fid, '1'); + assert.strictEqual(res.body.pid, '2'); }); +}); - it('nests routers with prefixes at path', function (done) { - const app = new Koa(); - const forums = new Router({ - prefix: '/api' +it('nests routers with prefixes at path', async () => { + const app = new Koa(); + const forums = new Router({ + prefix: '/api' + }); + const posts = new Router({ + prefix: '/posts' + }); + + posts + .get('/', (ctx, next) => { + ctx.status = 204; + return next(); + }) + .get('/:pid', (ctx, next) => { + ctx.body = ctx.params; + return next(); }); - const posts = new Router({ - prefix: '/posts' + + forums.use('/forums/:fid', posts.routes()); + + const server = http.createServer(app.use(forums.routes()).callback()); + + await request(server).get('/api/forums/1/posts').expect(204); + + await request(server).get('/api/forums/1').expect(404); + + const res = await request(server).get('/api/forums/1/posts/2').expect(200); + + assert.strictEqual(res.body.fid, '1'); + assert.strictEqual(res.body.pid, '2'); +}); + +it('runs subrouter middleware after parent', async () => { + const app = new Koa(); + const subrouter = new Router() + .use((ctx, next) => { + ctx.msg = 'subrouter'; + return next(); + }) + .get('/', (ctx) => { + ctx.body = { msg: ctx.msg }; }); - let server; + const router = new Router() + .use((ctx, next) => { + ctx.msg = 'router'; + return next(); + }) + .use(subrouter.routes()); + const res = await request( + http.createServer(app.use(router.routes()).callback()) + ) + .get('/') + .expect(200); + + assert.strictEqual(res.body.msg, 'subrouter'); +}); - posts - .get('/', function (ctx, next) { - ctx.status = 204; - return next(); - }) - .get('/:pid', function (ctx, next) { - ctx.body = ctx.params; - return next(); - }); +it('runs parent middleware for subrouter routes', async () => { + const app = new Koa(); + const subrouter = new Router().get('/sub', (ctx) => { + ctx.body = { msg: ctx.msg }; + }); + const router = new Router() + .use((ctx, next) => { + ctx.msg = 'router'; + return next(); + }) + .use('/parent', subrouter.routes()); + const res = await request( + http.createServer(app.use(router.routes()).callback()) + ) + .get('/parent/sub') + .expect(200); + + assert.strictEqual(res.body.msg, 'router'); +}); - forums.use('/forums/:fid', posts.routes()); - - server = http.createServer(app.use(forums.routes()).callback()); - - request(server) - .get('/api/forums/1/posts') - .expect(204) - .end(function (err) { - if (err) return done(err); - - request(server) - .get('/api/forums/1') - .expect(404) - .end(function (err) { - if (err) return done(err); - - request(server) - .get('/api/forums/1/posts/2') - .expect(200) - .end(function (err, res) { - if (err) return done(err); - - expect(res.body).to.have.property('fid', '1'); - expect(res.body).to.have.property('pid', '2'); - done(); - }); - }); - }); +it('matches corresponding requests', async () => { + const app = new Koa(); + const router = new Router(); + app.use(router.routes()); + router.get('/:category/:title', (ctx) => { + assert.strictEqual('params' in ctx, true); + assert.strictEqual(ctx.params.category, 'programming'); + assert.strictEqual(ctx.params.title, 'how-to-node'); + ctx.status = 204; + }); + router.post('/:category', (ctx) => { + assert.strictEqual('params' in ctx, true); + assert.strictEqual(ctx.params.category, 'programming'); + ctx.status = 204; }); + router.put('/:category/not-a-title', (ctx) => { + assert.strictEqual('params' in ctx, true); + assert.strictEqual(ctx.params.category, 'programming'); + assert.strictEqual('title' in ctx.params, false); - it('runs subrouter middleware after parent', function (done) { - const app = new Koa(); - const subrouter = Router() - .use(function (ctx, next) { - ctx.msg = 'subrouter'; - return next(); - }) - .get('/', function (ctx) { - ctx.body = { msg: ctx.msg }; - }); - const router = Router() - .use(function (ctx, next) { - ctx.msg = 'router'; - return next(); - }) - .use(subrouter.routes()); - request(http.createServer(app.use(router.routes()).callback())) - .get('/') - .expect(200) - .end(function (err, res) { - if (err) return done(err); - expect(res.body).to.have.property('msg', 'subrouter'); - done(); - }); + ctx.status = 204; }); + const server = http.createServer(app.callback()); + await request(server).get('/programming/how-to-node').expect(204); - it('runs parent middleware for subrouter routes', function (done) { - const app = new Koa(); - const subrouter = Router().get('/sub', function (ctx) { - ctx.body = { msg: ctx.msg }; - }); - const router = Router() - .use(function (ctx, next) { - ctx.msg = 'router'; - return next(); - }) - .use('/parent', subrouter.routes()); - request(http.createServer(app.use(router.routes()).callback())) - .get('/parent/sub') - .expect(200) - .end(function (err, res) { - if (err) return done(err); - expect(res.body).to.have.property('msg', 'router'); - done(); - }); + await request(server).post('/programming').expect(204); + + await request(server).put('/programming/not-a-title').expect(204); +}); + +it('matches corresponding requests with optional route parameter', async () => { + const app = new Koa(); + const router = new Router(); + app.use(router.routes()); + router.get('/resources', (ctx) => { + assert.strictEqual('params' in ctx, true); + assert.deepStrictEqual(ctx.params, {}); + ctx.status = 204; + }); + const id = '10'; + const ext = '.json'; + router.get('/resources/:id{.:ext}?', (ctx) => { + assert.strictEqual('params' in ctx, true); + assert.strictEqual(ctx.params.id, id); + if (ctx.params.ext) assert.strictEqual(ctx.params.ext, ext.slice(1)); + ctx.status = 204; }); + const server = http.createServer(app.callback()); + await request(server).get('/resources').expect(204); - it('matches corresponding requests', function (done) { - const app = new Koa(); - const router = new Router(); - app.use(router.routes()); - router.get('/:category/:title', function (ctx) { - ctx.should.have.property('params'); - ctx.params.should.have.property('category', 'programming'); - ctx.params.should.have.property('title', 'how-to-node'); - ctx.status = 204; - }); - router.post('/:category', function (ctx) { - ctx.should.have.property('params'); - ctx.params.should.have.property('category', 'programming'); - ctx.status = 204; - }); - router.put('/:category/not-a-title', function (ctx) { - ctx.should.have.property('params'); - ctx.params.should.have.property('category', 'programming'); - ctx.params.should.not.have.property('title'); + await request(server) + .get('/resources/' + id) + .expect(204); + + await request(server) + .get('/resources/' + id + ext) + .expect(204); +}); + +it('executes route middleware using `app.context`', async () => { + const app = new Koa(); + const router = new Router(); + app.use(router.routes()); + router.use((ctx, next) => { + ctx.bar = 'baz'; + return next(); + }); + router.get( + '/:category/:title', + (ctx, next) => { + ctx.foo = 'bar'; + return next(); + }, + (ctx) => { + assert.strictEqual(ctx.bar, 'baz'); + assert.strictEqual(ctx.foo, 'bar'); + assert.strictEqual('app' in ctx, true); + assert.strictEqual('req' in ctx, true); + assert.strictEqual('res' in ctx, true); ctx.status = 204; - }); - const server = http.createServer(app.callback()); - request(server) - .get('/programming/how-to-node') - .expect(204) - .end(function (err) { - if (err) return done(err); - request(server) - .post('/programming') - .expect(204) - .end(function (err) { - if (err) return done(err); - request(server) - .put('/programming/not-a-title') - .expect(204) - .end(function (err, res) { - done(err); - }); - }); - }); + } + ); + await request(http.createServer(app.callback())) + .get('/match/this') + .expect(204); +}); + +it('does not match after ctx.throw()', async () => { + const app = new Koa(); + let counter = 0; + const router = new Router(); + app.use(router.routes()); + router.get('/', (ctx) => { + counter++; + ctx.throw(403); + }); + router.get('/', () => { + counter++; }); + const server = http.createServer(app.callback()); + await request(server).get('/').expect(403); - it('matches corresponding requests with optional route parameter', function (done) { - const app = new Koa(); - const router = new Router(); - app.use(router.routes()); - router.get('/resources', function (ctx) { - ctx.should.have.property('params'); - ctx.params.should.be.empty(); - ctx.status = 204; - }); - const id = '10'; - const ext = '.json'; - router.get('/resources/:id{.:ext}?', function (ctx) { - ctx.should.have.property('params'); - ctx.params.should.have.property('id', id); - if (ctx.params.ext) ctx.params.ext.should.be.equal(ext.slice(1)); - ctx.status = 204; - }); - const server = http.createServer(app.callback()); - request(server) - .get('/resources') - .expect(204) - .end(function (err) { - if (err) return done(err); - request(server) - .get('/resources/' + id) - .expect(204) - .end(function (err) { - if (err) return done(err); - request(server) - .get('/resources/' + id + ext) - .expect(204) - .end(function (err, res) { - done(err); - }); - }); + assert.strictEqual(counter, 1); +}); + +it('supports promises for route middleware', async () => { + const app = new Koa(); + const router = new Router(); + app.use(router.routes()); + const readVersion = () => { + return new Promise((resolve, reject) => { + const packagePath = path.join(__dirname, '..', '..', 'package.json'); + fs.readFile(packagePath, 'utf8', (err, data) => { + if (err) return reject(err); + resolve(JSON.parse(data).version); }); - }); + }); + }; - it('executes route middleware using `app.context`', function (done) { - const app = new Koa(); - const router = new Router(); - app.use(router.routes()); - router.use(function (ctx, next) { - ctx.bar = 'baz'; + router.get( + '/', + (_, next) => { return next(); - }); - router.get( - '/:category/:title', - function (ctx, next) { - ctx.foo = 'bar'; - return next(); - }, - function (ctx) { - ctx.should.have.property('bar', 'baz'); - ctx.should.have.property('foo', 'bar'); - ctx.should.have.property('app'); - ctx.should.have.property('req'); - ctx.should.have.property('res'); + }, + (ctx) => { + return readVersion().then(() => { ctx.status = 204; - done(); - } - ); - request(http.createServer(app.callback())) - .get('/match/this') - .expect(204) - .end(function (err) { - if (err) return done(err); }); - }); + } + ); + await request(http.createServer(app.callback())).get('/').expect(204); +}); - it('does not match after ctx.throw()', function (done) { +describe('Router#allowedMethods()', () => { + it('responds to OPTIONS requests', async () => { const app = new Koa(); - let counter = 0; const router = new Router(); app.use(router.routes()); - router.get('/', function (ctx) { - counter++; - ctx.throw(403); - }); - router.get('/', function () { - counter++; + app.use(router.allowedMethods()); + router.get('/users', () => {}); + router.put('/users', () => {}); + const res = await request(http.createServer(app.callback())) + .options('/users') + .expect(200); + assert.strictEqual(res.header['content-length'], '0'); + assert.strictEqual(res.header.allow, 'HEAD, GET, PUT'); + }); +}); + +it('responds with 405 Method Not Allowed', async () => { + const app = new Koa(); + const router = new Router(); + router.get('/users', () => {}); + router.put('/users', () => {}); + router.post('/events', () => {}); + app.use(router.routes()); + app.use(router.allowedMethods()); + const res = await request(http.createServer(app.callback())) + .post('/users') + .expect(405); + assert.strictEqual(res.header.allow, 'HEAD, GET, PUT'); +}); + +it('responds with 405 Method Not Allowed using the "throw" option', async () => { + const app = new Koa(); + const router = new Router(); + app.use(router.routes()); + app.use((ctx, next) => { + return next().catch((err) => { + // assert that the correct HTTPError was thrown + assert.strictEqual(err.name, 'MethodNotAllowedError'); + assert.strictEqual(err.statusCode, 405); + + // translate the HTTPError to a normal response + ctx.body = err.name; + ctx.status = err.statusCode; }); - const server = http.createServer(app.callback()); - request(server) - .get('/') - .expect(403) - .end(function (err, res) { - if (err) return done(err); - counter.should.equal(1); - done(); - }); }); + app.use(router.allowedMethods({ throw: true })); + router.get('/users', () => {}); + router.put('/users', () => {}); + router.post('/events', () => {}); + const res = await request(http.createServer(app.callback())) + .post('/users') + .expect(405); + // the 'Allow' header is not set when throwing + assert.strictEqual('allow' in res.header, false); +}); - it('supports promises for route middleware', function (done) { - const app = new Koa(); - const router = new Router(); - app.use(router.routes()); - const readVersion = function () { - return new Promise(function (resolve, reject) { - const packagePath = path.join(__dirname, '..', '..', 'package.json'); - fs.readFile(packagePath, 'utf8', function (err, data) { - if (err) return reject(err); - resolve(JSON.parse(data).version); - }); - }); - }; +it('responds with user-provided throwable using the "throw" and "methodNotAllowed" options', async () => { + const app = new Koa(); + const router = new Router(); + app.use(router.routes()); + app.use((ctx, next) => { + return next().catch((err) => { + // assert that the correct HTTPError was thrown + assert.strictEqual(err.message, 'Custom Not Allowed Error'); + assert.strictEqual(err.statusCode, 405); - router.get( - '/', - function (ctx, next) { - return next(); - }, - function (ctx) { - return readVersion().then(function () { - ctx.status = 204; - }); + // translate the HTTPError to a normal response + ctx.body = err.body; + ctx.status = err.statusCode; + }); + }); + app.use( + router.allowedMethods({ + throw: true, + methodNotAllowed() { + const notAllowedErr = new Error('Custom Not Allowed Error'); + notAllowedErr.type = 'custom'; + notAllowedErr.statusCode = 405; + notAllowedErr.body = { + error: 'Custom Not Allowed Error', + statusCode: 405, + otherStuff: true + }; + return notAllowedErr; } - ); - request(http.createServer(app.callback())).get('/').expect(204).end(done); + }) + ); + router.get('/users', () => {}); + router.put('/users', () => {}); + router.post('/events', () => {}); + const res = await request(http.createServer(app.callback())) + .post('/users') + .expect(405); + // the 'Allow' header is not set when throwing + assert.strictEqual('allow' in res.header, false); + assert.deepStrictEqual(res.body, { + error: 'Custom Not Allowed Error', + statusCode: 405, + otherStuff: true }); +}); - describe('Router#allowedMethods()', function () { - it('responds to OPTIONS requests', function (done) { - const app = new Koa(); - const router = new Router(); - app.use(router.routes()); - app.use(router.allowedMethods()); - router.get('/users', function () {}); - router.put('/users', function () {}); - request(http.createServer(app.callback())) - .options('/users') - .expect(200) - .end(function (err, res) { - if (err) return done(err); - res.header.should.have.property('content-length', '0'); - res.header.should.have.property('allow', 'HEAD, GET, PUT'); - done(); - }); - }); +it('responds with 501 Not Implemented', async () => { + const app = new Koa(); + const router = new Router(); + app.use(router.routes()); + app.use(router.allowedMethods()); + router.get('/users', () => {}); + router.put('/users', () => {}); + await request(http.createServer(app.callback())).search('/users').expect(501); +}); - it('responds with 405 Method Not Allowed', function (done) { - const app = new Koa(); - const router = new Router(); - router.get('/users', function () {}); - router.put('/users', function () {}); - router.post('/events', function () {}); - app.use(router.routes()); - app.use(router.allowedMethods()); - request(http.createServer(app.callback())) - .post('/users') - .expect(405) - .end(function (err, res) { - if (err) return done(err); - res.header.should.have.property('allow', 'HEAD, GET, PUT'); - done(); - }); - }); +it('responds with 501 Not Implemented using the "throw" option', async () => { + const app = new Koa(); + const router = new Router(); + app.use(router.routes()); + app.use((ctx, next) => { + return next().catch((err) => { + // assert that the correct HTTPError was thrown + assert.strictEqual(err.name, 'NotImplementedError'); + assert.strictEqual(err.statusCode, 501); - it('responds with 405 Method Not Allowed using the "throw" option', function (done) { - const app = new Koa(); - const router = new Router(); - app.use(router.routes()); - app.use(function (ctx, next) { - return next().catch(function (err) { - // assert that the correct HTTPError was thrown - err.name.should.equal('MethodNotAllowedError'); - err.statusCode.should.equal(405); - - // translate the HTTPError to a normal response - ctx.body = err.name; - ctx.status = err.statusCode; - }); - }); - app.use(router.allowedMethods({ throw: true })); - router.get('/users', function () {}); - router.put('/users', function () {}); - router.post('/events', function () {}); - request(http.createServer(app.callback())) - .post('/users') - .expect(405) - .end(function (err, res) { - if (err) return done(err); - // the 'Allow' header is not set when throwing - res.header.should.not.have.property('allow'); - done(); - }); + // translate the HTTPError to a normal response + ctx.body = err.name; + ctx.status = err.statusCode; }); + }); + app.use(router.allowedMethods({ throw: true })); + router.get('/users', () => {}); + router.put('/users', () => {}); + const res = await request(http.createServer(app.callback())) + .search('/users') + .expect(501); + // the 'Allow' header is not set when throwing + assert.strictEqual('allow' in res.header, false); +}); - it('responds with user-provided throwable using the "throw" and "methodNotAllowed" options', function (done) { - const app = new Koa(); - const router = new Router(); - app.use(router.routes()); - app.use(function (ctx, next) { - return next().catch(function (err) { - // assert that the correct HTTPError was thrown - err.message.should.equal('Custom Not Allowed Error'); - err.statusCode.should.equal(405); - - // translate the HTTPError to a normal response - ctx.body = err.body; - ctx.status = err.statusCode; - }); - }); - app.use( - router.allowedMethods({ - throw: true, - methodNotAllowed() { - const notAllowedErr = new Error('Custom Not Allowed Error'); - notAllowedErr.type = 'custom'; - notAllowedErr.statusCode = 405; - notAllowedErr.body = { - error: 'Custom Not Allowed Error', - statusCode: 405, - otherStuff: true - }; - return notAllowedErr; - } - }) - ); - router.get('/users', function () {}); - router.put('/users', function () {}); - router.post('/events', function () {}); - request(http.createServer(app.callback())) - .post('/users') - .expect(405) - .end(function (err, res) { - if (err) return done(err); - // the 'Allow' header is not set when throwing - res.header.should.not.have.property('allow'); - res.body.should.eql({ - error: 'Custom Not Allowed Error', - statusCode: 405, - otherStuff: true - }); - done(); - }); +it('responds with user-provided throwable using the "throw" and "notImplemented" options', async () => { + const app = new Koa(); + const router = new Router(); + app.use(router.routes()); + app.use((ctx, next) => { + return next().catch((err) => { + // assert that our custom error was thrown + assert.strictEqual(err.message, 'Custom Not Implemented Error'); + assert.strictEqual(err.type, 'custom'); + assert.strictEqual(err.statusCode, 501); + + // translate the HTTPError to a normal response + ctx.body = err.body; + ctx.status = err.statusCode; }); + }); + app.use( + router.allowedMethods({ + throw: true, + notImplemented() { + const notImplementedErr = new Error('Custom Not Implemented Error'); + notImplementedErr.type = 'custom'; + notImplementedErr.statusCode = 501; + notImplementedErr.body = { + error: 'Custom Not Implemented Error', + statusCode: 501, + otherStuff: true + }; + return notImplementedErr; + } + }) + ); + router.get('/users', () => {}); + router.put('/users', () => {}); + const res = await request(http.createServer(app.callback())) + .search('/users') + .expect(501); + // the 'Allow' header is not set when throwing + assert.strictEqual('allow' in res.header, false); + assert.deepStrictEqual(res.body, { + error: 'Custom Not Implemented Error', + statusCode: 501, + otherStuff: true + }); +}); - it('responds with 501 Not Implemented', function (done) { - const app = new Koa(); - const router = new Router(); - app.use(router.routes()); - app.use(router.allowedMethods()); - router.get('/users', function () {}); - router.put('/users', function () {}); - request(http.createServer(app.callback())) - .search('/users') - .expect(501) - .end(function (err) { - if (err) return done(err); - done(); - }); - }); +it('does not send 405 if route matched but status is 404', async () => { + const app = new Koa(); + const router = new Router(); + app.use(router.routes()); + app.use(router.allowedMethods()); + router.get('/users', (ctx) => { + ctx.status = 404; + }); + await request(http.createServer(app.callback())).get('/users').expect(404); +}); - it('responds with 501 Not Implemented using the "throw" option', function (done) { - const app = new Koa(); - const router = new Router(); - app.use(router.routes()); - app.use(function (ctx, next) { - return next().catch(function (err) { - // assert that the correct HTTPError was thrown - err.name.should.equal('NotImplementedError'); - err.statusCode.should.equal(501); - - // translate the HTTPError to a normal response - ctx.body = err.name; - ctx.status = err.statusCode; - }); - }); - app.use(router.allowedMethods({ throw: true })); - router.get('/users', function () {}); - router.put('/users', function () {}); - request(http.createServer(app.callback())) - .search('/users') - .expect(501) - .end(function (err, res) { - if (err) return done(err); - // the 'Allow' header is not set when throwing - res.header.should.not.have.property('allow'); - done(); - }); - }); +it('sets the allowed methods to a single Allow header #273', async () => { + // https://tools.ietf.org/html/rfc7231#section-7.4.1 + const app = new Koa(); + const router = new Router(); + app.use(router.routes()); + app.use(router.allowedMethods()); - it('responds with user-provided throwable using the "throw" and "notImplemented" options', function (done) { - const app = new Koa(); - const router = new Router(); - app.use(router.routes()); - app.use(function (ctx, next) { - return next().catch(function (err) { - // assert that our custom error was thrown - err.message.should.equal('Custom Not Implemented Error'); - err.type.should.equal('custom'); - err.statusCode.should.equal(501); - - // translate the HTTPError to a normal response - ctx.body = err.body; - ctx.status = err.statusCode; - }); - }); - app.use( - router.allowedMethods({ - throw: true, - notImplemented() { - const notImplementedErr = new Error('Custom Not Implemented Error'); - notImplementedErr.type = 'custom'; - notImplementedErr.statusCode = 501; - notImplementedErr.body = { - error: 'Custom Not Implemented Error', - statusCode: 501, - otherStuff: true - }; - return notImplementedErr; - } - }) - ); - router.get('/users', function () {}); - router.put('/users', function () {}); - request(http.createServer(app.callback())) - .search('/users') - .expect(501) - .end(function (err, res) { - if (err) return done(err); - // the 'Allow' header is not set when throwing - res.header.should.not.have.property('allow'); - res.body.should.eql({ - error: 'Custom Not Implemented Error', - statusCode: 501, - otherStuff: true - }); - done(); - }); - }); + router.get('/', () => {}); - it('does not send 405 if route matched but status is 404', function (done) { - const app = new Koa(); - const router = new Router(); - app.use(router.routes()); - app.use(router.allowedMethods()); - router.get('/users', function (ctx) { - ctx.status = 404; - }); - request(http.createServer(app.callback())) - .get('/users') - .expect(404) - .end(function (err) { - if (err) return done(err); - done(); - }); - }); + const res = await request(http.createServer(app.callback())) + .options('/') + .expect(200); - it('sets the allowed methods to a single Allow header #273', function (done) { - // https://tools.ietf.org/html/rfc7231#section-7.4.1 - const app = new Koa(); - const router = new Router(); - app.use(router.routes()); - app.use(router.allowedMethods()); + assert.strictEqual(res.header.allow, 'HEAD, GET'); + const allowHeaders = res.res.rawHeaders.filter((item) => item === 'Allow'); + assert.strictEqual(allowHeaders.length, 1); +}); - router.get('/', function () {}); +it('allowedMethods check if flow (allowedArr.length)', async () => { + const app = new Koa(); + const router = new Router(); + app.use(router.routes()); + app.use(router.allowedMethods()); - request(http.createServer(app.callback())) - .options('/') - .expect(200) - .end(function (err, res) { - if (err) return done(err); - res.header.should.have.property('allow', 'HEAD, GET'); - const allowHeaders = res.res.rawHeaders.filter( - (item) => item === 'Allow' - ); - expect(allowHeaders.length).to.eql(1); - done(); - }); - }); + router.get(''); + + await request(http.createServer(app.callback())).get('/users'); +}); + +it('supports custom routing detect path: ctx.routerPath', async () => { + const app = new Koa(); + const router = new Router(); + app.use((ctx, next) => { + // bind helloworld.example.com/users => example.com/helloworld/users + const appname = ctx.request.hostname.split('.', 1)[0]; + ctx.newRouterPath = '/' + appname + ctx.path; + return next(); + }); + app.use(router.routes()); + router.get('/helloworld/users', (ctx) => { + ctx.body = ctx.method + ' ' + ctx.url; }); - it('allowedMethods check if flow (allowedArr.length)', function (done) { - const app = new Koa(); - const router = new Router(); - app.use(router.routes()); - app.use(router.allowedMethods()); + await request(http.createServer(app.callback())) + .get('/users') + .set('Host', 'helloworld.example.com') + .expect(200) + .expect('GET /users'); +}); + +it('parameter added to request in ctx', async () => { + const app = new Koa(); + const router = new Router(); + router.get('/echo/:saying', (ctx) => { + try { + assert.strictEqual(ctx.params.saying, 'helloWorld'); + assert.strictEqual(ctx.request.params.saying, 'helloWorld'); + ctx.body = { echo: ctx.params.saying }; + } catch (err) { + ctx.status = 500; + ctx.body = err.message; + } + }); + app.use(router.routes()); + const res = await request(http.createServer(app.callback())) + .get('/echo/helloWorld') + .expect(200); + + assert.deepStrictEqual(res.body, { echo: 'helloWorld' }); +}); + +it('two routes with the same path', async () => { + const app = new Koa(); + const router1 = new Router(); + const router2 = new Router(); + router1.get('/echo/:saying', (ctx, next) => { + try { + assert.strictEqual(ctx.params.saying, 'helloWorld'); + assert.strictEqual(ctx.request.params.saying, 'helloWorld'); + next(); + } catch (err) { + ctx.status = 500; + ctx.body = err.message; + } + }); + router2.get('/echo/:saying', (ctx) => { + try { + assert.strictEqual(ctx.params.saying, 'helloWorld'); + assert.strictEqual(ctx.request.params.saying, 'helloWorld'); + ctx.body = { echo: ctx.params.saying }; + } catch (err) { + ctx.status = 500; + ctx.body = err.message; + } + }); + app.use(router1.routes()); + app.use(router2.routes()); + const res = await request(http.createServer(app.callback())) + .get('/echo/helloWorld') + .expect(200); + + assert.deepStrictEqual(res.body, { echo: 'helloWorld' }); +}); + +it('parameter added to request in ctx with sub router', async () => { + const app = new Koa(); + const router = new Router(); + const subrouter = new Router(); - router.get(''); + router.use((ctx, next) => { + ctx.foo = 'boo'; + return next(); + }); - request(http.createServer(app.callback())) - .get('/users') - .end(() => done()); + subrouter.get('/:saying', (ctx) => { + try { + assert.strictEqual(ctx.params.saying, 'helloWorld'); + assert.strictEqual(ctx.request.params.saying, 'helloWorld'); + ctx.body = { echo: ctx.params.saying }; + } catch (err) { + ctx.status = 500; + ctx.body = err.message; + } }); - it('supports custom routing detect path: ctx.routerPath', function (done) { + router.use('/echo', subrouter.routes()); + app.use(router.routes()); + const res = await request(http.createServer(app.callback())) + .get('/echo/helloWorld') + .expect(200); + + assert.deepStrictEqual(res.body, { echo: 'helloWorld' }); +}); + +describe('Router#[verb]()', () => { + it('registers route specific to HTTP verb', () => { const app = new Koa(); const router = new Router(); - app.use(function (ctx, next) { - // bind helloworld.example.com/users => example.com/helloworld/users - const appname = ctx.request.hostname.split('.', 1)[0]; - ctx.newRouterPath = '/' + appname + ctx.path; - return next(); - }); app.use(router.routes()); - router.get('/helloworld/users', function (ctx) { - ctx.body = ctx.method + ' ' + ctx.url; - }); + for (const method of methods) { + assert.strictEqual(method in router, true); + assert.strictEqual(typeof router[method], 'function'); + router[method]('/', () => {}); + } - request(http.createServer(app.callback())) - .get('/users') - .set('Host', 'helloworld.example.com') - .expect(200) - .expect('GET /users', done); + assert.strictEqual(router.stack.length, methods.length); }); - it('parameter added to request in ctx', function (done) { - const app = new Koa(); + it('registers route with a regexp path', () => { const router = new Router(); - router.get('/echo/:saying', function (ctx) { - try { - expect(ctx.params.saying).eql('helloWorld'); - expect(ctx.request.params.saying).eql('helloWorld'); - ctx.body = { echo: ctx.params.saying }; - } catch (err) { - ctx.status = 500; - ctx.body = err.message; - } + for (const method of methods) { + assert.strictEqual( + router[method](/^\/\w$/i, () => {}), + router + ); + } + }); + + it('registers route with a given name', () => { + const router = new Router(); + for (const method of methods) { + assert.strictEqual( + router[method](method, '/', () => {}), + router + ); + } + }); + + it('registers route with with a given name and regexp path', () => { + const router = new Router(); + for (const method of methods) { + assert.strictEqual( + router[method](method, /^\/$/i, () => {}), + router + ); + } + }); + + it('enables route chaining', () => { + const router = new Router(); + for (const method of methods) { + assert.strictEqual( + router[method]('/', () => {}), + router + ); + } + }); + + it('registers array of paths (gh-203)', () => { + const router = new Router(); + router.get(['/one', '/two'], (ctx, next) => { + return next(); }); - app.use(router.routes()); - request(http.createServer(app.callback())) - .get('/echo/helloWorld') - .expect(200) - .end(function (err, res) { - if (err) return done(err); - expect(res.body).to.eql({ echo: 'helloWorld' }); - done(); - }); + assert.strictEqual(router.stack.length, 2); + assert.strictEqual(router.stack[0].path, '/one'); + assert.strictEqual(router.stack[1].path, '/two'); }); - it('two routes with the same path', function (done) { + it('resolves non-parameterized routes without attached parameters', async () => { const app = new Koa(); - const router1 = new Router(); - const router2 = new Router(); - router1.get('/echo/:saying', function (ctx, next) { - try { - expect(ctx.params.saying).eql('helloWorld'); - expect(ctx.request.params.saying).eql('helloWorld'); - next(); - } catch (err) { - ctx.status = 500; - ctx.body = err.message; - } + const router = new Router(); + + router.get('/notparameter', (ctx) => { + ctx.body = { + param: ctx.params.parameter + }; + }); + + router.get('/:parameter', (ctx) => { + ctx.body = { + param: ctx.params.parameter + }; }); - router2.get('/echo/:saying', function (ctx) { + + app.use(router.routes()); + const res = await request(http.createServer(app.callback())) + .get('/notparameter') + .expect(200); + assert.strictEqual('param' in res.body, false); + }); + + it('correctly returns an error when not passed a path for verb-specific registration (gh-147)', () => { + const router = new Router(); + for (const el of methods) { try { - expect(ctx.params.saying).eql('helloWorld'); - expect(ctx.request.params.saying).eql('helloWorld'); - ctx.body = { echo: ctx.params.saying }; + router[el](() => {}); } catch (err) { - ctx.status = 500; - ctx.body = err.message; + assert.strictEqual( + err.message, + `You have to provide a path when adding a ${el} handler` + ); } - }); - app.use(router1.routes()); - app.use(router2.routes()); - request(http.createServer(app.callback())) - .get('/echo/helloWorld') - .expect(200) - .end(function (err, res) { - if (err) return done(err); - expect(res.body).to.eql({ echo: 'helloWorld' }); - done(); - }); + } + }); + + it('correctly returns an error when not passed a path for "all" registration (gh-147)', () => { + const router = new Router(); + try { + router.all(() => {}); + } catch (err) { + assert.strictEqual( + err.message, + 'You have to provide a path when adding an all handler' + ); + } }); +}); - it('parameter added to request in ctx with sub router', function (done) { +describe('Router#use()', () => { + it('uses router middleware without path', async () => { const app = new Koa(); const router = new Router(); - const subrouter = new Router(); - router.use(function (ctx, next) { - ctx.foo = 'boo'; + router.use((ctx, next) => { + ctx.foo = 'baz'; return next(); }); - subrouter.get('/:saying', function (ctx) { - try { - expect(ctx.params.saying).eql('helloWorld'); - expect(ctx.request.params.saying).eql('helloWorld'); - ctx.body = { echo: ctx.params.saying }; - } catch (err) { - ctx.status = 500; - ctx.body = err.message; - } + router.use((ctx, next) => { + ctx.foo = 'foo'; + return next(); + }); + + router.get('/foo/bar', (ctx) => { + ctx.body = { + foobar: ctx.foo + 'bar' + }; }); - router.use('/echo', subrouter.routes()); app.use(router.routes()); - request(http.createServer(app.callback())) - .get('/echo/helloWorld') - .expect(200) - .end(function (err, res) { - if (err) return done(err); - expect(res.body).to.eql({ echo: 'helloWorld' }); - done(); - }); + const res = await request(http.createServer(app.callback())) + .get('/foo/bar') + .expect(200); + + assert.strictEqual(res.body.foobar, 'foobar'); }); - describe('Router#[verb]()', function () { - it('registers route specific to HTTP verb', function () { - const app = new Koa(); - const router = new Router(); - app.use(router.routes()); - for (const method of methods) { - router.should.have.property(method); - router[method].should.be.type('function'); - router[method]('/', function () {}); - } + it('uses router middleware at given path', async () => { + const app = new Koa(); + const router = new Router(); - router.stack.should.have.length(methods.length); + router.use('/foo/bar', (ctx, next) => { + ctx.foo = 'foo'; + return next(); }); - it('registers route with a regexp path', function () { - const router = new Router(); - for (const method of methods) { - router[method](/^\/\w$/i, function () {}).should.equal(router); - } + router.get('/foo/bar', (ctx) => { + ctx.body = { + foobar: ctx.foo + 'bar' + }; }); - it('registers route with a given name', function () { - const router = new Router(); - for (const method of methods) { - router[method](method, '/', function () {}).should.equal(router); - } - }); + app.use(router.routes()); + const res = await request(http.createServer(app.callback())) + .get('/foo/bar') + .expect(200); - it('registers route with with a given name and regexp path', function () { - const router = new Router(); - for (const method of methods) { - router[method](method, /^\/$/i, function () {}).should.equal(router); - } - }); + assert.strictEqual(res.body.foobar, 'foobar'); + }); - it('enables route chaining', function () { - const router = new Router(); - for (const method of methods) { - router[method]('/', function () {}).should.equal(router); - } - }); + it('runs router middleware before subrouter middleware', async () => { + const app = new Koa(); + const router = new Router(); + const subrouter = new Router(); - it('registers array of paths (gh-203)', function () { - const router = new Router(); - router.get(['/one', '/two'], function (ctx, next) { - return next(); - }); - expect(router.stack).to.have.property('length', 2); - expect(router.stack[0]).to.have.property('path', '/one'); - expect(router.stack[1]).to.have.property('path', '/two'); + router.use((ctx, next) => { + ctx.foo = 'boo'; + return next(); }); - it('resolves non-parameterized routes without attached parameters', function (done) { - const app = new Koa(); - const router = new Router(); - - router.get('/notparameter', function (ctx) { + subrouter + .use((ctx, next) => { + ctx.foo = 'foo'; + return next(); + }) + .get('/bar', (ctx) => { ctx.body = { - param: ctx.params.parameter + foobar: ctx.foo + 'bar' }; }); - router.get('/:parameter', function (ctx) { - ctx.body = { - param: ctx.params.parameter - }; - }); + router.use('/foo', subrouter.routes()); + app.use(router.routes()); + const res = await request(http.createServer(app.callback())) + .get('/foo/bar') + .expect(200); - app.use(router.routes()); - request(http.createServer(app.callback())) - .get('/notparameter') - .expect(200) - .end(function (err, res) { - if (err) return done(err); + assert.strictEqual(res.body.foobar, 'foobar'); + }); - expect(res.body.param).to.equal(undefined); - done(); - }); + it('assigns middleware to array of paths', async () => { + const app = new Koa(); + const router = new Router(); + + router.use(['/foo', '/bar'], (ctx, next) => { + ctx.foo = 'foo'; + ctx.bar = 'bar'; + return next(); }); - it('correctly returns an error when not passed a path for verb-specific registration (gh-147)', function () { - const router = new Router(); - for (const el of methods) { - try { - router[el](function () {}); - } catch (err) { - expect(err.message).to.be( - `You have to provide a path when adding a ${el} handler` - ); - } - } + router.get('/foo', (ctx) => { + ctx.body = { + foobar: ctx.foo + 'bar' + }; }); - it('correctly returns an error when not passed a path for "all" registration (gh-147)', function () { - const router = new Router(); - try { - router.all(function () {}); - } catch (err) { - expect(err.message).to.be( - 'You have to provide a path when adding an all handler' - ); - } + router.get('/bar', (ctx) => { + ctx.body = { + foobar: 'foo' + ctx.bar + }; }); + + app.use(router.routes()); + const res = await request(http.createServer(app.callback())) + .get('/foo') + .expect(200); + assert.strictEqual(res.body.foobar, 'foobar'); + + const secondRes = await request(http.createServer(app.callback())) + .get('/bar') + .expect(200); + + assert.strictEqual(secondRes.body.foobar, 'foobar'); }); +}); - describe('Router#use()', function () { - it('uses router middleware without path', function (done) { - const app = new Koa(); - const router = new Router(); +it('without path, does not set params.0 to the matched path - gh-247', async () => { + const app = new Koa(); + const router = new Router(); - router.use(function (ctx, next) { - ctx.foo = 'baz'; - return next(); - }); + router.use((ctx, next) => { + return next(); + }); - router.use(function (ctx, next) { - ctx.foo = 'foo'; - return next(); - }); + router.get('/foo/:id', (ctx) => { + ctx.body = ctx.params; + }); - router.get('/foo/bar', function (ctx) { - ctx.body = { - foobar: ctx.foo + 'bar' - }; - }); + app.use(router.routes()); + const res = await request(http.createServer(app.callback())) + .get('/foo/815') + .expect(200); - app.use(router.routes()); - request(http.createServer(app.callback())) - .get('/foo/bar') - .expect(200) - .end(function (err, res) { - if (err) return done(err); + assert.strictEqual(res.body.id, '815'); + assert.strictEqual('0' in res.body, false); +}); - expect(res.body).to.have.property('foobar', 'foobar'); - done(); - }); - }); +it('does not add an erroneous (.*) to unprefiexed nested routers - gh-369 gh-410', async () => { + const app = new Koa(); + const router = new Router(); + const nested = new Router(); + let called = 0; - it('uses router middleware at given path', function (done) { - const app = new Koa(); - const router = new Router(); + nested + .get('/', (ctx, next) => { + ctx.body = 'root'; + called += 1; + return next(); + }) + .get('/test', (ctx, next) => { + ctx.body = 'test'; + called += 1; + return next(); + }); - router.use('/foo/bar', function (ctx, next) { - ctx.foo = 'foo'; - return next(); - }); + router.use(nested.routes()); + app.use(router.routes()); - router.get('/foo/bar', function (ctx) { - ctx.body = { - foobar: ctx.foo + 'bar' - }; - }); + await request(app.callback()).get('/test').expect(200).expect('test'); - app.use(router.routes()); - request(http.createServer(app.callback())) - .get('/foo/bar') - .expect(200) - .end(function (err, res) { - if (err) return done(err); + assert.strictEqual(called, 1, 'too many routes matched'); +}); - expect(res.body).to.have.property('foobar', 'foobar'); - done(); - }); - }); +it('assigns middleware to array of paths with function middleware and router need to nest. - gh-22', async () => { + const app = new Koa(); + const base = new Router({ prefix: '/api' }); + const nested = new Router({ prefix: '/qux' }); + const pathList = ['/foo', '/bar']; + + nested.get('/baz', (ctx) => { + ctx.body = { + foo: ctx.foo, + bar: ctx.bar, + baz: 'baz' + }; + }); - it('runs router middleware before subrouter middleware', function (done) { - const app = new Koa(); - const router = new Router(); - const subrouter = new Router(); + base.use( + pathList, + (ctx, next) => { + ctx.foo = 'foo'; + ctx.bar = 'bar'; - router.use(function (ctx, next) { - ctx.foo = 'boo'; - return next(); - }); + return next(); + }, + nested.routes() + ); - subrouter - .use(function (ctx, next) { - ctx.foo = 'foo'; - return next(); - }) - .get('/bar', function (ctx) { - ctx.body = { - foobar: ctx.foo + 'bar' - }; - }); + app.use(base.routes()); - router.use('/foo', subrouter.routes()); - app.use(router.routes()); + await Promise.all( + pathList.map((pathname) => request(http.createServer(app.callback())) - .get('/foo/bar') + .get(`/api${pathname}/qux/baz`) .expect(200) - .end(function (err, res) { - if (err) return done(err); + ) + ).then((resList) => { + for (const res of resList) { + assert.deepStrictEqual(res.body, { foo: 'foo', bar: 'bar', baz: 'baz' }); + } + }); +}); - expect(res.body).to.have.property('foobar', 'foobar'); - done(); - }); - }); +it('uses a same router middleware at given paths continuously - ZijianHe/koa-router#gh-244 gh-18', async () => { + const app = new Koa(); + const base = new Router({ prefix: '/api' }); + const nested = new Router({ prefix: '/qux' }); - it('assigns middleware to array of paths', function (done) { - const app = new Koa(); - const router = new Router(); + nested.get('/baz', (ctx) => { + ctx.body = { + foo: ctx.foo, + bar: ctx.bar, + baz: 'baz' + }; + }); - router.use(['/foo', '/bar'], function (ctx, next) { + base + .use( + '/foo', + (ctx, next) => { ctx.foo = 'foo'; ctx.bar = 'bar'; - return next(); - }); - - router.get('/foo', function (ctx) { - ctx.body = { - foobar: ctx.foo + 'bar' - }; - }); - router.get('/bar', function (ctx) { - ctx.body = { - foobar: 'foo' + ctx.bar - }; - }); - - app.use(router.routes()); - request(http.createServer(app.callback())) - .get('/foo') - .expect(200) - .end(function (err, res) { - if (err) return done(err); - expect(res.body).to.have.property('foobar', 'foobar'); - request(http.createServer(app.callback())) - .get('/bar') - .expect(200) - .end(function (err, res) { - if (err) return done(err); - expect(res.body).to.have.property('foobar', 'foobar'); - done(); - }); - }); - }); - - it('without path, does not set params.0 to the matched path - gh-247', function (done) { - const app = new Koa(); - const router = new Router(); + return next(); + }, + nested.routes() + ) + .use( + '/bar', + (ctx, next) => { + ctx.foo = 'foo'; + ctx.bar = 'bar'; - router.use(function (ctx, next) { return next(); - }); + }, + nested.routes() + ); - router.get('/foo/:id', function (ctx) { - ctx.body = ctx.params; - }); + app.use(base.routes()); - app.use(router.routes()); + await Promise.all( + ['/foo', '/bar'].map((pathname) => request(http.createServer(app.callback())) - .get('/foo/815') + .get(`/api${pathname}/qux/baz`) .expect(200) - .end(function (err, res) { - if (err) return done(err); - - expect(res.body).to.have.property('id', '815'); - expect(res.body).to.not.have.property('0'); - done(); - }); - }); - - it('does not add an erroneous (.*) to unprefiexed nested routers - gh-369 gh-410', function (done) { - const app = new Koa(); - const router = new Router(); - const nested = new Router(); - let called = 0; + ) + ).then((resList) => { + for (const res of resList) { + assert.deepStrictEqual(res.body, { foo: 'foo', bar: 'bar', baz: 'baz' }); + } + }); +}); - nested - .get('/', (ctx, next) => { - ctx.body = 'root'; - called += 1; - return next(); - }) - .get('/test', (ctx, next) => { - ctx.body = 'test'; - called += 1; - return next(); - }); +describe('Router#register()', () => { + it('registers new routes', async () => { + const app = new Koa(); + const router = new Router(); + assert.strictEqual('register' in router, true); + assert.strictEqual(typeof router.register, 'function'); + router.register('/', ['GET', 'POST'], () => {}); + app.use(router.routes()); + assert.strictEqual(Array.isArray(router.stack), true); + assert.strictEqual(router.stack.length, 1); + assert.strictEqual(router.stack[0].path, '/'); + }); +}); - router.use(nested.routes()); - app.use(router.routes()); +describe('Router#redirect()', () => { + it('registers redirect routes', async () => { + const app = new Koa(); + const router = new Router(); + assert.strictEqual('redirect' in router, true); + assert.strictEqual(typeof router.redirect, 'function'); + router.redirect('/source', '/destination', 302); + app.use(router.routes()); + assert.strictEqual(router.stack.length, 1); + assert.strictEqual(router.stack[0] instanceof Layer, true); + assert.strictEqual(router.stack[0].path, '/source'); + }); - request(app.callback()) - .get('/test') - .expect(200) - .expect('test') - .end(function (err) { - if (err) return done(err); - expect(called).to.eql(1, 'too many routes matched'); - done(); - }); - }); + it('redirects using route names', async () => { + const app = new Koa(); + const router = new Router(); + app.use(router.routes()); + router.get('home', '/', () => {}); + router.get('sign-up-form', '/sign-up-form', () => {}); + router.redirect('home', 'sign-up-form'); + const res = await request(http.createServer(app.callback())) + .post('/') + .expect(301); + assert.strictEqual(res.header.location, '/sign-up-form'); + }); +}); - it('assigns middleware to array of paths with function middleware and router need to nest. - gh-22', function (done) { - const app = new Koa(); - const base = new Router({ prefix: '/api' }); - const nested = new Router({ prefix: '/qux' }); - const pathList = ['/foo', '/bar']; +it('redirects using symbols as route names', async () => { + const app = new Koa(); + const router = new Router(); + app.use(router.routes()); + const homeSymbol = Symbol('home'); + const signUpFormSymbol = Symbol('sign-up-form'); + router.get(homeSymbol, '/', () => {}); + router.get(signUpFormSymbol, '/sign-up-form', () => {}); + router.redirect(homeSymbol, signUpFormSymbol); + const res = await request(http.createServer(app.callback())) + .post('/') + .expect(301); + assert.strictEqual(res.header.location, '/sign-up-form'); +}); - nested.get('/baz', (ctx) => { - ctx.body = { - foo: ctx.foo, - bar: ctx.bar, - baz: 'baz' - }; - }); +it('throws an error if no route is found for name', () => { + const router = new Router(); + assert.throws(() => router.redirect('missing', '/destination')); + assert.throws(() => router.redirect('/source', 'missing')); + assert.throws(() => router.redirect(Symbol('missing'), '/destination')); + assert.throws(() => router.redirect('/source', Symbol('missing'))); +}); - base.use( - pathList, - (ctx, next) => { - ctx.foo = 'foo'; - ctx.bar = 'bar'; +it('redirects to external sites', async () => { + const app = new Koa(); + const router = new Router(); + app.use(router.routes()); + router.redirect('/', 'https://www.example.com'); + const res = await request(http.createServer(app.callback())) + .post('/') + .expect(301); + assert.strictEqual(res.header.location, 'https://www.example.com/'); +}); - return next(); - }, - nested.routes() - ); +it('redirects to any external protocol', async () => { + const app = new Koa(); + const router = new Router(); + app.use(router.routes()); + router.redirect('/', 'my-custom-app-protocol://www.example.com/foo'); + const res = await request(http.createServer(app.callback())) + .post('/') + .expect(301); + + assert.strictEqual( + res.header.location, + 'my-custom-app-protocol://www.example.com/foo' + ); +}); - app.use(base.routes()); - - Promise.all( - pathList.map((pathname) => { - return request(http.createServer(app.callback())) - .get(`/api${pathname}/qux/baz`) - .expect(200); - }) - ).then( - (resList) => { - for (const res of resList) { - assert.deepEqual(res.body, { foo: 'foo', bar: 'bar', baz: 'baz' }); - } - - done(); - }, - (err) => done(err) - ); +describe('Router#route()', () => { + it('inherits routes from nested router', () => { + const subrouter = new Router().get('child', '/hello', (ctx) => { + ctx.body = { hello: 'world' }; }); + const router = new Router().use(subrouter.routes()); + assert.strictEqual(router.route('child').name, 'child'); + }); - it('uses a same router middleware at given paths continuously - ZijianHe/koa-router#gh-244 gh-18', function (done) { - const app = new Koa(); - const base = new Router({ prefix: '/api' }); - const nested = new Router({ prefix: '/qux' }); - - nested.get('/baz', (ctx) => { - ctx.body = { - foo: ctx.foo, - bar: ctx.bar, - baz: 'baz' - }; - }); - - base - .use( - '/foo', - (ctx, next) => { - ctx.foo = 'foo'; - ctx.bar = 'bar'; - - return next(); - }, - nested.routes() - ) - .use( - '/bar', - (ctx, next) => { - ctx.foo = 'foo'; - ctx.bar = 'bar'; - - return next(); - }, - nested.routes() - ); - - app.use(base.routes()); - - Promise.all( - ['/foo', '/bar'].map((pathname) => { - return request(http.createServer(app.callback())) - .get(`/api${pathname}/qux/baz`) - .expect(200); - }) - ).then( - (resList) => { - for (const res of resList) { - assert.deepEqual(res.body, { foo: 'foo', bar: 'bar', baz: 'baz' }); - } - - done(); - }, - (err) => done(err) - ); + it('supports symbols as names', () => { + const childSymbol = Symbol('child'); + const subrouter = new Router().get(childSymbol, '/hello', (ctx) => { + ctx.body = { hello: 'world' }; }); + const router = new Router().use(subrouter.routes()); + assert.strictEqual(router.route(childSymbol).name, childSymbol); }); - describe('Router#register()', function () { - it('registers new routes', function (done) { - const app = new Koa(); - const router = new Router(); - router.should.have.property('register'); - router.register.should.be.type('function'); - router.register('/', ['GET', 'POST'], function () {}); - app.use(router.routes()); - router.stack.should.be.an.instanceOf(Array); - router.stack.should.have.property('length', 1); - router.stack[0].should.have.property('path', '/'); - done(); + it('returns false if no name matches', () => { + const router = new Router(); + router.get('books', '/books', (ctx) => { + ctx.status = 204; + }); + router.get(Symbol('Picard'), '/enterprise', (ctx) => { + ctx.status = 204; }); + assert.strictEqual(Boolean(router.route('Picard')), false); + assert.strictEqual(Boolean(router.route(Symbol('books'))), false); }); +}); - describe('Router#redirect()', function () { - it('registers redirect routes', function (done) { - const app = new Koa(); - const router = new Router(); - router.should.have.property('redirect'); - router.redirect.should.be.type('function'); - router.redirect('/source', '/destination', 302); - app.use(router.routes()); - router.stack.should.have.property('length', 1); - router.stack[0].should.be.instanceOf(Layer); - router.stack[0].should.have.property('path', '/source'); - done(); +describe('Router#url()', () => { + it('generates URL for given route name', () => { + const app = new Koa(); + const router = new Router(); + app.use(router.routes()); + router.get('books', '/:category/:title', (ctx) => { + ctx.status = 204; }); - - it('redirects using route names', function (done) { - const app = new Koa(); - const router = new Router(); - app.use(router.routes()); - router.get('home', '/', function () {}); - router.get('sign-up-form', '/sign-up-form', function () {}); - router.redirect('home', 'sign-up-form'); - request(http.createServer(app.callback())) - .post('/') - .expect(301) - .end(function (err, res) { - if (err) return done(err); - res.header.should.have.property('location', '/sign-up-form'); - done(); - }); + let url = router.url( + 'books', + { category: 'programming', title: 'how to node' }, + { encode: encodeURIComponent } + ); + assert.strictEqual(url, '/programming/how%20to%20node'); + url = router.url('books', 'programming', 'how to node', { + encode: encodeURIComponent }); + assert.strictEqual(url, '/programming/how%20to%20node'); + }); - it('redirects using symbols as route names', function (done) { - const app = new Koa(); - const router = new Router(); - app.use(router.routes()); - const homeSymbol = Symbol('home'); - const signUpFormSymbol = Symbol('sign-up-form'); - router.get(homeSymbol, '/', function () {}); - router.get(signUpFormSymbol, '/sign-up-form', function () {}); - router.redirect(homeSymbol, signUpFormSymbol); - request(http.createServer(app.callback())) - .post('/') - .expect(301) - .end(function (err, res) { - if (err) return done(err); - res.header.should.have.property('location', '/sign-up-form'); - done(); - }); + it('generates URL for given route name within embedded routers', () => { + const app = new Koa(); + const router = new Router({ + prefix: '/books' }); - it('throws an error if no route is found for name', function () { - const router = new Router(); - expect(() => router.redirect('missing', '/destination')).to.throwError(); - expect(() => router.redirect('/source', 'missing')).to.throwError(); - expect(() => - router.redirect(Symbol('missing'), '/destination') - ).to.throwError(); - expect(() => - router.redirect('/source', Symbol('missing')) - ).to.throwError(); + const embeddedRouter = new Router({ + prefix: '/chapters' }); - - it('redirects to external sites', function (done) { - const app = new Koa(); - const router = new Router(); - app.use(router.routes()); - router.redirect('/', 'https://www.example.com'); - request(http.createServer(app.callback())) - .post('/') - .expect(301) - .end(function (err, res) { - if (err) return done(err); - res.header.should.have.property( - 'location', - 'https://www.example.com' - ); - done(); - }); + embeddedRouter.get('chapters', '/:chapterName/:pageNumber', (ctx) => { + ctx.status = 204; }); - - it('redirects to any external protocol', function (done) { - const app = new Koa(); - const router = new Router(); - app.use(router.routes()); - router.redirect('/', 'my-custom-app-protocol://www.example.com/foo'); - request(http.createServer(app.callback())) - .post('/') - .expect(301) - .end(function (err, res) { - if (err) return done(err); - res.header.should.have.property( - 'location', - 'my-custom-app-protocol://www.example.com/foo' - ); - done(); - }); + router.use(embeddedRouter.routes()); + app.use(router.routes()); + let url = router.url( + 'chapters', + { chapterName: 'Learning ECMA6', pageNumber: 123 }, + { encode: encodeURIComponent } + ); + assert.strictEqual(url, '/books/chapters/Learning%20ECMA6/123'); + url = router.url('chapters', 'Learning ECMA6', 123, { + encode: encodeURIComponent }); + assert.strictEqual(url, '/books/chapters/Learning%20ECMA6/123'); }); - describe('Router#route()', function () { - it('inherits routes from nested router', function () { - const subrouter = Router().get('child', '/hello', function (ctx) { - ctx.body = { hello: 'world' }; - }); - const router = Router().use(subrouter.routes()); - expect(router.route('child')).to.have.property('name', 'child'); + it('generates URL for given route name within two embedded routers', () => { + const app = new Koa(); + const router = new Router({ + prefix: '/books' }); - - it('supports symbols as names', function () { - const childSymbol = Symbol('child'); - const subrouter = Router().get(childSymbol, '/hello', function (ctx) { - ctx.body = { hello: 'world' }; - }); - const router = Router().use(subrouter.routes()); - expect(router.route(childSymbol)).to.have.property('name', childSymbol); + const embeddedRouter = new Router({ + prefix: '/chapters' }); - - it('returns false if no name matches', function () { - const router = new Router(); - router.get('books', '/books', function (ctx) { - ctx.status = 204; - }); - router.get(Symbol('Picard'), '/enterprise', function (ctx) { - ctx.status = 204; - }); - router.route('Picard').should.be.false(); - router.route(Symbol('books')).should.be.false(); + const embeddedRouter2 = new Router({ + prefix: '/:chapterName/pages' }); - }); - - describe('Router#url()', function () { - it('generates URL for given route name', function (done) { - const app = new Koa(); - const router = new Router(); - app.use(router.routes()); - router.get('books', '/:category/:title', function (ctx) { - ctx.status = 204; - }); - let url = router.url( - 'books', - { category: 'programming', title: 'how to node' }, - { encode: encodeURIComponent } - ); - url.should.equal('/programming/how%20to%20node'); - url = router.url('books', 'programming', 'how to node', { - encode: encodeURIComponent - }); - url.should.equal('/programming/how%20to%20node'); - done(); + embeddedRouter2.get('chapters', '/:pageNumber', (ctx) => { + ctx.status = 204; }); + embeddedRouter.use(embeddedRouter2.routes()); + router.use(embeddedRouter.routes()); + app.use(router.routes()); + const url = router.url( + 'chapters', + { chapterName: 'Learning ECMA6', pageNumber: 123 }, + { encode: encodeURIComponent } + ); + assert.strictEqual(url, '/books/chapters/Learning%20ECMA6/pages/123'); + }); - it('generates URL for given route name within embedded routers', function (done) { - const app = new Koa(); - const router = new Router({ - prefix: '/books' - }); + it('generates URL for given route name with params and query params', () => { + const router = new Router(); + const query = { page: 3, limit: 10 }; - const embeddedRouter = new Router({ - prefix: '/chapters' - }); - embeddedRouter.get( - 'chapters', - '/:chapterName/:pageNumber', - function (ctx) { - ctx.status = 204; - } - ); - router.use(embeddedRouter.routes()); - app.use(router.routes()); - let url = router.url( - 'chapters', - { chapterName: 'Learning ECMA6', pageNumber: 123 }, - { encode: encodeURIComponent } - ); - url.should.equal('/books/chapters/Learning%20ECMA6/123'); - url = router.url('chapters', 'Learning ECMA6', 123, { - encode: encodeURIComponent - }); - url.should.equal('/books/chapters/Learning%20ECMA6/123'); - done(); + router.get('books', '/books/:category/:id', (ctx) => { + ctx.status = 204; }); + let url = router.url('books', 'programming', 4, { query }); + assert.strictEqual(url, '/books/programming/4?page=3&limit=10'); + url = router.url('books', { category: 'programming', id: 4 }, { query }); + assert.strictEqual(url, '/books/programming/4?page=3&limit=10'); + url = router.url( + 'books', + { category: 'programming', id: 4 }, + { query: 'page=3&limit=10' } + ); + assert.strictEqual(url, '/books/programming/4?page=3&limit=10'); + }); - it('generates URL for given route name within two embedded routers', function (done) { - const app = new Koa(); - const router = new Router({ - prefix: '/books' - }); - const embeddedRouter = new Router({ - prefix: '/chapters' - }); - const embeddedRouter2 = new Router({ - prefix: '/:chapterName/pages' - }); - embeddedRouter2.get('chapters', '/:pageNumber', function (ctx) { - ctx.status = 204; - }); - embeddedRouter.use(embeddedRouter2.routes()); - router.use(embeddedRouter.routes()); - app.use(router.routes()); - const url = router.url( - 'chapters', - { chapterName: 'Learning ECMA6', pageNumber: 123 }, - { encode: encodeURIComponent } - ); - url.should.equal('/books/chapters/Learning%20ECMA6/pages/123'); - done(); + it('generates URL for given route name without params and query params', () => { + let url; + const router = new Router(); + router.get('books', '/books', (ctx) => { + ctx.status = 204; }); + url = router.url('books'); + assert.strictEqual(url, '/books'); + url = router.url('books'); + assert.strictEqual(url, '/books', {}); + url = router.url('books'); + assert.strictEqual(url, '/books', {}, {}); + url = router.url('books', {}, { query: { page: 3, limit: 10 } }); + assert.strictEqual(url, '/books?page=3&limit=10'); + url = router.url('books', {}, { query: 'page=3&limit=10' }); + assert.strictEqual(url, '/books?page=3&limit=10'); + }); - it('generates URL for given route name with params and query params', function (done) { - const router = new Router(); - const query = { page: 3, limit: 10 }; - - router.get('books', '/books/:category/:id', function (ctx) { - ctx.status = 204; - }); - let url = router.url('books', 'programming', 4, { query }); - url.should.equal('/books/programming/4?page=3&limit=10'); - url = router.url('books', { category: 'programming', id: 4 }, { query }); - url.should.equal('/books/programming/4?page=3&limit=10'); - url = router.url( - 'books', - { category: 'programming', id: 4 }, - { query: 'page=3&limit=10' } - ); - url.should.equal('/books/programming/4?page=3&limit=10'); - done(); + it('generates URL for given route name without params and query params', () => { + const router = new Router(); + router.get('category', '/category', (ctx) => { + ctx.status = 204; }); - - it('generates URL for given route name without params and query params', function (done) { - const router = new Router(); - router.get('books', '/books', function (ctx) { - ctx.status = 204; - }); - var url = router.url('books'); - url.should.equal('/books'); - var url = router.url('books'); - url.should.equal('/books', {}); - var url = router.url('books'); - url.should.equal('/books', {}, {}); - var url = router.url('books', {}, { query: { page: 3, limit: 10 } }); - url.should.equal('/books?page=3&limit=10'); - var url = router.url('books', {}, { query: 'page=3&limit=10' }); - url.should.equal('/books?page=3&limit=10'); - done(); - }); - - it('generates URL for given route name without params and query params', function (done) { - const router = new Router(); - router.get('category', '/category', function (ctx) { - ctx.status = 204; - }); - const url = router.url('category', { - query: { page: 3, limit: 10 } - }); - url.should.equal('/category?page=3&limit=10'); - done(); + const url = router.url('category', { + query: { page: 3, limit: 10 } }); + assert.strictEqual(url, '/category?page=3&limit=10'); + }); - it('returns an Error if no route is found for name', function () { - const app = new Koa(); - const router = new Router(); - app.use(router.routes()); - router.get('books', '/books', function (ctx) { - ctx.status = 204; - }); - router.get(Symbol('Picard'), '/enterprise', function (ctx) { - ctx.status = 204; - }); - - router.url('Picard').should.be.Error(); - router.url(Symbol('books')).should.be.Error(); + it('returns an Error if no route is found for name', () => { + const app = new Koa(); + const router = new Router(); + app.use(router.routes()); + router.get('books', '/books', (ctx) => { + ctx.status = 204; }); - - it('escapes using encodeURIComponent()', function () { - const url = Router.url('/:category/:title', { - category: 'programming', - title: 'how to node & js/ts' - }); - url.should.equal('/programming/how%20to%20node%20%26%20js%2Fts'); + router.get(Symbol('Picard'), '/enterprise', (ctx) => { + ctx.status = 204; }); + + assert.strictEqual(router.url('Picard') instanceof Error, true); + assert.strictEqual(router.url(Symbol('books')) instanceof Error, true); }); - describe('Router#param()', function () { - it('runs parameter middleware', function (done) { - const app = new Koa(); - const router = new Router(); - app.use(router.routes()); - router - .param('user', function (id, ctx, next) { - ctx.user = { name: 'alex' }; - if (!id) return (ctx.status = 404); - return next(); - }) - .get('/users/:user', function (ctx) { - ctx.body = ctx.user; - }); - request(http.createServer(app.callback())) - .get('/users/3') - .expect(200) - .end(function (err, res) { - if (err) return done(err); - res.should.have.property('body'); - res.body.should.have.property('name', 'alex'); - done(); - }); + it('escapes using encodeURIComponent()', () => { + const url = Router.url('/:category/:title', { + category: 'programming', + title: 'how to node & js/ts' }); + assert.strictEqual(url, '/programming/how%20to%20node%20%26%20js%2Fts'); + }); +}); - it('runs parameter middleware in order of URL appearance', function (done) { - const app = new Koa(); - const router = new Router(); - router - .param('user', function (id, ctx, next) { - ctx.user = { name: 'alex' }; - if (ctx.ranFirst) { - ctx.user.ordered = 'parameters'; - } - - if (!id) return (ctx.status = 404); - return next(); - }) - .param('first', function (id, ctx, next) { - ctx.ranFirst = true; - if (ctx.user) { - ctx.ranFirst = false; - } - - if (!id) return (ctx.status = 404); - return next(); - }) - .get('/:first/users/:user', function (ctx) { - ctx.body = ctx.user; - }); +describe('Router#param()', () => { + it('runs parameter middleware', async () => { + const app = new Koa(); + const router = new Router(); + app.use(router.routes()); + router + .param('user', (id, ctx, next) => { + ctx.user = { name: 'alex' }; + if (!id) { + ctx.status = 404; + return; + } - request(http.createServer(app.use(router.routes()).callback())) - .get('/first/users/3') - .expect(200) - .end(function (err, res) { - if (err) return done(err); - res.should.have.property('body'); - res.body.should.have.property('name', 'alex'); - res.body.should.have.property('ordered', 'parameters'); - done(); - }); - }); + return next(); + }) + .get('/users/:user', (ctx) => { + ctx.body = ctx.user; + }); + const res = await request(http.createServer(app.callback())) + .get('/users/3') + .expect(200); + assert.strictEqual('body' in res, true); + assert.strictEqual(res.body.name, 'alex'); + }); - it('runs parameter middleware in order of URL appearance even when added in random order', function (done) { - const app = new Koa(); - const router = new Router(); - router - // intentional random order - .param('a', function (id, ctx, next) { - ctx.state.loaded = [id]; - return next(); - }) - .param('d', function (id, ctx, next) { - ctx.state.loaded.push(id); - return next(); - }) - .param('c', function (id, ctx, next) { - ctx.state.loaded.push(id); - return next(); - }) - .param('b', function (id, ctx, next) { - ctx.state.loaded.push(id); - return next(); - }) - .get('/:a/:b/:c/:d', function (ctx, next) { - ctx.body = ctx.state.loaded; - }); + it('runs parameter middleware in order of URL appearance', async () => { + const app = new Koa(); + const router = new Router(); + router + .param('user', (id, ctx, next) => { + ctx.user = { name: 'alex' }; + if (ctx.ranFirst) { + ctx.user.ordered = 'parameters'; + } - request(http.createServer(app.use(router.routes()).callback())) - .get('/1/2/3/4') - .expect(200) - .end(function (err, res) { - if (err) return done(err); - res.should.have.property('body'); - res.body.should.eql(['1', '2', '3', '4']); - done(); - }); - }); + if (!id) { + ctx.status = 404; + return; + } - it('runs parent parameter middleware for subrouter', function (done) { - const app = new Koa(); - const router = new Router(); - const subrouter = new Router(); - subrouter.get('/:cid', function (ctx) { - ctx.body = { - id: ctx.params.id, - cid: ctx.params.cid - }; + return next(); + }) + .param('first', (id, ctx, next) => { + ctx.ranFirst = true; + if (ctx.user) { + ctx.ranFirst = false; + } + + if (!id) { + ctx.status = 404; + return; + } + + return next(); + }) + .get('/:first/users/:user', (ctx) => { + ctx.body = ctx.user; }); - router - .param('id', function (id, ctx, next) { - ctx.params.id = 'ran'; - if (!id) return (ctx.status = 404); - return next(); - }) - .use('/:id/children', subrouter.routes()); - request(http.createServer(app.use(router.routes()).callback())) - .get('/did-not-run/children/2') - .expect(200) - .end(function (err, res) { - if (err) return done(err); - res.should.have.property('body'); - res.body.should.have.property('id', 'ran'); - res.body.should.have.property('cid', '2'); - done(); - }); + const res = await request( + http.createServer(app.use(router.routes()).callback()) + ) + .get('/first/users/3') + .expect(200); + assert.deepStrictEqual(res.body, { + name: 'alex', + ordered: 'parameters' }); }); - describe('Router#opts', function () { - it('responds with 200', function (done) { - const app = new Koa(); - const router = new Router({ - strict: true - }); - router.get('/info', function (ctx) { - ctx.body = 'hello'; + it('runs parameter middleware in order of URL appearance even when added in random order', async () => { + const app = new Koa(); + const router = new Router(); + router + // intentional random order + .param('a', (id, ctx, next) => { + ctx.state.loaded = [id]; + return next(); + }) + .param('d', (id, ctx, next) => { + ctx.state.loaded.push(id); + return next(); + }) + .param('c', (id, ctx, next) => { + ctx.state.loaded.push(id); + return next(); + }) + .param('b', (id, ctx, next) => { + ctx.state.loaded.push(id); + return next(); + }) + .get('/:a/:b/:c/:d', (ctx) => { + ctx.body = ctx.state.loaded; }); - request(http.createServer(app.use(router.routes()).callback())) - .get('/info') - .expect(200) - .end(function (err, res) { - if (err) return done(err); - res.text.should.equal('hello'); - done(); - }); - }); - it('should allow setting a prefix', function (done) { - const app = new Koa(); - const routes = Router({ prefix: '/things/:thing_id' }); + const res = await request( + http.createServer(app.use(router.routes()).callback()) + ) + .get('/1/2/3/4') + .expect(200); + assert.strictEqual('body' in res, true); + assert.deepStrictEqual(res.body, ['1', '2', '3', '4']); + }); - routes.get('/list', function (ctx) { - ctx.body = ctx.params; - }); + it('runs parent parameter middleware for subrouter', async () => { + const app = new Koa(); + const router = new Router(); + const subrouter = new Router(); + subrouter.get('/:cid', (ctx) => { + ctx.body = { + id: ctx.params.id, + cid: ctx.params.cid + }; + }); + router + .param('id', (id, ctx, next) => { + ctx.params.id = 'ran'; + if (!id) { + ctx.status = 404; + return; + } - app.use(routes.routes()); + return next(); + }) + .use('/:id/children', subrouter.routes()); - request(http.createServer(app.callback())) - .get('/things/1/list') - .expect(200) - .end(function (err, res) { - if (err) return done(err); - res.body.thing_id.should.equal('1'); - done(); - }); + const res = await request( + http.createServer(app.use(router.routes()).callback()) + ) + .get('/did-not-run/children/2') + .expect(200); + assert.deepStrictEqual(res.body, { + id: 'ran', + cid: '2' }); + }); +}); - it('responds with 404 when has a trailing slash', function (done) { - const app = new Koa(); - const router = new Router({ - strict: true - }); - router.get('/info', function (ctx) { - ctx.body = 'hello'; - }); - request(http.createServer(app.use(router.routes()).callback())) - .get('/info/') - .expect(404) - .end(function (err) { - if (err) return done(err); - done(); - }); +describe('Router#opts', () => { + it('responds with 200', async () => { + const app = new Koa(); + const router = new Router({ + strict: true + }); + router.get('/info', (ctx) => { + ctx.body = 'hello'; + }); + const res = await request( + http.createServer(app.use(router.routes()).callback()) + ) + .get('/info') + .expect(200); + assert.strictEqual(res.text, 'hello'); + }); + + it('should allow setting a prefix', async () => { + const app = new Koa(); + const routes = new Router({ prefix: '/things/:thing_id' }); + + routes.get('/list', (ctx) => { + ctx.body = ctx.params; }); + + app.use(routes.routes()); + + const res = await request(http.createServer(app.callback())) + .get('/things/1/list') + .expect(200); + assert.strictEqual(res.body.thing_id, '1'); }); - describe('use middleware with opts', function () { - it('responds with 200', function (done) { - const app = new Koa(); - const router = new Router({ - strict: true - }); - router.get('/info', function (ctx) { - ctx.body = 'hello'; - }); - request(http.createServer(app.use(router.routes()).callback())) - .get('/info') - .expect(200) - .end(function (err, res) { - if (err) return done(err); - res.text.should.equal('hello'); - done(); - }); + it('responds with 404 when has a trailing slash', async () => { + const app = new Koa(); + const router = new Router({ + strict: true + }); + router.get('/info', (ctx) => { + ctx.body = 'hello'; }); + await request(http.createServer(app.use(router.routes()).callback())) + .get('/info/') + .expect(404); + }); +}); - it('responds with 404 when has a trailing slash', function (done) { - const app = new Koa(); - const router = new Router({ - strict: true - }); - router.get('/info', function (ctx) { - ctx.body = 'hello'; - }); - request(http.createServer(app.use(router.routes()).callback())) - .get('/info/') - .expect(404) - .end(function (err, res) { - if (err) return done(err); - done(); - }); +describe('use middleware with opts', () => { + it('responds with 200', async () => { + const app = new Koa(); + const router = new Router({ + strict: true + }); + router.get('/info', (ctx) => { + ctx.body = 'hello'; + }); + const res = await request( + http.createServer(app.use(router.routes()).callback()) + ) + .get('/info') + .expect(200); + assert.strictEqual(res.text, 'hello'); + }); + + it('responds with 404 when has a trailing slash', async () => { + const app = new Koa(); + const router = new Router({ + strict: true }); + router.get('/info', (ctx) => { + ctx.body = 'hello'; + }); + await request(http.createServer(app.use(router.routes()).callback())) + .get('/info/') + .expect(404); }); +}); - describe('router.routes()', function () { - it('should return composed middleware', function (done) { - const app = new Koa(); - const router = new Router(); - let middlewareCount = 0; - const middlewareA = function (ctx, next) { - middlewareCount++; - return next(); - }; +describe('router.routes()', () => { + it('should return composed middleware', async () => { + const app = new Koa(); + const router = new Router(); + let middlewareCount = 0; + const middlewareA = (ctx, next) => { + middlewareCount++; + return next(); + }; - const middlewareB = function (ctx, next) { - middlewareCount++; - return next(); - }; + const middlewareB = (ctx, next) => { + middlewareCount++; + return next(); + }; - router.use(middlewareA, middlewareB); - router.get('/users/:id', function (ctx) { - should.exist(ctx.params.id); - ctx.body = { hello: 'world' }; - }); + router.use(middlewareA, middlewareB); + router.get('/users/:id', (ctx) => { + assert.strictEqual(Boolean(ctx.params.id), true); + ctx.body = { hello: 'world' }; + }); - const routerMiddleware = router.routes(); + const routerMiddleware = router.routes(); - expect(routerMiddleware).to.be.a('function'); + assert.strictEqual(typeof routerMiddleware, 'function'); - request(http.createServer(app.use(routerMiddleware).callback())) - .get('/users/1') - .expect(200) - .end(function (err, res) { - if (err) return done(err); - expect(res.body).to.be.an('object'); - expect(res.body).to.have.property('hello', 'world'); - expect(middlewareCount).to.equal(2); - done(); - }); + const res = await request( + http.createServer(app.use(routerMiddleware).callback()) + ) + .get('/users/1') + .expect(200); + assert.strictEqual(typeof res.body, 'object'); + assert.strictEqual(res.body.hello, 'world'); + assert.strictEqual(middlewareCount, 2); + }); + + it('places a `_matchedRoute` value on context', async () => { + const app = new Koa(); + const router = new Router(); + const middleware = (ctx, next) => { + next(); + assert.strictEqual(ctx._matchedRoute, '/users/:id'); + }; + + router.use(middleware); + router.get('/users/:id', (ctx) => { + assert.strictEqual(ctx._matchedRoute, '/users/:id'); + assert.strictEqual(Boolean(ctx.params.id), true); + ctx.body = { hello: 'world' }; }); - it('places a `_matchedRoute` value on context', function (done) { - const app = new Koa(); - const router = new Router(); - const middleware = function (ctx, next) { - next(); - expect(ctx._matchedRoute).to.be('/users/:id'); - }; + const routerMiddleware = router.routes(); - router.use(middleware); - router.get('/users/:id', function (ctx) { - expect(ctx._matchedRoute).to.be('/users/:id'); - should.exist(ctx.params.id); - ctx.body = { hello: 'world' }; - }); + await request(http.createServer(app.use(routerMiddleware).callback())) + .get('/users/1') + .expect(200); + }); - const routerMiddleware = router.routes(); + it('places a `_matchedRouteName` value on the context for a named route', async () => { + const app = new Koa(); + const router = new Router(); - request(http.createServer(app.use(routerMiddleware).callback())) - .get('/users/1') - .expect(200) - .end(function (err) { - if (err) return done(err); - done(); - }); + router.get('users#show', '/users/:id', (ctx) => { + assert.strictEqual(ctx._matchedRouteName, 'users#show'); + ctx.status = 200; }); - it('places a `_matchedRouteName` value on the context for a named route', function (done) { - const app = new Koa(); - const router = new Router(); + await request(http.createServer(app.use(router.routes()).callback())) + .get('/users/1') + .expect(200); + }); - router.get('users#show', '/users/:id', function (ctx) { - expect(ctx._matchedRouteName).to.be('users#show'); - ctx.status = 200; - }); + it('does not place a `_matchedRouteName` value on the context for unnamed routes', async () => { + const app = new Koa(); + const router = new Router(); - request(http.createServer(app.use(router.routes()).callback())) - .get('/users/1') - .expect(200) - .end(function (err) { - if (err) return done(err); - done(); - }); + router.get('/users/:id', (ctx) => { + assert.strictEqual(ctx._matchedRouteName, undefined); + ctx.status = 200; }); - it('does not place a `_matchedRouteName` value on the context for unnamed routes', function (done) { - const app = new Koa(); - const router = new Router(); + await request(http.createServer(app.use(router.routes()).callback())) + .get('/users/1') + .expect(200); + }); - router.get('/users/:id', function (ctx) { - expect(ctx._matchedRouteName).to.be(undefined); - ctx.status = 200; - }); + it('places a `routerPath` value on the context for current route', async () => { + const app = new Koa(); + const router = new Router(); - request(http.createServer(app.use(router.routes()).callback())) - .get('/users/1') - .expect(200) - .end(function (err) { - if (err) return done(err); - done(); - }); + router.get('/users/:id', (ctx) => { + assert.strictEqual(ctx.routerPath, '/users/:id'); + ctx.status = 200; }); - it('places a `routerPath` value on the context for current route', function (done) { - const app = new Koa(); - const router = new Router(); + await request(http.createServer(app.use(router.routes()).callback())) + .get('/users/1') + .expect(200); + }); - router.get('/users/:id', function (ctx) { - expect(ctx.routerPath).to.be('/users/:id'); - ctx.status = 200; - }); + it('places a `_matchedRoute` value on the context for current route', async () => { + const app = new Koa(); + const router = new Router(); - request(http.createServer(app.use(router.routes()).callback())) - .get('/users/1') - .expect(200) - .end(function (err) { - if (err) return done(err); - done(); - }); + router.get('/users/list', (ctx) => { + assert(ctx._matchedRoute, '/users/list'); + ctx.status = 200; + }); + router.get('/users/:id', (ctx) => { + assert.strictEqual(ctx._matchedRoute, '/users/:id'); + ctx.status = 200; }); - it('places a `_matchedRoute` value on the context for current route', function (done) { - const app = new Koa(); - const router = new Router(); + await request(http.createServer(app.use(router.routes()).callback())) + .get('/users/list') + .expect(200); + }); +}); + +describe('If no HEAD method, default to GET', () => { + it('should default to GET', async () => { + const app = new Koa(); + const router = new Router(); + router.get('/users/:id', (ctx) => { + assert.strictEqual(Boolean(ctx.params.id), true); + ctx.body = 'hello'; + }); + const res = await request( + http.createServer(app.use(router.routes()).callback()) + ) + .head('/users/1') + .expect(200); + assert.deepStrictEqual(res.body, {}); + }); + + it('should work with middleware', async () => { + const app = new Koa(); + const router = new Router(); + router.get('/users/:id', (ctx) => { + assert.strictEqual(Boolean(ctx.params.id), true); + ctx.body = 'hello'; + }); + const res = await request( + http.createServer(app.use(router.routes()).callback()) + ) + .head('/users/1') + .expect(200); + assert.deepStrictEqual(res.body, {}); + }); +}); + +describe('Router#prefix', () => { + it('should set opts.prefix', () => { + const router = new Router(); + assert.strictEqual('prefix' in router.opts, false); + router.prefix('/things/:thing_id'); + assert.strictEqual(router.opts.prefix, '/things/:thing_id'); + }); - router.get('/users/list', function (ctx) { - expect(ctx._matchedRoute).to.be('/users/list'); - ctx.status = 200; + it('should prefix existing routes', () => { + const router = new Router(); + router.get('/users/:id', (ctx) => { + ctx.body = 'test'; + }); + router.prefix('/things/:thing_id'); + const route = router.stack[0]; + assert.strictEqual(route.path, '/things/:thing_id/users/:id'); + assert.strictEqual(route.paramNames.length, 2); + assert.strictEqual(route.paramNames[0].name, 'thing_id'); + assert.strictEqual(route.paramNames[1].name, 'id'); + }); + + it('populates ctx.params correctly for router prefix (including use)', async () => { + const app = new Koa(); + const router = new Router({ prefix: '/:category' }); + app.use(router.routes()); + router + .use((ctx, next) => { + assert.strictEqual('params' in ctx, true); + assert.strictEqual(typeof ctx.params, 'object'); + assert.strictEqual(ctx.params.category, 'cats'); + return next(); + }) + .get('/suffixHere', (ctx) => { + assert.strictEqual('params' in ctx, true); + assert.strictEqual(typeof ctx.params, 'object'); + assert.strictEqual(ctx.params.category, 'cats'); + ctx.status = 204; }); - router.get('/users/:id', function (ctx) { - expect(ctx._matchedRoute).to.be('/users/:id'); - ctx.status = 200; + await request(http.createServer(app.callback())) + .get('/cats/suffixHere') + .expect(204); + }); + + it('populates ctx.params correctly for more complex router prefix (including use)', async () => { + const app = new Koa(); + const router = new Router({ prefix: '/:category/:color' }); + app.use(router.routes()); + router + .use((ctx, next) => { + assert.strictEqual('params' in ctx, true); + assert.strictEqual(typeof ctx.params, 'object'); + assert.strictEqual(ctx.params.category, 'cats'); + assert.strictEqual(ctx.params.color, 'gray'); + return next(); + }) + .get('/:active/suffixHere', (ctx) => { + assert.strictEqual('params' in ctx, true); + assert.strictEqual(typeof ctx.params, 'object'); + assert.strictEqual(ctx.params.category, 'cats'); + assert.strictEqual(ctx.params.color, 'gray'); + assert.strictEqual(ctx.params.active, 'true'); + ctx.status = 204; }); + await request(http.createServer(app.callback())) + .get('/cats/gray/true/suffixHere') + .expect(204); + }); - request(http.createServer(app.use(router.routes()).callback())) - .get('/users/list') - .expect(200) - .end(function (err) { - if (err) return done(err); - done(); - }); - }); + it('populates ctx.params correctly for dynamic and static prefix (including async use)', async () => { + const app = new Koa(); + const router = new Router({ prefix: '/:ping/pong' }); + app.use(router.routes()); + router + .use(async (ctx, next) => { + assert.strictEqual('params' in ctx, true); + assert.strictEqual(typeof ctx.params, 'object'); + assert.strictEqual(ctx.params.ping, 'pingKey'); + await next(); + }) + .get('/', (ctx) => { + assert.strictEqual('params' in ctx, true); + assert.strictEqual(typeof ctx.params, 'object'); + assert.strictEqual(ctx.params.ping, 'pingKey'); + ctx.body = ctx.params; + }); + await request(http.createServer(app.callback())) + .get('/pingKey/pong') + .expect(200, /{"ping":"pingKey"}/); }); - describe('If no HEAD method, default to GET', function () { - it('should default to GET', function (done) { - const app = new Koa(); - const router = new Router(); - router.get('/users/:id', function (ctx) { - should.exist(ctx.params.id); - ctx.body = 'hello'; + it('populates ctx.params correctly for static prefix', async () => { + const app = new Koa(); + const router = new Router({ prefix: '/all' }); + app.use(router.routes()); + router + .use((ctx, next) => { + assert.strictEqual('params' in ctx, true); + assert.strictEqual(typeof ctx.params, 'object'); + assert.deepStrictEqual(ctx.params, {}); + return next(); + }) + .get('/:active/suffixHere', (ctx) => { + assert.strictEqual('params' in ctx, true); + assert.strictEqual(typeof ctx.params, 'object'); + assert.strictEqual(ctx.params.active, 'true'); + ctx.status = 204; }); - request(http.createServer(app.use(router.routes()).callback())) - .head('/users/1') - .expect(200) - .end(function (err, res) { - if (err) return done(err); - expect(res.body).to.be.empty(); - done(); - }); - }); + await request(http.createServer(app.callback())) + .get('/all/true/suffixHere') + .expect(204); + }); - it('should work with middleware', function (done) { + describe('when used with .use(fn) - gh-247', () => { + it('does not set params.0 to the matched path', async () => { const app = new Koa(); const router = new Router(); - router.get('/users/:id', function (ctx) { - should.exist(ctx.params.id); - ctx.body = 'hello'; - }); - request(http.createServer(app.use(router.routes()).callback())) - .head('/users/1') - .expect(200) - .end(function (err, res) { - if (err) return done(err); - expect(res.body).to.be.empty(); - done(); - }); - }); - }); - describe('Router#prefix', function () { - it('should set opts.prefix', function () { - const router = Router(); - expect(router.opts).to.not.have.key('prefix'); - router.prefix('/things/:thing_id'); - expect(router.opts.prefix).to.equal('/things/:thing_id'); - }); + router.use((ctx, next) => { + return next(); + }); - it('should prefix existing routes', function () { - const router = Router(); - router.get('/users/:id', function (ctx) { - ctx.body = 'test'; + router.get('/foo/:id', (ctx) => { + ctx.body = ctx.params; }); - router.prefix('/things/:thing_id'); - const route = router.stack[0]; - expect(route.path).to.equal('/things/:thing_id/users/:id'); - expect(route.paramNames).to.have.length(2); - expect(route.paramNames[0]).to.have.property('name', 'thing_id'); - expect(route.paramNames[1]).to.have.property('name', 'id'); - }); - it('populates ctx.params correctly for router prefix (including use)', function (done) { - const app = new Koa(); - const router = new Router({ prefix: '/:category' }); - app.use(router.routes()); - router - .use((ctx, next) => { - ctx.should.have.property('params'); - ctx.params.should.be.type('object'); - ctx.params.should.have.property('category', 'cats'); - return next(); - }) - .get('/suffixHere', function (ctx) { - ctx.should.have.property('params'); - ctx.params.should.be.type('object'); - ctx.params.should.have.property('category', 'cats'); - ctx.status = 204; - }); - request(http.createServer(app.callback())) - .get('/cats/suffixHere') - .expect(204) - .end(function (err) { - if (err) return done(err); - done(); - }); - }); + router.prefix('/things'); - it('populates ctx.params correctly for more complex router prefix (including use)', function (done) { - const app = new Koa(); - const router = new Router({ prefix: '/:category/:color' }); app.use(router.routes()); - router - .use((ctx, next) => { - ctx.should.have.property('params'); - ctx.params.should.be.type('object'); - ctx.params.should.have.property('category', 'cats'); - ctx.params.should.have.property('color', 'gray'); - return next(); - }) - .get('/:active/suffixHere', function (ctx) { - ctx.should.have.property('params'); - ctx.params.should.be.type('object'); - ctx.params.should.have.property('category', 'cats'); - ctx.params.should.have.property('color', 'gray'); - ctx.params.should.have.property('active', 'true'); - ctx.status = 204; - }); - request(http.createServer(app.callback())) - .get('/cats/gray/true/suffixHere') - .expect(204) - .end(function (err, res) { - if (err) return done(err); - done(); - }); - }); + const res = await request(http.createServer(app.callback())) + .get('/things/foo/108') + .expect(200); - it('populates ctx.params correctly for dynamic and static prefix (including async use)', function (done) { - const app = new Koa(); - const router = new Router({ prefix: '/:ping/pong' }); - app.use(router.routes()); - router - .use(async (ctx, next) => { - ctx.should.have.property('params'); - ctx.params.should.be.type('object'); - ctx.params.should.have.property('ping', 'pingKey'); - await next(); - }) - .get('/', function (ctx) { - ctx.should.have.property('params'); - ctx.params.should.be.type('object'); - ctx.params.should.have.property('ping', 'pingKey'); - ctx.body = ctx.params; - }); - request(http.createServer(app.callback())) - .get('/pingKey/pong') - .expect(200, /{"ping":"pingKey"}/) - .end(function (err) { - if (err) return done(err); - done(); - }); + assert.strictEqual(res.body.id, '108'); + assert.strictEqual('0' in res.body, false); }); + }); - it('populates ctx.params correctly for static prefix', function (done) { - const app = new Koa(); - const router = new Router({ prefix: '/all' }); - app.use(router.routes()); - router - .use((ctx, next) => { - ctx.should.have.property('params'); - ctx.params.should.be.type('object'); - ctx.params.should.be.empty(); - return next(); - }) - .get('/:active/suffixHere', function (ctx) { - ctx.should.have.property('params'); - ctx.params.should.be.type('object'); - ctx.params.should.have.property('active', 'true'); - ctx.status = 204; - }); - request(http.createServer(app.callback())) - .get('/all/true/suffixHere') - .expect(204) - .end(function (err) { - if (err) return done(err); - done(); - }); - }); + describe('with trailing slash', testPrefix('/admin/')); + describe('without trailing slash', testPrefix('/admin')); + + function testPrefix(prefix) { + return () => { + let server; + let middlewareCount = 0; - describe('when used with .use(fn) - gh-247', function () { - it('does not set params.0 to the matched path', function (done) { + before(() => { const app = new Koa(); const router = new Router(); - router.use(function (ctx, next) { + router.use((ctx, next) => { + middlewareCount++; + ctx.thing = 'worked'; return next(); }); - router.get('/foo/:id', function (ctx) { - ctx.body = ctx.params; + router.get('/', (ctx) => { + middlewareCount++; + ctx.body = { name: ctx.thing }; }); - router.prefix('/things'); - - app.use(router.routes()); - request(http.createServer(app.callback())) - .get('/things/foo/108') - .expect(200) - .end(function (err, res) { - if (err) return done(err); + router.prefix(prefix); + server = http.createServer(app.use(router.routes()).callback()); + }); - expect(res.body).to.have.property('id', '108'); - expect(res.body).to.not.have.property('0'); - done(); - }); + after(() => { + server.close(); }); - }); - describe('with trailing slash', testPrefix('/admin/')); - describe('without trailing slash', testPrefix('/admin')); + beforeEach(() => { + middlewareCount = 0; + }); - function testPrefix(prefix) { - return function () { - let server; - let middlewareCount = 0; + it('should support root level router middleware', async () => { + const res = await request(server).get(prefix).expect(200); - before(function () { - const app = new Koa(); - const router = Router(); + assert.strictEqual(middlewareCount, 2); + assert.strictEqual(typeof res.body, 'object'); + assert.strictEqual(res.body.name, 'worked'); + }); - router.use(function (ctx, next) { - middlewareCount++; - ctx.thing = 'worked'; - return next(); - }); + it('should support requests with a trailing path slash', async () => { + const res = await request(server).get('/admin/').expect(200); - router.get('/', function (ctx) { - middlewareCount++; - ctx.body = { name: ctx.thing }; - }); + assert.strictEqual(middlewareCount, 2); + assert.strictEqual(typeof res.body, 'object'); + assert.strictEqual(res.body.name, 'worked'); + }); - router.prefix(prefix); - server = http.createServer(app.use(router.routes()).callback()); - }); + it('should support requests without a trailing path slash', async () => { + const res = await request(server).get('/admin').expect(200); - after(function () { - server.close(); - }); + assert.strictEqual(middlewareCount, 2); + assert.strictEqual(typeof res.body, 'object'); + assert.strictEqual(res.body.name, 'worked'); + }); + }; + } - beforeEach(function () { - middlewareCount = 0; - }); + it(`prefix and '/' route behavior`, async () => { + const app = new Koa(); + const router = new Router({ + strict: false, + prefix: '/foo' + }); - it('should support root level router middleware', function (done) { - request(server) - .get(prefix) - .expect(200) - .end(function (err, res) { - if (err) return done(err); - expect(middlewareCount).to.equal(2); - expect(res.body).to.be.an('object'); - expect(res.body).to.have.property('name', 'worked'); - done(); - }); - }); + const strictRouter = new Router({ + strict: true, + prefix: '/bar' + }); - it('should support requests with a trailing path slash', function (done) { - request(server) - .get('/admin/') - .expect(200) - .end(function (err, res) { - if (err) return done(err); - expect(middlewareCount).to.equal(2); - expect(res.body).to.be.an('object'); - expect(res.body).to.have.property('name', 'worked'); - done(); - }); - }); + router.get('/', (ctx) => { + ctx.body = ''; + }); - it('should support requests without a trailing path slash', function (done) { - request(server) - .get('/admin') - .expect(200) - .end(function (err, res) { - if (err) return done(err); - expect(middlewareCount).to.equal(2); - expect(res.body).to.be.an('object'); - expect(res.body).to.have.property('name', 'worked'); - done(); - }); - }); - }; - } + strictRouter.get('/', (ctx) => { + ctx.body = ''; + }); - it(`prefix and '/' route behavior`, function (done) { - const app = new Koa(); - const router = new Router({ - strict: false, - prefix: '/foo' - }); + app.use(router.routes()); + app.use(strictRouter.routes()); - const strictRouter = new Router({ - strict: true, - prefix: '/bar' - }); + const server = http.createServer(app.callback()); - router.get('/', function (ctx) { - ctx.body = ''; - }); + await request(server).get('/foo').expect(200); + await request(server).get('/foo/').expect(200); + await request(server).get('/bar').expect(404); + await request(server).get('/bar/').expect(200); + }); +}); - strictRouter.get('/', function (ctx) { - ctx.body = ''; - }); +describe('Static Router#url()', () => { + it('generates route URL', () => { + const url = Router.url('/:category/:title', { + category: 'programming', + title: 'how-to-node' + }); + assert.strictEqual(url, '/programming/how-to-node'); + }); - app.use(router.routes()); - app.use(strictRouter.routes()); + it('escapes using encodeURIComponent()', () => { + const url = Router.url( + '/:category/:title', + { category: 'programming', title: 'how to node' }, + { encode: encodeURIComponent } + ); + assert.strictEqual(url, '/programming/how%20to%20node'); + }); - const server = http.createServer(app.callback()); + it('generates route URL with params and query params', async () => { + const query = { page: 3, limit: 10 }; + let url = Router.url('/books/:category/:id', 'programming', 4, { query }); + assert.strictEqual(url, '/books/programming/4?page=3&limit=10'); + url = Router.url( + '/books/:category/:id', + { category: 'programming', id: 4 }, + { query } + ); + assert.strictEqual(url, '/books/programming/4?page=3&limit=10'); + url = Router.url( + '/books/:category/:id', + { category: 'programming', id: 4 }, + { query: 'page=3&limit=10' } + ); + assert.strictEqual(url, '/books/programming/4?page=3&limit=10'); + }); - request(server) - .get('/foo') - .expect(200) - .end(function (err) { - if (err) return done(err); - - request(server) - .get('/foo/') - .expect(200) - .end(function (err) { - if (err) return done(err); - - request(server) - .get('/bar') - .expect(404) - .end(function (err) { - if (err) return done(err); - - request(server) - .get('/bar/') - .expect(200) - .end(function (err) { - if (err) return done(err); - done(); - }); - }); - }); - }); + it('generates router URL without params and with with query params', async () => { + const url = Router.url('/category', { + query: { page: 3, limit: 10 } }); + assert.strictEqual(url, '/category?page=3&limit=10'); }); +}); - describe('Static Router#url()', function () { - it('generates route URL', function () { - const url = Router.url('/:category/:title', { - category: 'programming', - title: 'how-to-node' - }); - url.should.equal('/programming/how-to-node'); +describe('Support host', () => { + it('should support host match', async () => { + const app = new Koa(); + const router = new Router({ + host: 'test.domain' }); - - it('escapes using encodeURIComponent()', function () { - const url = Router.url( - '/:category/:title', - { category: 'programming', title: 'how to node' }, - { encode: encodeURIComponent } - ); - url.should.equal('/programming/how%20to%20node'); + router.get('/', (ctx) => { + ctx.body = { + url: '/' + }; }); + app.use(router.routes()); - it('generates route URL with params and query params', function (done) { - const query = { page: 3, limit: 10 }; - let url = Router.url('/books/:category/:id', 'programming', 4, { query }); - url.should.equal('/books/programming/4?page=3&limit=10'); - url = Router.url( - '/books/:category/:id', - { category: 'programming', id: 4 }, - { query } - ); - url.should.equal('/books/programming/4?page=3&limit=10'); - url = Router.url( - '/books/:category/:id', - { category: 'programming', id: 4 }, - { query: 'page=3&limit=10' } - ); - url.should.equal('/books/programming/4?page=3&limit=10'); - done(); - }); + const server = http.createServer(app.callback()); - it('generates router URL without params and with with query params', function (done) { - const url = Router.url('/category', { - query: { page: 3, limit: 10 } - }); - url.should.equal('/category?page=3&limit=10'); - done(); - }); + await request(server).get('/').set('Host', 'test.domain').expect(200); + await request(server).get('/').set('Host', 'a.domain').expect(404); }); - describe('Support host', function () { - it('should support host match', function (done) { - const app = new Koa(); - const router = new Router({ - host: 'test.domain' - }); - router.get('/', (ctx) => { - ctx.body = { - url: '/' - }; - }); - app.use(router.routes()); + it('should support host match regexp', async () => { + const app = new Koa(); + const router = new Router({ + host: /^(.*\.)?test\.domain/ + }); + router.get('/', (ctx) => { + ctx.body = { + url: '/' + }; + }); + app.use(router.routes()); + const server = http.createServer(app.callback()); - const server = http.createServer(app.callback()); + await request(server).get('/').set('Host', 'test.domain').expect(200); - request(server) - .get('/') - .set('Host', 'test.domain') - .expect(200) - .end(function (err, res) { - if (err) return done(err); - - request(server) - .get('/') - .set('Host', 'a.domain') - .expect(404) - .end(function (err, res) { - if (err) return done(err); - done(); - }); - }); - }); - it('should support host match regexp', function (done) { - const app = new Koa(); - const router = new Router({ - host: /^(.*\.)?test\.domain/ - }); - router.get('/', (ctx) => { - ctx.body = { - url: '/' - }; - }); - app.use(router.routes()); - const server = http.createServer(app.callback()); + await request(server).get('/').set('Host', 'www.test.domain').expect(200); - request(server) - .get('/') - .set('Host', 'test.domain') - .expect(200) - .end(function (err, res) { - if (err) return done(err); - - request(server) - .get('/') - .set('Host', 'www.test.domain') - .expect(200) - .end(function (err, res) { - if (err) return done(err); - - request(server) - .get('/') - .set('Host', 'any.sub.test.domain') - .expect(200) - .end(function (err, res) { - if (err) return done(err); - - request(server) - .get('/') - .set('Host', 'sub.anytest.domain') - .expect(404) - .end(function (err, res) { - if (err) return done(err); - - done(); - }); - }); - }); - }); - }); + await request(server) + .get('/') + .set('Host', 'any.sub.test.domain') + .expect(200); + + await request(server) + .get('/') + .set('Host', 'sub.anytest.domain') + .expect(404); }); });