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');
/**
* 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');
};
/**
- * @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),
});
/**
- *
+ * 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;
* 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)) {
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) => {
/**
* Return a new object with selected props.
* @param {Object} obj
- * @param {string[]} props
+ * @param {String[]} props
+ * @returns {Object}
*/
const pick = (obj, props) => {
const picked = {};
* 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, [
* 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) {
* 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) {
/**
* 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, [
/**
- * 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
};
/**
- * 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);
};
/**
- * 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,
};
/**
- * 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) {
/**
* Merges folded header lines
* @param {String[]} lines
+ * @returns {String}
*/
const unfoldHeaderLines = (lines) => {
const foldedLineRE = /^(\t| +)(.*)$/;
strictAccept: true,
selfBaseUrl: '',
staticMetadata: true,
- staticPath: undefined, // no reasonable default
+ staticPath: undefined, // No reasonable default
trustProxy: true,
querystring,
};
* 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 = [];
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);
*/
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) {
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);
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);
}
/**
* 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) {
+++ /dev/null
-/* 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;
--- /dev/null
+/* 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<String|PathParameter>} 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<String>} 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<String>} 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;
--- /dev/null
+'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
{
"name": "@squeep/api-dingus",
- "version": "1.2.5",
+ "version": "1.2.6",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
}
},
"@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",
"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"
}
},
"@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"
"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": {
"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",
"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"
}
},
"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": {
}
},
"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"
}
},
"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",
}
},
"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": {
"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",
"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",
"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": {
"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",
{
"name": "@squeep/api-dingus",
- "version": "1.2.5",
+ "version": "1.2.6",
"description": "A minimal API server framework",
"main": "index.js",
"scripts": {
"author": "Justin Wind <jwind-npm@squeep.com>",
"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"
}
}
});
}); // 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 = {
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 () {
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 () {
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';
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);
});
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);
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);
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);
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);
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);
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 () {
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]);
});
beforeEach(function () {
pathsByLengthOrig = router.pathsByLength;
router.pathsByLength = {
- 2: [ router._pathDefinitionToPathMatch('/:id') ],
+ 2: [ router._pathToRoutePath('/:id') ],
};
});
afterEach(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);
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);
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);
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);
});
--- /dev/null
+/* 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