From: Justin Wind Date: Fri, 29 Apr 2022 16:21:51 +0000 (-0700) Subject: Merge branch 'v1.2-dev' as v1.2.6 X-Git-Tag: v1.2.6 X-Git-Url: https://git.squeep.com/?p=squeep-api-dingus;a=commitdiff_plain;h=f944684c37532e67a7d28e34909178435cba03a5;hp=12568946a94e853c3c16974d57dd34de1ad3877c Merge branch 'v1.2-dev' as v1.2.6 --- diff --git a/lib/common.js b/lib/common.js index 21070b8..40a9f55 100644 --- a/lib/common.js +++ b/lib/common.js @@ -11,8 +11,15 @@ const uuid = require('uuid'); const Enum = require('./enum'); /** - * Return a function which combines a part of the filename with a scope, for use in logging. - * @param {string} filename + * @callback ScopeFn + * @param {String} scope + * @returns {String} + */ +/** + * Return a function which prefixes a provided scope with the most- + * relevant part of the filename, for use in logging. + * @param {String} filename + * @returns {ScopeFn} */ const fileScope = (filename) => { let fScope = path.basename(filename, '.js'); @@ -24,9 +31,10 @@ const fileScope = (filename) => { /** * Simple ETag from data. - * @param {string} filePath - * @param {object} fileStat - * @param {*} fileData + * @param {String} filePath (currently unused) + * @param {fs.Stats} fileStat + * @param {crypto.BinaryLike} fileData content + * @returns {String} */ const generateETag = (_filePath, fileStat, fileData) => { const hash = crypto.createHash('sha256'); @@ -39,16 +47,19 @@ const generateETag = (_filePath, fileStat, fileData) => { }; /** - * @param {object} obj - * @param {string} prop - * @param {*} def + * Access property with default. + * @param {Object} obj + * @param {String} prop + * @param {*} def default value if prop does not exist for obj + * @return {*} */ const get = (obj, prop, def) => obj && prop && (prop in obj) ? obj[prop] : def; /** * @param {http.ClientRequest} req * @param {http.ServerResponse} res - * @param {object} ctx + * @param {Object} ctx + * @deprecated after v1.2.5 (integrated into logger module) */ const handlerLogData = (req, res, ctx) => ({ req: requestLogData(req), @@ -57,10 +68,12 @@ const handlerLogData = (req, res, ctx) => ({ }); /** - * + * Determine whether a client has already requested a resource, + * based on If-Modified-Since and If-None-Match headers. * @param {http.ClientRequest} req * @param {Number} modifiedTimeMs - * @param {string} eTag + * @param {String} eTag + * @returns {Boolean} */ const isClientCached = (req, modifiedTimeMs, eTag) => { let clientCached = false; @@ -93,6 +106,7 @@ const isClientCached = (req, modifiedTimeMs, eTag) => { * Expects only one-level deep, is not recursive! * @param {Object} origEnum * @param {Object} additionalEnum + * @returns {Object} */ const mergeEnum = (origEnum, additionalEnum) => { for (const e of Object.keys(additionalEnum)) { @@ -118,9 +132,9 @@ const httpStatusCodeClass = (statusCode) => Math.floor(statusCode / 100); const _isObject = (obj) => obj && typeof obj === 'object'; const _isArray = (obj) => Array.isArray(obj); /** - * Return a new object with all objects combined. - * @param {...any} objects - * @returns + * Return a new object with all objects combined, later properties taking precedence. + * @param {...Object} objects + * @returns {Object} */ const mergeDeep = (...objects) => { return objects.reduce((acc, obj) => { @@ -144,7 +158,8 @@ const mergeDeep = (...objects) => { /** * Return a new object with selected props. * @param {Object} obj - * @param {string[]} props + * @param {String[]} props + * @returns {Object} */ const pick = (obj, props) => { const picked = {}; @@ -160,6 +175,7 @@ const pick = (obj, props) => { * Return a subset of a request object, suitable for logging. * Obscures sensitive header values. * @param {http.ClientRequest} req + * @deprecated after v1.2.5 (integrated into logger module) */ const requestLogData = (req) => { const data = pick(req, [ @@ -178,6 +194,7 @@ const requestLogData = (req) => { * Remove sensitive header data. * @param {Object} data * @param {Object} data.headers + * @deprecated after v1.2.5 (integrated into logger module) */ const scrubHeaderObject = (data) => { if (data && data.headers && 'authorization' in data.headers) { @@ -192,6 +209,7 @@ const scrubHeaderObject = (data) => { * Hide sensitive part of an Authorization header. * @param {String} authHeader * @returns {String} + * @deprecated after v1.2.5 (integrated into logger module) */ const obscureAuthorizationHeader = (authHeader) => { if (!authHeader) { @@ -206,6 +224,7 @@ const obscureAuthorizationHeader = (authHeader) => { /** * Return a subset of a response object, suitable for logging. * @param {http.ServerResponse} res + * @deprecated after v1.2.5 (integrated into logger module) */ const responseLogData = (res) => { const response = pick(res, [ @@ -218,7 +237,7 @@ const responseLogData = (res) => { /** - * Store updates to defaultOptions, but no new properties. + * Store all properties in defaultOptions on target from either options or defaultOptions. * @param {Object} target * @param {Object} defaultOptions * @param {Object} options @@ -228,10 +247,10 @@ const setOptions = (target, defaultOptions, options) => { }; /** - * Return a list of source split at first delimiter. - * @param {string} src - * @param {string} delimiter - * @param {string} fill trailing stand-in if no delimiter in src + * Return a two-item list of src, split at first delimiter encountered. + * @param {String} src + * @param {String} delimiter + * @param {String} fill trailing stand-in if no delimiter in src */ const splitFirst = (src, delimiter, fill) => { const idx = src.indexOf(delimiter); @@ -243,14 +262,21 @@ const splitFirst = (src, delimiter, fill) => { }; /** - * Generate a new request identifier. + * Generate a new request identifier, a time/host-based uuid. * @returns {String} */ const requestId = () => { return uuid.v1(); }; +/** + * Do nothing. + */ const nop = () => { /**/ }; + +/** + * A logger object which does nothing. + */ const nullLogger = { error: nop, warn: nop, @@ -260,8 +286,9 @@ const nullLogger = { }; /** - * Populates any absent logger levels. + * Populates any absent logger level functions on a logger object. * @param {Object} logger + * @returns {Object} */ const ensureLoggerLevels = (logger = {}) => { for (const level in nullLogger) { @@ -275,6 +302,7 @@ const ensureLoggerLevels = (logger = {}) => { /** * Merges folded header lines * @param {String[]} lines + * @returns {String} */ const unfoldHeaderLines = (lines) => { const foldedLineRE = /^(\t| +)(.*)$/; diff --git a/lib/dingus.js b/lib/dingus.js index 51da6fa..943aa2f 100644 --- a/lib/dingus.js +++ b/lib/dingus.js @@ -27,7 +27,7 @@ const defaultOptions = { strictAccept: true, selfBaseUrl: '', staticMetadata: true, - staticPath: undefined, // no reasonable default + staticPath: undefined, // No reasonable default trustProxy: true, querystring, }; @@ -349,8 +349,9 @@ class Dingus { * Return all body data from a request. * @param {http.ClientRequest} req * @param {Number=} maximumBodySize + * @param {Boolean=} toString */ - async bodyData(req, maximumBodySize) { + async bodyData(req, maximumBodySize, toString = true) { const _scope = _fileScope('bodyData'); return new Promise((resolve, reject) => { const body = []; @@ -363,7 +364,10 @@ class Dingus { reject(new ResponseError(Enum.ErrorResponse.RequestEntityTooLarge)); } }); - req.on('end', () => resolve(Buffer.concat(body).toString())); + req.on('end', () => { + const bodyBuffer = Buffer.concat(body); + resolve(toString ? bodyBuffer.toString() : bodyBuffer); + }); req.on('error', (e) => { this.logger.error(_scope, 'failed', { error: e }); reject(e); @@ -521,7 +525,7 @@ class Dingus { */ async serveFile(req, res, ctx, directory, fileName) { const _scope = _fileScope('serveFile'); - this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx }); + this.logger.debug(_scope, 'called', { req, ctx }); // Require a directory field. if (!directory) { @@ -561,18 +565,20 @@ class Dingus { break; } const suffix = Enum.EncodingTypeSuffix[encoding]; - if (suffix) { - const encodedFilePath = `${filePath}${suffix}`; - const [ encodedStat, encodedData ] = await this._readFileInfo(encodedFilePath); - if (encodedStat) { - ([ stat, data ] = [ encodedStat, encodedData ]); - ctx.selectedEncoding = encoding; - Dingus.addEncodingHeader(res, encoding); - res.setHeader(Enum.Header.Vary, Enum.Header.AcceptEncoding); - this.logger.debug(_scope, 'serving encoded version', { ctx, encodedFilePath }); - } - break; + if (!suffix) { + this.logger.error(_scope, 'supported encoding missing mapped suffix', { ctx, encoding }); + continue; + } + const encodedFilePath = `${filePath}${suffix}`; + const [ encodedStat, encodedData ] = await this._readFileInfo(encodedFilePath); + if (encodedStat) { + ([ stat, data ] = [ encodedStat, encodedData ]); + ctx.selectedEncoding = encoding; + Dingus.addEncodingHeader(res, encoding); + res.setHeader(Enum.Header.Vary, Enum.Header.AcceptEncoding); + this.logger.debug(_scope, 'serving encoded version', { ctx, encodedFilePath }); } + break; } const lastModifiedDate = new Date(stat.mtimeMs); @@ -649,11 +655,11 @@ class Dingus { if (err && err.statusCode) { res.statusCode = err.statusCode; body = this.renderError(res.getHeader(Enum.Header.ContentType), err); - this.logger.debug(_scope, 'handler error', { err, ...common.handlerLogData(req, res, ctx) }); + this.logger.debug(_scope, 'handler error', { err, req, res, ctx }); } else { res.statusCode = 500; body = this.renderError(res.getHeader(Enum.Header.ContentType), Enum.ErrorResponse.InternalServerError); - this.logger.error(_scope, 'handler exception', { err, ...common.handlerLogData(req, res, ctx) }); + this.logger.error(_scope, 'handler exception', { err, req, res, ctx }); } res.end(body); } diff --git a/lib/patches/incoming-message.js b/lib/patches/incoming-message.js index 894ca65..0b2e259 100644 --- a/lib/patches/incoming-message.js +++ b/lib/patches/incoming-message.js @@ -3,9 +3,9 @@ const { IncomingMessage } = require('http'); /** * Welp, here we are, already into the crazy. - * Per documentation (https://nodejs.org/docs/latest-v12.x/api/http.html#http_request_getheader_name) - * this should exist, yet (as of 12.18.4) it does not. So let us change this pitch up, and patch -*/ + * This originally existed due to a misinterpretation of the documentation, + * but it's really quite useful, so it stays in our world. + */ /* istanbul ignore else */ if (typeof IncomingMessage.getHeader !== 'function') { IncomingMessage.prototype.getHeader = function (name) { diff --git a/lib/router.js b/lib/router.js deleted file mode 100644 index 61a8d11..0000000 --- a/lib/router.js +++ /dev/null @@ -1,213 +0,0 @@ -/* eslint-disable security/detect-object-injection */ -'use strict'; - -/** - * A very simple router. - */ - -const { METHODS: httpMethods } = require('http'); -const common = require('./common'); -const { DingusError } = require('./errors'); - -// Internal identifiers for route entries. -const METHODS = Symbol('METHODS'); -const PARAM = Symbol('PARAM'); - -const defaultOptions = { - ignoreTrailingSlash: false, - paramPrefix: ':', -}; - -class Router { - /** - * @param {Object} options - * @param {Boolean} options.ignoreTrailingSlash - * @param {Boolean} options.paramPrefix - */ - constructor(options = {}) { - common.setOptions(this, defaultOptions, options); - - // Keep lists of paths to match, indexed by path length. - this.pathsByLength = { - 1: [], - }; - - this.METHODS = METHODS; - this.PARAM = PARAM; - } - - - /** - * Prepare a path for insertion into search list. - * A searchable path is a list of path parts, with a property of method handlers. - * @param {string} pathDefinition - */ - _pathDefinitionToPathMatch(pathDefinition) { - const pathMatch = pathDefinition.split('/').map((p) => p.startsWith(this.paramPrefix) ? { [PARAM]: p.slice(this.paramPrefix.length) } : p); - if (this.ignoreTrailingSlash - && pathMatch[pathMatch.length - 1] === '') { - pathMatch.pop(); - } - pathMatch[METHODS] = {}; - pathMatch.forEach((p) => Object.freeze(p)); - Object.freeze(pathMatch); - return pathMatch; - } - - - /** - * Compare checkPath to fixedPath, no param substitution, params must match. - * @param {*} fixedPath - * @param {*} checkPath - */ - static _pathCompareExact(fixedPath, checkPath) { - if (fixedPath.length !== checkPath.length) { - return false; - } - for (let i = 0; i < fixedPath.length; i++) { - const fixedPart = fixedPath[i]; - const checkPart = checkPath[i]; - if (typeof fixedPart === 'object' && typeof checkPart === 'object') { - if (fixedPart[PARAM] !== checkPart[PARAM]) { - return false; - } - } else if (fixedPart !== checkPart) { - return false; - } - } - return true; - } - - - /** - * Compare checkPath to fixedPath, populating params. - * @param {*} fixedPath - * @param {*} checkPath - * @param {*} returnParams - */ - static _pathCompareParam(fixedPath, checkPath, returnParams = {}) { - const params = {}; - - if (fixedPath.length !== checkPath.length) { - return false; - } - for (let i = 0; i < fixedPath.length; i++) { - const fixedPart = fixedPath[i]; - const checkPart = checkPath[i]; - if (typeof fixedPart === 'object') { - params[fixedPart[PARAM]] = checkPart; - } else if (fixedPart !== checkPart) { - return false; - } - } - Object.assign(returnParams, params); - return true; - } - - - /** - * Search for an existing path, return matched path and path parameters. - * @param {Array} matchParts - */ - _pathFind(matchParts) { - const result = { - pathParams: {}, - matchedPath: undefined, - }; - const pathsByLength = this.pathsByLength[matchParts.length]; - if (pathsByLength) { - for (const p of pathsByLength) { - if (Router._pathCompareParam(p, matchParts, result.pathParams)) { - result.matchedPath = p; - break; - } - } - } - return result; - } - - - /** - * Return a matching path, no param substitution, params must match - * @param {*} matchParts - */ - _pathFindExact(matchParts) { - const pathsByLength = this.pathsByLength[matchParts.length]; - if (pathsByLength) { - for (const p of pathsByLength) { - if (Router._pathCompareExact(p, matchParts)) { - return p; - } - } - } - return undefined; - } - - - /** - * Insert a new path handler. - * @param {string|string[]} methods - * @param {string} urlPath - * @param {fn} handler - * @param {*[]} handlerArgs - */ - on(methods, urlPath, handler, handlerArgs = []) { - const matchParts = this._pathDefinitionToPathMatch(urlPath); - let existingPath = this._pathFindExact(matchParts); - if (!existingPath) { - existingPath = matchParts; - if (!(matchParts.length in this.pathsByLength)) { - this.pathsByLength[matchParts.length] = []; - } - this.pathsByLength[matchParts.length].push(existingPath); - } - if (!Array.isArray(methods)) { - methods = [methods]; - } - if (!Array.isArray(handlerArgs)) { - throw new TypeError(`handlerArgs must be an Array, not '${typeof handlerArgs}'`); - } - methods.forEach((method) => { - if (!httpMethods.includes(method) && method !== '*') { - throw new DingusError(`invalid method '${method}'`); - } - existingPath[METHODS][method] = { handler, handlerArgs }; - }); - } - - - /** - * Return an object, which contains a matching handler and any extra - * arguments, for a requested url. - * Also sets path parameters on context. - * @param {string} method - * @param {string[]} urlPath - * @param {object} ctx - * @returns {object} - */ - lookup(method, urlPath, ctx = {}) { - const pathParts = urlPath.split('/').map((part) => decodeURIComponent(part)); - if (this.ignoreTrailingSlash - && pathParts[pathParts.length - 1] === '') { - pathParts.pop(); - } - const { matchedPath, pathParams } = this._pathFind(pathParts); - ctx.params = pathParams; - if (matchedPath) { - ctx.matchedPath = matchedPath; - if (method in matchedPath[METHODS]) { - return matchedPath[METHODS][method]; - } - if ('*' in matchedPath[METHODS]) { - return matchedPath[METHODS]['*']; - } - throw new DingusError('NoMethod'); - } - ctx.unmatchedPath = pathParts; - throw new DingusError('NoPath'); - } - - -} - -module.exports = Router; diff --git a/lib/router/index.js b/lib/router/index.js new file mode 100644 index 0000000..a13ac47 --- /dev/null +++ b/lib/router/index.js @@ -0,0 +1,253 @@ +/* eslint-disable security/detect-object-injection */ +'use strict'; + +/** + * A very simple router. + */ + +const { METHODS: httpMethods } = require('http'); +const common = require('../common'); +const { DingusError } = require('../errors'); +const PathParameter = require('./path-parameter'); + +// Internal identifiers for route entries. +const kPathMethods = Symbol('kSqueepDingusRouterPathMethods'); + +const defaultOptions = { + ignoreTrailingSlash: false, + paramPrefix: ':', +}; + +/** + * A naïve router which maps incoming requests to handler functions + * by way of url path and request method. + * + * Regex parsing of paths was eschewed, as a design decision. + * + * Instead, each path to be searched for is deconstructed into a list + * of its constituent parts as strings or objects, for invariant or + * parameterized parts respectively. Each search path is assigned a + * mapping of methods to handler functions. + * + * @property {Object} pathsByLength index to registered paths by number of parts + */ +class Router { + /** + * @param {Object} options + * @param {Boolean} options.ignoreTrailingSlash discard any trailing slashes when registering and comparing paths (default: false) + * @param {String} options.paramPrefix prefix of a path part denoting a named parameter when registering paths (default: ':') + */ + constructor(options = {}) { + common.setOptions(this, defaultOptions, options); + + // Keep lists of paths to match, indexed by path length. + this.pathsByLength = { + 1: [], + }; + } + + + /** + * @typedef {Array} Router~RoutePath + * @property {Object} kPathMethods (symbol key) + */ + /** + * Prepare a path for insertion into search list. + * A route path is an Array of path parts, with a symbolic property of an object mapping method handlers. + * @param {String} rawPath + * @returns {Router~RoutePath} + * @private + */ + _pathToRoutePath(rawPath) { + const routePath = rawPath + .split('/') + .map((p) => p.startsWith(this.paramPrefix) ? new PathParameter(p.slice(this.paramPrefix.length)) : p); + if (this.ignoreTrailingSlash + && routePath[routePath.length - 1] === '') { + routePath.pop(); + } + routePath[kPathMethods] = {}; + Object.freeze(routePath); + return routePath; + } + + + /** + * Compare checkPath to fixedPath, no param substitution, params must match. + * @param {Router~RoutePath} routePath + * @param {Router~RoutePath} checkPath + * @returns {Boolean} + * @private + */ + static _pathCompareExact(routePath, checkPath) { + if (routePath.length !== checkPath.length) { + return false; + } + for (let i = 0; i < routePath.length; i++) { + const fixedPart = routePath[i]; + const checkPart = checkPath[i]; + if (fixedPart instanceof PathParameter && checkPart instanceof PathParameter) { + if (fixedPart.parameter !== checkPart.parameter) { + return false; + } + } else if (fixedPart !== checkPart) { + return false; + } + } + return true; + } + + + /** + * Compare routePath to fixedPath, populating params. + * @param {Router~RoutePath} routePath + * @param {Array} checkPath + * @param {Object} returnParams + * @returns {Boolean} + * @private + */ + static _pathCompareParam(routePath, checkPath, returnParams = {}) { + const params = {}; + + if (routePath.length !== checkPath.length) { + return false; + } + for (let i = 0; i < routePath.length; i++) { + const fixedPart = routePath[i]; + const checkPart = checkPath[i]; + if (fixedPart instanceof PathParameter) { + params[fixedPart.parameter] = checkPart; + } else if (fixedPart !== checkPart) { + return false; + } + } + Object.assign(returnParams, params); + return true; + } + + + /** + * @typedef Router~MatchedPath + * @property {Object} pathParams populated param fields + * @property {Router~RoutePath} matchedPath + */ + /** + * Search for an existing path, return matched path and path parameters. + * @param {Array} matchParts + * @returns {Router~MatchedPath} + * @private + */ + _pathFind(matchParts) { + const result = { + pathParams: {}, + matchedPath: undefined, + }; + const pathsByLength = this.pathsByLength[matchParts.length]; + if (pathsByLength) { + for (const p of pathsByLength) { + if (Router._pathCompareParam(p, matchParts, result.pathParams)) { + result.matchedPath = p; + break; + } + } + } + return result; + } + + + /** + * Return a matching path, no param substitution, params must match + * @param {Router~RoutePath} routePath + * @returns {Router~RoutePath=} + * @private + */ + _pathFindExact(routePath) { + const pathsByLength = this.pathsByLength[routePath.length]; + if (pathsByLength) { + for (const p of pathsByLength) { + if (Router._pathCompareExact(p, routePath)) { + return p; + } + } + } + return undefined; + } + + + /** + * @callback Router~HandlerFn + */ + /** + * @typedef {Object} Router~PathHandler + * @property {Router~HandlerFn} handler + * @property {any[]} handlerArgs + */ + /** + * Insert a new path handler. + * @param {string|string[]} methods + * @param {string} urlPath + * @param {Router~HandlerFn} handler + * @param {any[]} handlerArgs + */ + on(methods, urlPath, handler, handlerArgs = []) { + const matchParts = this._pathToRoutePath(urlPath); + let existingPath = this._pathFindExact(matchParts); + if (!existingPath) { + existingPath = matchParts; + if (!(matchParts.length in this.pathsByLength)) { + this.pathsByLength[matchParts.length] = []; + } + this.pathsByLength[matchParts.length].push(existingPath); + } + if (!Array.isArray(methods)) { + methods = [methods]; + } + if (!Array.isArray(handlerArgs)) { + throw new TypeError(`handlerArgs must be an Array, not '${typeof handlerArgs}'`); + } + methods.forEach((method) => { + if (!httpMethods.includes(method) && method !== '*') { + throw new DingusError(`invalid method '${method}'`); + } + existingPath[kPathMethods][method] = { handler, handlerArgs }; + }); + } + + + /** + * Return an object, which contains a matching handler and any extra + * arguments, for a requested url. + * Also sets path named-parameters as #params and the matched path as + * #matchedPath on the context. + * @param {string} method + * @param {string[]} urlPath + * @param {object} ctx + * @returns {Router~PathHandler} + */ + lookup(method, urlPath, ctx = {}) { + const pathParts = urlPath.split('/').map((part) => decodeURIComponent(part)); + if (this.ignoreTrailingSlash + && pathParts[pathParts.length - 1] === '') { + pathParts.pop(); + } + const { matchedPath, pathParams } = this._pathFind(pathParts); + ctx.params = pathParams; + if (matchedPath) { + ctx.matchedPath = matchedPath; + if (method in matchedPath[kPathMethods]) { + return matchedPath[kPathMethods][method]; + } + if ('*' in matchedPath[kPathMethods]) { + return matchedPath[kPathMethods]['*']; + } + throw new DingusError('NoMethod'); + } + ctx.unmatchedPath = pathParts; + throw new DingusError('NoPath'); + } + + +} +Router.kPathMethods = kPathMethods; + +module.exports = Router; diff --git a/lib/router/path-parameter.js b/lib/router/path-parameter.js new file mode 100644 index 0000000..3c6f1cb --- /dev/null +++ b/lib/router/path-parameter.js @@ -0,0 +1,40 @@ +'use strict'; + +const parameters = new Map(); +/** + * De-duplicating factory of minimal-objects to track the named-parameter parts of path definitions. + * + * @property {String} parameter + */ +class PathParameter extends null { + constructor(parameter) { + if (!parameter || typeof(parameter) !== 'string') { + throw new RangeError('parameter must be string'); + } + if (parameters.has(parameter)) { + return parameters.get(parameter); + } + const pathParameter = Object.create(PathParameter.prototype); + pathParameter.parameter = parameter; + parameters.set(parameter, pathParameter); + Object.freeze(pathParameter); + return pathParameter; + } + + /** + * @returns {String} + */ + toString() { + return `{${this.constructor.name} ${this.parameter}}`; + } + + /** + * Clear the de-duplication table, for tests. + */ + static _flush() { + this.parameters.clear(); + } +} +PathParameter.parameters = parameters; + +module.exports = PathParameter; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8d270e0..d98036a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@squeep/api-dingus", - "version": "1.2.5", + "version": "1.2.6", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -236,9 +236,9 @@ } }, "@eslint/eslintrc": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.1.tgz", - "integrity": "sha512-bxvbYnBPN1Gibwyp6NrpnFzA3YtRL3BBAyEAFVIpNTm2Rn4Vy87GA5M4aSn3InRrlsbX5N0GW7XIx+U4SAEKdQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.2.tgz", + "integrity": "sha512-lTVWHs7O2hjBFZunXTZYnYqtB9GakA1lnxIf+gKq2nY5gxkkNi/lQvveW6t8gFdOHTg6nG50Xs95PrLqVpcaLg==", "dev": true, "requires": { "ajv": "^6.12.4", @@ -259,9 +259,9 @@ "dev": true }, "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "requires": { "ms": "2.1.2" @@ -369,9 +369,9 @@ } }, "@sinonjs/fake-timers": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.0.0.tgz", - "integrity": "sha512-+shXA2X7KNP7H7qNbQTJ3SA+NQc0pZDSBrdvFSRwF8sAo/ohw+ZQFD8Moc+gnz51+1eRXtEQBpKWPiQ4jsRC/w==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz", + "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==", "dev": true, "requires": { "@sinonjs/commons": "^1.7.0" @@ -407,9 +407,9 @@ "dev": true }, "acorn": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", - "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", + "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", "dev": true }, "acorn-jsx": { @@ -799,12 +799,12 @@ "dev": true }, "eslint": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.11.0.tgz", - "integrity": "sha512-/KRpd9mIRg2raGxHRGwW9ZywYNAClZrHjdueHcrVDuO3a6bj83eoTirCCk0M0yPwOjWYKHwRVRid+xK4F/GHgA==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.14.0.tgz", + "integrity": "sha512-3/CE4aJX7LNEiE3i6FeodHmI/38GZtWCsAtsymScmzYapx8q1nVVb+eLcLSzATmCPXw5pT4TqVs1E0OmxAd9tw==", "dev": true, "requires": { - "@eslint/eslintrc": "^1.2.1", + "@eslint/eslintrc": "^1.2.2", "@humanwhocodes/config-array": "^0.9.2", "ajv": "^6.10.0", "chalk": "^4.0.0", @@ -848,9 +848,9 @@ "dev": true }, "debug": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", - "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "requires": { "ms": "2.1.2" @@ -944,18 +944,18 @@ } }, "eslint-plugin-security": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-1.4.0.tgz", - "integrity": "sha512-xlS7P2PLMXeqfhyf3NpqbvbnW04kN8M9NtmhpR3XGyOvt/vNKS7XPXT5EDbwKW9vCjWH4PpfQvgD/+JgN0VJKA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-1.5.0.tgz", + "integrity": "sha512-hAFVwLZ/UeXrlyVD2TDarv/x00CoFVpaY0IUZhKjPjiFxqkuQVixsK4f2rxngeQOqSxi6OUjzJM/jMwKEVjJ8g==", "dev": true, "requires": { - "safe-regex": "^1.1.0" + "safe-regex": "^2.1.1" } }, "eslint-plugin-sonarjs": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.12.0.tgz", - "integrity": "sha512-soxjK67hoYxO8hesKqXWN50GSM+oG2r35N5WnAMehetahO6zoVpv3HZbdziP0jYWNopEe6te/BFUZOYAZI+qhg==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-0.13.0.tgz", + "integrity": "sha512-t3m7ta0EspzDxSOZh3cEOJIJVZgN/TlJYaBGnQlK6W/PZNbWep8q4RQskkJkA7/zwNpX0BaoEOSUUrqaADVoqA==", "dev": true }, "eslint-scope": { @@ -1187,9 +1187,9 @@ } }, "globals": { - "version": "13.12.1", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.1.tgz", - "integrity": "sha512-317dFlgY2pdJZ9rspXDks7073GpDmXdfbM3vYYp0HAMKGDh1FfWPleI2ljVNLQX5M5lXcAslTcPTrOrMEFOjyw==", + "version": "13.13.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.13.0.tgz", + "integrity": "sha512-EQ7Q18AJlPwp3vUDL4mKA0KXrXyNIQyWon6T6XQiBQF0XHvRsiCSrWmmeATpUzdJN2HhWZU6Pdl0a9zdep5p6A==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -1610,9 +1610,9 @@ } }, "mime-db": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", - "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==" + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" }, "minimatch": { "version": "3.0.4", @@ -1624,9 +1624,9 @@ } }, "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, "mocha": { @@ -2256,6 +2256,12 @@ "picomatch": "^2.2.1" } }, + "regexp-tree": { + "version": "0.1.24", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.24.tgz", + "integrity": "sha512-s2aEVuLhvnVJW6s/iPgEGK6R+/xngd2jNQ+xy4bXNDKxZKJH6jpPHY6kVeVv1IeLCHgswRj+Kl3ELaDjG6V1iw==", + "dev": true + }, "regexpp": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", @@ -2298,12 +2304,6 @@ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true - }, "rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -2320,12 +2320,12 @@ "dev": true }, "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz", + "integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==", "dev": true, "requires": { - "ret": "~0.1.10" + "regexp-tree": "~0.1.1" } }, "semver": { @@ -2371,13 +2371,13 @@ "dev": true }, "sinon": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-13.0.1.tgz", - "integrity": "sha512-8yx2wIvkBjIq/MGY1D9h1LMraYW+z1X0mb648KZnKSdvLasvDu7maa0dFaNYdTDczFgbjNw2tOmWdTk9saVfwQ==", + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-13.0.2.tgz", + "integrity": "sha512-KvOrztAVqzSJWMDoxM4vM+GPys1df2VBoXm+YciyB/OLMamfS3VXh3oGh5WtrAGSzrgczNWFFY22oKb7Fi5eeA==", "dev": true, "requires": { "@sinonjs/commons": "^1.8.3", - "@sinonjs/fake-timers": "^9.0.0", + "@sinonjs/fake-timers": "^9.1.2", "@sinonjs/samsam": "^6.1.1", "diff": "^5.0.0", "nise": "^5.1.1", diff --git a/package.json b/package.json index 84e5763..6bff130 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@squeep/api-dingus", - "version": "1.2.5", + "version": "1.2.6", "description": "A minimal API server framework", "main": "index.js", "scripts": { @@ -22,17 +22,17 @@ "author": "Justin Wind ", "license": "ISC", "dependencies": { - "mime-db": "^1.51.0", + "mime-db": "^1.52.0", "uuid": "^8.3.2" }, "devDependencies": { - "eslint": "^8.11.0", + "eslint": "^8.14.0", "eslint-plugin-node": "^11.1.0", - "eslint-plugin-security": "^1.4.0", - "eslint-plugin-sonarjs": "^0.12.0", + "eslint-plugin-security": "^1.5.0", + "eslint-plugin-sonarjs": "^0.13.0", "mocha": "^9.2.2", "nyc": "^15.1.0", "pre-commit": "^1.2.2", - "sinon": "^13.0.1" + "sinon": "^13.0.2" } } diff --git a/test/lib/common.js b/test/lib/common.js index bacd6be..57c4113 100644 --- a/test/lib/common.js +++ b/test/lib/common.js @@ -196,6 +196,32 @@ describe('common', function () { }); }); // responseLogData + describe('handlerLogData', function () { + it('covers', function () { + const req = { + method: 'GET', + somethingElse: 'blah', + }; + const res = { + getHeaders: () => ({}), + statusCode: 200, + blah: 'blah', + }; + const ctx = {}; + const result = common.handlerLogData(req, res, ctx); + assert.deepStrictEqual(result, { + req: { + method: 'GET', + }, + res: { + headers: {}, + statusCode: 200, + }, + ctx: {}, + }); + }); + }); // handlerLogData + describe('setOptions', function () { it('sets options', function () { const expected = { diff --git a/test/lib/dingus.js b/test/lib/dingus.js index 356974f..ab61256 100644 --- a/test/lib/dingus.js +++ b/test/lib/dingus.js @@ -535,6 +535,14 @@ describe('Dingus', function () { assert.strictEqual(e.statusCode, 413); } }); + it('provides buffer', async function () { + const p = dingus.bodyData(res, 0, false); + const expected = Buffer.from('bleat'); + resEvents['data'](expected); + resEvents['end'](); + const result = await p; + assert.deepStrictEqual(result, expected); + }); }); // bodyData describe('ingestBody', function () { @@ -813,6 +821,13 @@ Content-Type: image/sgi await dingus.serveFile(req, res, ctx, directory, fileName); assert(res.end.called); }); + it('handles misconfigured encoding', async function () { + Enum.EncodingType.Flarp = 'flarp'; + req._headers[Enum.Header.AcceptEncoding] = 'flarp, gzip'; + await dingus.serveFile(req, res, ctx, directory, fileName); + delete Enum.EncodingType.Flarp; + assert(res.end.called); + }); }); // serveFile describe('renderError', function () { diff --git a/test/lib/router.js b/test/lib/router.js index 7cc92a0..d639170 100644 --- a/test/lib/router.js +++ b/test/lib/router.js @@ -5,6 +5,7 @@ const assert = require('assert'); const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-require const Router = require('../../lib/router'); +const PathParameter = require('../../lib/router/path-parameter') const { DingusError } = require('../../lib/errors'); const noExpectedException = 'did not get expected exception'; @@ -22,61 +23,61 @@ describe('Router', function () { router.ignoreTrailingSlash = _its; }); - describe('_pathDefinitionToPathMatch', function () { + describe('_pathToRoutePath', function () { it('defines a simple path', function () { const p = '/a/b/c'; const expected = ['', 'a', 'b', 'c']; - expected[router.METHODS] = {}; - const r = router._pathDefinitionToPathMatch(p); + expected[Router.kPathMethods] = {}; + const r = router._pathToRoutePath(p); assert.deepStrictEqual(r, expected); }); it('defines a path with parameter', function () { const p = '/a/b/:c'; - const expected = ['', 'a', 'b', { [router.PARAM]: 'c' }]; - expected[router.METHODS] = {}; - const r = router._pathDefinitionToPathMatch(p); + const expected = ['', 'a', 'b', new PathParameter('c')]; + expected[Router.kPathMethods] = {}; + const r = router._pathToRoutePath(p); assert.deepStrictEqual(r, expected); }); it('defines a path with trailing slash', function () { router.ignoreTrailingSlash = true; const p = '/a/b/:c/'; - const expected = ['', 'a', 'b', { [router.PARAM]: 'c' }]; - expected[router.METHODS] = {}; - const r = router._pathDefinitionToPathMatch(p); + const expected = ['', 'a', 'b', new PathParameter('c')]; + expected[Router.kPathMethods] = {}; + const r = router._pathToRoutePath(p); assert.deepStrictEqual(r, expected); }); - }); // _pathDefinitionToPathMatch + }); // _pathToRoutePath describe('_pathCompareExact', function () { let fixedPath, checkPath; it('compares static paths which match', function () { - fixedPath = router._pathDefinitionToPathMatch('/a/b/c'); - checkPath = router._pathDefinitionToPathMatch('/a/b/c'); + fixedPath = router._pathToRoutePath('/a/b/c'); + checkPath = router._pathToRoutePath('/a/b/c'); const r = Router._pathCompareExact(fixedPath, checkPath); assert.strictEqual(r, true); }); it('compares static paths which do not match', function () { - fixedPath = router._pathDefinitionToPathMatch('/a/b/c'); - checkPath = router._pathDefinitionToPathMatch('/a/b/d'); + fixedPath = router._pathToRoutePath('/a/b/c'); + checkPath = router._pathToRoutePath('/a/b/d'); const r = Router._pathCompareExact(fixedPath, checkPath); assert.strictEqual(r, false); }); it('compares unequal static paths', function () { - fixedPath = router._pathDefinitionToPathMatch('/a/b/c'); - checkPath = router._pathDefinitionToPathMatch('/a/b'); + fixedPath = router._pathToRoutePath('/a/b/c'); + checkPath = router._pathToRoutePath('/a/b'); const r = Router._pathCompareExact(fixedPath, checkPath); assert.strictEqual(r, false); }); it('compares param paths which match', function () { - fixedPath = router._pathDefinitionToPathMatch('/a/:b/c'); - checkPath = router._pathDefinitionToPathMatch('/a/:b/c'); + fixedPath = router._pathToRoutePath('/a/:b/c'); + checkPath = router._pathToRoutePath('/a/:b/c'); const r = Router._pathCompareExact(fixedPath, checkPath); assert.strictEqual(r, true); }); it('compares param paths which do not match', function () { - fixedPath = router._pathDefinitionToPathMatch('/a/:b/c'); - checkPath = router._pathDefinitionToPathMatch('/a/:g/c'); + fixedPath = router._pathToRoutePath('/a/:b/c'); + checkPath = router._pathToRoutePath('/a/:g/c'); const r = Router._pathCompareExact(fixedPath, checkPath); assert.strictEqual(r, false); }); @@ -88,8 +89,8 @@ describe('Router', function () { it('compares static paths which match', function () { const params = {}; const expectedParams = {}; - fixedPath = router._pathDefinitionToPathMatch('/a/b/c'); - checkPath = router._pathDefinitionToPathMatch('/a/b/c'); + fixedPath = router._pathToRoutePath('/a/b/c'); + checkPath = router._pathToRoutePath('/a/b/c'); const r = Router._pathCompareParam(fixedPath, checkPath); assert.strictEqual(r, true); assert.deepStrictEqual(params, expectedParams); @@ -97,8 +98,8 @@ describe('Router', function () { it('compares static paths which do not match', function () { const params = {}; const expectedParams = {}; - fixedPath = router._pathDefinitionToPathMatch('/a/b/c'); - checkPath = router._pathDefinitionToPathMatch('/a/b/d'); + fixedPath = router._pathToRoutePath('/a/b/c'); + checkPath = router._pathToRoutePath('/a/b/d'); const r = Router._pathCompareParam(fixedPath, checkPath, params); assert.strictEqual(r, false); assert.deepStrictEqual(params, expectedParams); @@ -106,8 +107,8 @@ describe('Router', function () { it('compares unequal static paths', function () { const params = {}; const expectedParams = {}; - fixedPath = router._pathDefinitionToPathMatch('/a/b/c'); - checkPath = router._pathDefinitionToPathMatch('/a/b'); + fixedPath = router._pathToRoutePath('/a/b/c'); + checkPath = router._pathToRoutePath('/a/b'); const r = Router._pathCompareParam(fixedPath, checkPath, params); assert.strictEqual(r, false); assert.deepStrictEqual(params, expectedParams); @@ -117,8 +118,8 @@ describe('Router', function () { const expectedParams = { b: 'bar', }; - fixedPath = router._pathDefinitionToPathMatch('/a/:b/c'); - checkPath = router._pathDefinitionToPathMatch('/a/bar/c'); + fixedPath = router._pathToRoutePath('/a/:b/c'); + checkPath = router._pathToRoutePath('/a/bar/c'); const r = Router._pathCompareParam(fixedPath, checkPath, params); assert.strictEqual(r, true); assert.deepStrictEqual(params, expectedParams); @@ -129,8 +130,8 @@ describe('Router', function () { b: 'gaz', c: '123', }; - fixedPath = router._pathDefinitionToPathMatch('/a/:b/:c'); - checkPath = router._pathDefinitionToPathMatch('/a/gaz/123'); + fixedPath = router._pathToRoutePath('/a/:b/:c'); + checkPath = router._pathToRoutePath('/a/gaz/123'); const r = Router._pathCompareParam(fixedPath, checkPath, params); assert.strictEqual(r, true); assert.deepStrictEqual(params, expectedParams); @@ -143,8 +144,8 @@ describe('Router', function () { beforeEach(function () { pathsByLengthOrig = router.pathsByLength; router.pathsByLength = { - 2: [ router._pathDefinitionToPathMatch('/:id') ], - 3: [ router._pathDefinitionToPathMatch('/a/b') ], + 2: [ router._pathToRoutePath('/:id') ], + 3: [ router._pathToRoutePath('/a/b') ], }; }); afterEach(function () { @@ -167,7 +168,7 @@ describe('Router', function () { describe('_pathFindExact', function () { it('finds a path', function () { - const pathParts = ['', { [router.PARAM]: 'id' }]; + const pathParts = ['', new PathParameter('id')]; const r = router._pathFindExact(pathParts); assert.strictEqual(r, router.pathsByLength[2][0]); }); @@ -191,7 +192,7 @@ describe('Router', function () { beforeEach(function () { pathsByLengthOrig = router.pathsByLength; router.pathsByLength = { - 2: [ router._pathDefinitionToPathMatch('/:id') ], + 2: [ router._pathToRoutePath('/:id') ], }; }); afterEach(function () { @@ -200,17 +201,17 @@ describe('Router', function () { it('adds new path', function () { const urlPath = '/a/:id'; - const expected = router._pathDefinitionToPathMatch(urlPath); - expected[router.METHODS]['GET'] = stubEntry; + const expected = router._pathToRoutePath(urlPath); + expected[Router.kPathMethods]['GET'] = stubEntry; router.on('GET', urlPath, stubHandler); assert.deepStrictEqual(router.pathsByLength[3][0], expected); }); it('adds new method to path', function () { const urlPath = '/a/:id'; - const expected = router._pathDefinitionToPathMatch(urlPath); - expected[router.METHODS]['GET'] = stubEntry; - expected[router.METHODS]['POST'] = stubEntry; + const expected = router._pathToRoutePath(urlPath); + expected[Router.kPathMethods]['GET'] = stubEntry; + expected[Router.kPathMethods]['POST'] = stubEntry; router.on('GET', urlPath, stubHandler); router.on('POST', urlPath, stubHandler); @@ -219,8 +220,8 @@ describe('Router', function () { it('add some more paths', function () { let urlPath = '/a/b/c/d'; - const expected = router._pathDefinitionToPathMatch(urlPath); - expected[router.METHODS]['GET'] = stubEntry; + const expected = router._pathToRoutePath(urlPath); + expected[Router.kPathMethods]['GET'] = stubEntry; router.on('GET', urlPath, stubHandler); urlPath = '/a/b/x/y'; router.on('GET', urlPath, stubHandler); @@ -230,9 +231,9 @@ describe('Router', function () { it('adds multiple methods', function () { const urlPath = '/:id'; - const expected = router._pathDefinitionToPathMatch(urlPath); - expected[router.METHODS]['GET'] = stubEntry; - expected[router.METHODS]['HEAD'] = stubEntry; + const expected = router._pathToRoutePath(urlPath); + expected[Router.kPathMethods]['GET'] = stubEntry; + expected[Router.kPathMethods]['HEAD'] = stubEntry; router.on(['GET', 'HEAD'], urlPath, stubHandler); assert.deepStrictEqual(router.pathsByLength[2][0], expected); @@ -240,8 +241,8 @@ describe('Router', function () { it('adds new wildcard path', function () { const urlPath = '/a/:id'; - const expected = router._pathDefinitionToPathMatch(urlPath); - expected[router.METHODS]['*'] = stubEntry; + const expected = router._pathToRoutePath(urlPath); + expected[Router.kPathMethods]['*'] = stubEntry; router.on('*', urlPath, stubHandler); assert.deepStrictEqual(router.pathsByLength[3][0], expected); }); diff --git a/test/lib/router/path-parameter.js b/test/lib/router/path-parameter.js new file mode 100644 index 0000000..9b2fb1e --- /dev/null +++ b/test/lib/router/path-parameter.js @@ -0,0 +1,51 @@ +/* eslint-env mocha */ +'use strict'; + +const assert = require('assert'); +const PathParameter = require('../../../lib/router/path-parameter'); + +const noExpectedException = 'did not receive expected exception'; +describe('PathParameter', function () { + beforeEach(function () { + PathParameter._flush(); + }); + it('requires a parameter', function () { + try { + new PathParameter(); + assert.fail(noExpectedException); + } catch (e) { + assert(e instanceof RangeError, noExpectedException); + } + }); + it('requires parameter be a string', function () { + try { + new PathParameter({}); + assert.fail(noExpectedException); + } catch (e) { + assert(e instanceof RangeError, noExpectedException); + } + }); + it('creates a parameter object', function () { + const p = new PathParameter('foo'); + assert(p instanceof PathParameter); + assert.strictEqual(p.parameter, 'foo'); + }); + it('duplicate parameters are the same object', function () { + const p1 = new PathParameter('foo'); + const p2 = new PathParameter('foo'); + assert.strictEqual(p1, p2); + }); + it('shows itself', function () { + const p = new PathParameter('foo'); + assert(p.toString().includes('foo')); + }); + it('parameters are immutable', function () { + const p = new PathParameter('foo'); + try { + p[PathParameter.kPathParameter] = 'bar'; + assert.fail(noExpectedException); + } catch (e) { + assert(e instanceof TypeError, noExpectedException); + } + }); +}); // PathParameter \ No newline at end of file