/** * 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 */ /** * 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 * url - The url value, see * originalUrl - The originalUrl value, see * baseUrl - The baseUrl value, see * params - The parameters, see * body - The body values, , see */ 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;