651 lines
19 KiB
JavaScript
651 lines
19 KiB
JavaScript
/**
|
|
* File: mockRequest
|
|
*
|
|
* This file implements node.js's implementation of a 'request' object.
|
|
* This is actually closer to what Express offers the user, in that the
|
|
* body is really a parsed object of values.
|
|
*
|
|
* @author Howard Abrams <howard.abrams@gmail.com>
|
|
*/
|
|
|
|
/**
|
|
* Function: createRequest
|
|
*
|
|
* Creates a new mock 'request' instance. All values are reset to the
|
|
* defaults.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* options - An object of named parameters.
|
|
*
|
|
* Options:
|
|
*
|
|
* method - The method value, see <mockRequest._setMethod>
|
|
* url - The url value, see <mockRequest._setURL>
|
|
* originalUrl - The originalUrl value, see <mockRequest._setOriginalUrl>
|
|
* baseUrl - The baseUrl value, see <mockRequest._setBaseUrl>
|
|
* params - The parameters, see <mockRequest._setParam>
|
|
* body - The body values, , see <mockRequest._setBody>
|
|
*/
|
|
|
|
const url = require('url');
|
|
const typeis = require('type-is');
|
|
const accepts = require('accepts');
|
|
const parseRange = require('range-parser');
|
|
let { EventEmitter } = require('events');
|
|
const querystring = require('querystring');
|
|
const { createHeaders, getHeaderValue } = require('./headers');
|
|
|
|
const standardRequestOptions = [
|
|
'method',
|
|
'url',
|
|
'originalUrl',
|
|
'baseUrl',
|
|
'path',
|
|
'params',
|
|
'session',
|
|
'cookies',
|
|
'headers',
|
|
'body',
|
|
'query',
|
|
'files'
|
|
];
|
|
|
|
function createRequest(options = {}) {
|
|
if (options.eventEmitter) {
|
|
EventEmitter = options.eventEmitter;
|
|
}
|
|
|
|
// create mockRequest
|
|
const mockRequest = Object.create(EventEmitter.prototype);
|
|
EventEmitter.call(mockRequest);
|
|
|
|
mockRequest.method = options.method ? options.method : 'GET';
|
|
mockRequest.url = options.url || options.path || '';
|
|
mockRequest.originalUrl = options.originalUrl || mockRequest.url;
|
|
mockRequest.baseUrl = options.baseUrl || mockRequest.url;
|
|
mockRequest.path = options.path || (options.url ? url.parse(options.url).pathname : '');
|
|
mockRequest.params = options.params ? options.params : {};
|
|
if (options.session) {
|
|
mockRequest.session = options.session;
|
|
}
|
|
mockRequest.cookies = options.cookies ? options.cookies : {};
|
|
if (options.signedCookies) {
|
|
mockRequest.signedCookies = options.signedCookies;
|
|
}
|
|
|
|
// Create headers using the Headers.js module
|
|
mockRequest.headers = createHeaders(options.headers);
|
|
|
|
mockRequest.body = options.body ? options.body : {};
|
|
mockRequest.query = options.query ? options.query : {};
|
|
mockRequest.files = options.files ? options.files : {};
|
|
mockRequest.socket = options.socket ? options.socket : {};
|
|
|
|
mockRequest.ip = options.ip || '127.0.0.1';
|
|
mockRequest.ips = [mockRequest.ip];
|
|
|
|
mockRequest.destroy = () => {};
|
|
|
|
// parse query string from url to object
|
|
if (Object.keys(mockRequest.query).length === 0) {
|
|
mockRequest.query = querystring.parse(mockRequest.url.split('?')[1]);
|
|
|
|
if (!mockRequest.query.hasOwnProperty) {
|
|
Object.defineProperty(mockRequest.query, 'hasOwnProperty', {
|
|
enumerable: false,
|
|
value: Object.hasOwnProperty.bind(mockRequest.query)
|
|
});
|
|
}
|
|
}
|
|
|
|
// attach any other provided objects into the request for more advanced testing
|
|
for (const n in options) {
|
|
if (standardRequestOptions.indexOf(n) === -1) {
|
|
mockRequest[n] = options[n];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return request header.
|
|
*
|
|
* The `Referrer` header field is special-cased,
|
|
* both `Referrer` and `Referer` are interchangeable.
|
|
*
|
|
* Examples:
|
|
*
|
|
* mockRequest.get('Content-Type');
|
|
* // => "text/plain"
|
|
*
|
|
* mockRequest.get('content-type');
|
|
* // => "text/plain"
|
|
*
|
|
* mockRequest.get('Something');
|
|
* // => undefined
|
|
*
|
|
* Aliased as `mockRequest.header()`.
|
|
*
|
|
* @param {String} name
|
|
* @return {String}
|
|
* @api public
|
|
*/
|
|
mockRequest.getHeader = function getHeader(name) {
|
|
return getHeaderValue(mockRequest.headers, name);
|
|
};
|
|
mockRequest.header = mockRequest.getHeader;
|
|
mockRequest.get = mockRequest.getHeader;
|
|
|
|
/**
|
|
* Function: is
|
|
*
|
|
* Checks for matching content types in the content-type header.
|
|
* Requires a request body, identified by transfer-encoding or content-length headers
|
|
*
|
|
* Examples:
|
|
*
|
|
* mockRequest.headers['content-type'] = 'text/html';
|
|
* mockRequest.headers['transfer-encoding'] = 'chunked';
|
|
* mockRequest.headers['content-length'] = '100';
|
|
*
|
|
* mockRequest.is('html');
|
|
* // => "html"
|
|
*
|
|
* mockRequest.is('json');
|
|
* // => false
|
|
*
|
|
* mockRequest.is(['json', 'html', 'text']);
|
|
* // => "html"
|
|
*
|
|
* @param {String|String[]} types content type or array of types to match
|
|
* @return {String|false|null} Matching content type as string, false if no match, null if request has no body.
|
|
* @api public
|
|
*/
|
|
mockRequest.is = function isContentType(...args) {
|
|
let types = args;
|
|
|
|
if (Array.isArray(args[0])) {
|
|
types = args[0];
|
|
}
|
|
|
|
return typeis(mockRequest, types);
|
|
};
|
|
|
|
/**
|
|
* Function: accepts
|
|
*
|
|
* Checks for matching content types in the Accept header.
|
|
*
|
|
* Examples:
|
|
*
|
|
* mockRequest.headers['accept'] = 'application/json'
|
|
*
|
|
* mockRequest.accepts('json');
|
|
* // => 'json'
|
|
*
|
|
* mockRequest.accepts('html');
|
|
* // => false
|
|
*
|
|
* mockRequest.accepts(['html', 'json']);
|
|
* // => 'json'
|
|
*
|
|
* @param {String|String[]} types Mime type(s) to check against
|
|
* @return {String|false} Matching type or false if no match.
|
|
*/
|
|
mockRequest.accepts = function acceptsTypes(types) {
|
|
const Accepts = accepts(mockRequest);
|
|
return Accepts.type(types);
|
|
};
|
|
|
|
/**
|
|
* Check if the given `encoding`s are accepted.
|
|
*
|
|
* @param {String} ...encoding
|
|
* @return {String|Array}
|
|
* @public
|
|
*/
|
|
mockRequest.acceptsEncodings = function acceptsEncodings(...args) {
|
|
let encodings = args;
|
|
|
|
if (Array.isArray(args[0])) {
|
|
encodings = args[0];
|
|
}
|
|
|
|
const accept = accepts(mockRequest);
|
|
return accept.encodings(encodings);
|
|
};
|
|
|
|
/**
|
|
* Check if the given `charset`s are acceptable,
|
|
* otherwise you should respond with 406 "Not Acceptable".
|
|
*
|
|
* @param {String} ...charset
|
|
* @return {String|Array}
|
|
* @public
|
|
*/
|
|
mockRequest.acceptsCharsets = function acceptsCharsets(...args) {
|
|
let charsets = args;
|
|
|
|
if (Array.isArray(args[0])) {
|
|
charsets = args[0];
|
|
}
|
|
|
|
const accept = accepts(mockRequest);
|
|
return accept.charsets(charsets);
|
|
};
|
|
|
|
/**
|
|
* Check if the given `lang`s are acceptable,
|
|
* otherwise you should respond with 406 "Not Acceptable".
|
|
*
|
|
* @param {String} ...lang
|
|
* @return {String|Array}
|
|
* @public
|
|
*/
|
|
mockRequest.acceptsLanguages = function acceptsLanguages(...args) {
|
|
let languages = args;
|
|
|
|
if (Array.isArray(args[0])) {
|
|
languages = args[0];
|
|
}
|
|
|
|
const accept = accepts(mockRequest);
|
|
return accept.languages(languages);
|
|
};
|
|
|
|
/**
|
|
* Function: range
|
|
*
|
|
* Parse Range header field, capping to the given `size`.
|
|
*
|
|
* Unspecified ranges such as "0-" require knowledge of your resource length. In
|
|
* the case of a byte range this is of course the total number of bytes. If the
|
|
* Range header field is not given `undefined` is returned, `-1` when unsatisfiable,
|
|
* and `-2` when syntactically invalid.
|
|
*
|
|
* When ranges are returned, the array has a "type" property which is the type of
|
|
* range that is required (most commonly, "bytes"). Each array element is an object
|
|
* with a "start" and "end" property for the portion of the range.
|
|
*
|
|
* The "combine" option can be set to `true` and overlapping & adjacent ranges
|
|
* will be combined into a single range.
|
|
*
|
|
* NOTE: remember that ranges are inclusive, so for example "Range: users=0-3"
|
|
* should respond with 4 users when available, not 3.
|
|
*
|
|
* @param {number} size
|
|
* @param {object} [opts]
|
|
* @param {boolean} [opts.combine=false]
|
|
* @return {false|number|array}
|
|
* @public
|
|
*/
|
|
mockRequest.range = function isRange(size, opts) {
|
|
const range = mockRequest.get('Range');
|
|
if (!range) {
|
|
return undefined;
|
|
}
|
|
return parseRange(size, range, opts);
|
|
};
|
|
|
|
/**
|
|
* Function: param
|
|
*
|
|
* Return the value of param name when present.
|
|
* Lookup is performed in the following order:
|
|
* - req.params
|
|
* - req.body
|
|
* - req.query
|
|
*/
|
|
mockRequest.param = function param(parameterName, defaultValue) {
|
|
if ({}.hasOwnProperty.call(mockRequest.params, parameterName)) {
|
|
return mockRequest.params[parameterName];
|
|
}
|
|
if ({}.hasOwnProperty.call(mockRequest.body, parameterName)) {
|
|
return mockRequest.body[parameterName];
|
|
}
|
|
if ({}.hasOwnProperty.call(mockRequest.query, parameterName)) {
|
|
return mockRequest.query[parameterName];
|
|
}
|
|
return defaultValue;
|
|
};
|
|
|
|
/**
|
|
* Function: _setParameter
|
|
*
|
|
* Set parameters that the client can then get using the 'params'
|
|
* key.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* key - The key. For instance, 'bob' would be accessed: request.params.bob
|
|
* value - The value to return when accessed.
|
|
*/
|
|
mockRequest._setParameter = function _setParameter(key, value) {
|
|
mockRequest.params[key] = value;
|
|
};
|
|
|
|
/**
|
|
* Sets a variable that is stored in the session.
|
|
*
|
|
* @param variable The variable to store in the session
|
|
* @param value The value associated with the variable
|
|
*/
|
|
mockRequest._setSessionVariable = function _setSessionVariable(variable, value) {
|
|
if (typeof mockRequest.session !== 'object') {
|
|
mockRequest.session = {};
|
|
}
|
|
mockRequest.session[variable] = value;
|
|
};
|
|
|
|
/**
|
|
* Sets a variable that is stored in the cookies.
|
|
*
|
|
* @param variable The variable to store in the cookies
|
|
* @param value The value associated with the variable
|
|
*/
|
|
mockRequest._setCookiesVariable = function _setCookiesVariable(variable, value) {
|
|
mockRequest.cookies[variable] = value;
|
|
};
|
|
|
|
/**
|
|
* Sets a variable that is stored in the signed cookies.
|
|
*
|
|
* @param variable The variable to store in the signed cookies
|
|
* @param value The value associated with the variable
|
|
*/
|
|
mockRequest._setSignedCookiesVariable = function _setSignedCookiesVariable(variable, value) {
|
|
if (typeof mockRequest.signedCookies !== 'object') {
|
|
mockRequest.signedCookies = {};
|
|
}
|
|
mockRequest.signedCookies[variable] = value;
|
|
};
|
|
|
|
/**
|
|
* Sets a variable that is stored in the headers.
|
|
*
|
|
* @param variable The variable to store in the headers
|
|
* @param value The value associated with the variable
|
|
*/
|
|
mockRequest._setHeadersVariable = function _setHeadersVariable(variable, value) {
|
|
mockRequest.headers[variable] = value;
|
|
};
|
|
|
|
/**
|
|
* Sets a variable that is stored in the files.
|
|
*
|
|
* @param variable The variable to store in the files
|
|
* @param value The value associated with the variable
|
|
*/
|
|
mockRequest._setFilesVariable = function _setFilesVariable(variable, value) {
|
|
mockRequest.files[variable] = value;
|
|
};
|
|
|
|
/**
|
|
* Function: _setMethod
|
|
*
|
|
* Sets the HTTP method that the client gets when the called the 'method'
|
|
* property. This defaults to 'GET' if it is not set.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* method - The HTTP method, e.g. GET, POST, PUT, DELETE, etc.
|
|
*
|
|
* Note: We don't validate the string. We just return it.
|
|
*/
|
|
mockRequest._setMethod = function _setMethod(method) {
|
|
mockRequest.method = method;
|
|
};
|
|
|
|
/**
|
|
* Function: _setURL
|
|
*
|
|
* Sets the URL value that the client gets when the called the 'url'
|
|
* property.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* value - The request path, e.g. /my-route/452
|
|
*
|
|
* Note: We don't validate the string. We just return it. Typically, these
|
|
* do not include hostname, port or that part of the URL.
|
|
*/
|
|
mockRequest._setURL = function _setURL(value) {
|
|
mockRequest.url = value;
|
|
};
|
|
|
|
/**
|
|
* Function: _setBaseUrl
|
|
*
|
|
* Sets the URL value that the client gets when the called the 'baseUrl'
|
|
* property.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* value - The request base path, e.g. /my-route
|
|
*
|
|
* Note: We don't validate the string. We just return it. Typically, these
|
|
* do not include hostname, port or that part of the URL.
|
|
*/
|
|
mockRequest._setBaseUrl = function _setBaseUrl(value) {
|
|
mockRequest.baseUrl = value;
|
|
};
|
|
|
|
/**
|
|
* Function: _setOriginalUrl
|
|
*
|
|
* Sets the URL value that the client gets when the called the 'originalUrl'
|
|
* property.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* value - The request path, e.g. /my-route/452
|
|
*
|
|
* Note: We don't validate the string. We just return it. Typically, these
|
|
* do not include hostname, port or that part of the URL.
|
|
*/
|
|
mockRequest._setOriginalUrl = function _setOriginalUrl(value) {
|
|
mockRequest.originalUrl = value;
|
|
};
|
|
|
|
/**
|
|
* Function: _setBody
|
|
*
|
|
* Sets the body that the client gets when the called the 'body'
|
|
* parameter. This defaults to 'GET' if it is not set.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* body - An object representing the body.
|
|
*
|
|
* If you expect the 'body' to come from a form, this typically means that
|
|
* it would be a flat object of properties and values, as in:
|
|
*
|
|
* > { name: 'Howard Abrams',
|
|
* > age: 522
|
|
* > }
|
|
*
|
|
* If the client is expecting a JSON object through a REST interface, then
|
|
* this object could be anything.
|
|
*/
|
|
mockRequest._setBody = function _setBody(body) {
|
|
mockRequest.body = body;
|
|
};
|
|
|
|
/**
|
|
* Function: _addBody
|
|
*
|
|
* Adds another body parameter the client gets when calling the 'body'
|
|
* parameter with another property value, e.g. the name of a form element
|
|
* that was passed in.
|
|
*
|
|
* Parameters:
|
|
*
|
|
* key - The key. For instance, 'bob' would be accessed: request.params.bob
|
|
* value - The value to return when accessed.
|
|
*/
|
|
mockRequest._addBody = function _addBody(key, value) {
|
|
mockRequest.body[key] = value;
|
|
};
|
|
|
|
/**
|
|
* Function: send
|
|
*
|
|
* Write data to the request stream which will trigger request's 'data', and 'end' event
|
|
*
|
|
* Parameters:
|
|
*
|
|
* data - string, array, object, number, buffer
|
|
*/
|
|
mockRequest.send = function send(data) {
|
|
if (Buffer.isBuffer(data)) {
|
|
this.emit('data', data);
|
|
} else if (typeof data === 'object' || typeof data === 'number') {
|
|
this.emit('data', Buffer.from(JSON.stringify(data)));
|
|
} else if (typeof data === 'string') {
|
|
this.emit('data', Buffer.from(data));
|
|
}
|
|
this.emit('end');
|
|
};
|
|
|
|
/**
|
|
* Function: hostname
|
|
*
|
|
* If Hostname is not set explicitly, then derive it from the Host header without port information
|
|
*
|
|
*/
|
|
if (!mockRequest.hostname) {
|
|
mockRequest.hostname = (function getHostname() {
|
|
if (!mockRequest.headers.host) {
|
|
return '';
|
|
}
|
|
|
|
const hostname = mockRequest.headers.host.split(':')[0].split('.');
|
|
return hostname.join('.');
|
|
})();
|
|
}
|
|
|
|
/**
|
|
* Function: subdomains
|
|
*
|
|
* Subdomains are the dot-separated parts of the host before the main domain of the app.
|
|
*
|
|
*/
|
|
mockRequest.subdomains = (function getSubdomains() {
|
|
if (!mockRequest.headers.host) {
|
|
return [];
|
|
}
|
|
|
|
const offset = 2;
|
|
const subdomains = mockRequest.headers.host.split('.').reverse();
|
|
|
|
return subdomains.slice(offset);
|
|
})();
|
|
|
|
/**
|
|
* Function: asyncIterator
|
|
*
|
|
* Buffers data, error, end, and close events and yields them in order.
|
|
* Unlike stream.Readable, this async iterator implementation will not exit
|
|
* early on error or close.
|
|
*/
|
|
mockRequest[Symbol.asyncIterator] = async function* asyncIterator() {
|
|
let ended = false;
|
|
let closed = false;
|
|
let error = null;
|
|
const chunks = [];
|
|
let resolvePromise = null;
|
|
|
|
const promiseExecutor = (resolve) => {
|
|
resolvePromise = resolve;
|
|
};
|
|
|
|
const promiseResolver = () => {
|
|
if (resolvePromise) {
|
|
resolvePromise();
|
|
resolvePromise = null;
|
|
}
|
|
};
|
|
const dataEventHandler = (chunk) => {
|
|
if (ended || closed || error) {
|
|
return;
|
|
}
|
|
chunks.push(chunk);
|
|
promiseResolver();
|
|
};
|
|
const endEventHandler = () => {
|
|
if (ended || closed || error) {
|
|
return;
|
|
}
|
|
ended = true;
|
|
promiseResolver();
|
|
};
|
|
const closeEventHandler = () => {
|
|
if (closed || error) {
|
|
return;
|
|
}
|
|
closed = true;
|
|
promiseResolver();
|
|
};
|
|
const errorEventHandler = (err) => {
|
|
if (closed || error) {
|
|
return;
|
|
}
|
|
error = err;
|
|
promiseResolver();
|
|
};
|
|
|
|
mockRequest.on('data', dataEventHandler);
|
|
mockRequest.on('end', endEventHandler);
|
|
mockRequest.on('close', closeEventHandler);
|
|
mockRequest.on('error', errorEventHandler);
|
|
|
|
// Emit custom event after entering the loop.
|
|
setTimeout(() => {
|
|
this.emit('async_iterator');
|
|
});
|
|
|
|
try {
|
|
for (;;) {
|
|
// eslint-disable-next-line no-await-in-loop
|
|
await new Promise(promiseExecutor);
|
|
let i = 0;
|
|
for (;;) {
|
|
if (error) {
|
|
throw error;
|
|
}
|
|
if (closed) {
|
|
return;
|
|
}
|
|
|
|
const hasChunks = i < chunks.length;
|
|
if (!hasChunks) {
|
|
if (ended) {
|
|
// End signaled. Bail.
|
|
return;
|
|
}
|
|
// Wait for next push.
|
|
break;
|
|
}
|
|
|
|
const chunk = chunks[i];
|
|
chunks[i] = undefined;
|
|
i += 1;
|
|
yield chunk;
|
|
}
|
|
chunks.length = 0;
|
|
}
|
|
} finally {
|
|
chunks.length = 0;
|
|
error = null;
|
|
|
|
mockRequest.off('data', dataEventHandler);
|
|
mockRequest.off('end', endEventHandler);
|
|
mockRequest.off('close', closeEventHandler);
|
|
mockRequest.off('error', errorEventHandler);
|
|
}
|
|
};
|
|
|
|
return mockRequest;
|
|
}
|
|
|
|
module.exports.createRequest = createRequest;
|