UNPKG

@luminati-io/mountebank

Version:

Over the wire test doubles

419 lines (368 loc) 12.1 kB
'use strict'; const helpers = require('../util/helpers.js'), errors = require('../util/errors.js'); /** * An abstraction for loading imposters from in-memory * @module */ function repeatsFor (response) { return response.repeat || 1; } function repeatTransform (responses) { const result = []; let response, repeats; for (let i = 0; i < responses.length; i += 1) { response = responses[i]; repeats = repeatsFor(response); for (let j = 0; j < repeats; j += 1) { result.push(response); } } return result; } function createResponse (responseConfig, stubIndexFn) { const cloned = helpers.clone(responseConfig || { is: {} }); cloned.stubIndex = stubIndexFn ? stubIndexFn : () => Promise.resolve(0); return cloned; } function wrap (stub = {}) { const cloned = helpers.clone(stub), statefulResponses = repeatTransform(cloned.responses || []); /** * Adds a new response to the stub (e.g. during proxying) * @memberOf module:models/inMemoryImpostersRepository# * @param {Object} response - the response to add * @returns {Object} - the promise */ cloned.addResponse = async response => { cloned.responses = cloned.responses || []; cloned.responses.push(response); statefulResponses.push(response); return response; }; /** * Selects the next response from the stub, including repeat behavior and circling back to the beginning * @memberOf module:models/inMemoryImpostersRepository# * @returns {Object} - the response * @returns {Object} - the promise */ cloned.nextResponse = async () => { const responseConfig = statefulResponses.shift(); if (responseConfig) { statefulResponses.push(responseConfig); return createResponse(responseConfig, cloned.stubIndex); } else { return createResponse(); } }; /** * Records a match for debugging purposes * @memberOf module:models/inMemoryImpostersRepository# * @param {Object} request - the request * @param {Object} response - the response * @param {Object} responseConfig - the config that generated the response * @param {Number} processingTime - the time to match the predicate and generate the full response * @returns {Object} - the promise */ cloned.recordMatch = async (request, response, responseConfig, processingTime) => { cloned.matches = cloned.matches || []; cloned.matches.push({ timestamp: new Date().toJSON(), request, response, responseConfig, processingTime }); }; return cloned; } /** * Creates the stubs repository for a single imposter * @returns {Object} */ function createStubsRepository () { const stubs = []; let requests = []; function reindex () { // stubIndex() is used to find the right spot to insert recorded // proxy responses. We reindex after every state change stubs.forEach((stub, index) => { stub.stubIndex = async () => index; }); } /** * Returns the first stub whose predicates match the filter, or a default one if none match * @memberOf module:models/inMemoryImpostersRepository# * @param {Function} filter - the filter function * @param {Number} startIndex - the index to to start searching * @returns {Object} */ async function first (filter, startIndex = 0) { for (let i = startIndex; i < stubs.length; i += 1) { if (filter(stubs[i].predicates || [])) { return { success: true, stub: stubs[i] }; } } return { success: false, stub: wrap() }; } /** * Adds a new stub * @memberOf module:models/inMemoryImpostersRepository# * @param {Object} stub - the stub to add * @returns {Object} - the promise */ async function add (stub) { stubs.push(wrap(stub)); reindex(); } /** * Inserts a new stub at the given index * @memberOf module:models/inMemoryImpostersRepository# * @param {Object} stub - the stub to insert * @param {Number} index - the index to add the stub at * @returns {Object} - the promise */ async function insertAtIndex (stub, index) { stubs.splice(index, 0, wrap(stub)); reindex(); } /** * Overwrites the list of stubs with a new list * @memberOf module:models/inMemoryImpostersRepository# * @param {Object} newStubs - the new list of stubs * @returns {Object} - the promise */ async function overwriteAll (newStubs) { while (stubs.length > 0) { stubs.pop(); } newStubs.forEach(stub => add(stub)); reindex(); } /** * Overwrites the stub at the given index with the new stub * @memberOf module:models/inMemoryImpostersRepository# * @param {Object} newStub - the new stub * @param {Number} index - the index of the old stuib * @returns {Object} - the promise */ async function overwriteAtIndex (newStub, index) { if (typeof stubs[index] === 'undefined') { throw errors.MissingResourceError(`no stub at index ${index}`); } stubs[index] = wrap(newStub); reindex(); } /** * Deletes the stub at the given index * @memberOf module:models/inMemoryImpostersRepository# * @param {Number} index - the index of the stub to delete * @returns {Object} - the promise */ async function deleteAtIndex (index) { if (typeof stubs[index] === 'undefined') { throw errors.MissingResourceError(`no stub at index ${index}`); } stubs.splice(index, 1); reindex(); } /** * Returns a JSON-convertible representation * @memberOf module:models/inMemoryImpostersRepository# * @param {Object} options - The formatting options * @param {Boolean} options.debug - If true, includes debug information * @returns {Object} - the promise resolving to the JSON object */ async function toJSON (options = {}) { const cloned = helpers.clone(stubs); cloned.forEach(stub => { if (!options.debug) { delete stub.matches; } }); return cloned; } function isRecordedResponse (response) { return response.is && typeof response.is._proxyResponseTime === 'number'; } /** * Removes the saved proxy responses * @memberOf module:models/inMemoryImpostersRepository# * @returns {Object} - Promise */ async function deleteSavedProxyResponses () { const allStubs = await toJSON(); allStubs.forEach(stub => { stub.responses = stub.responses.filter(response => !isRecordedResponse(response)); }); const nonProxyStubs = allStubs.filter(stub => stub.responses.length > 0); await overwriteAll(nonProxyStubs); } /** * Adds a request for the imposter * @memberOf module:models/inMemoryImpostersRepository# * @param {Object} request - the request * @returns {Object} - the promise */ async function addRequest (request) { const recordedRequest = helpers.clone(request); recordedRequest.timestamp = new Date().toJSON(); requests.push(recordedRequest); } /** * Returns the saved requests for the imposter * @memberOf module:models/inMemoryImpostersRepository# * @returns {Object} - the promise resolving to the array of requests */ async function loadRequests () { return requests; } /** * Clears the saved requests list * @memberOf module:models/inMemoryImpostersRepository# * @param {Object} request - the request * @returns {Object} - Promise */ async function deleteSavedRequests () { requests = []; } return { count: () => stubs.length, first, add, insertAtIndex, overwriteAll, overwriteAtIndex, deleteAtIndex, toJSON, deleteSavedProxyResponses, addRequest, loadRequests, deleteSavedRequests }; } /** * Creates the repository * @returns {Object} */ function create () { const imposters = {}, stubRepos = {}; /** * Adds a new imposter * @memberOf module:models/inMemoryImpostersRepository# * @param {Object} imposter - the imposter to add * @returns {Object} - the promise */ async function add (imposter) { if (!imposter.stubs) { imposter.stubs = []; } imposters[String(imposter.port)] = imposter; const promises = (imposter.creationRequest.stubs || []).map(stubsFor(imposter.port).add); await Promise.all(promises); return imposter; } /** * Gets the imposter by id * @memberOf module:models/inMemoryImpostersRepository# * @param {Number} id - the id of the imposter (e.g. the port) * @returns {Object} - the imposter */ async function get (id) { return imposters[String(id)] || null; } /** * Gets all imposters * @memberOf module:models/inMemoryImpostersRepository# * @returns {Object} - all imposters keyed by port */ async function all () { return Promise.all(Object.keys(imposters).map(get)); } /** * Returns whether an imposter at the given id exists or not * @memberOf module:models/inMemoryImpostersRepository# * @param {Number} id - the id (e.g. the port) * @returns {boolean} */ async function exists (id) { return typeof imposters[String(id)] !== 'undefined'; } /** * Deletes the imposter at the given id * @memberOf module:models/inMemoryImpostersRepository# * @param {Number} id - the id (e.g. the port) * @returns {Object} - the deletion promise */ async function del (id) { const result = imposters[String(id)] || null; delete imposters[String(id)]; delete stubRepos[String(id)]; if (result) { await result.stop(); } return result; } /** * Deletes all imposters synchronously; used during shutdown * @memberOf module:models/inMemoryImpostersRepository# */ function stopAllSync () { Object.keys(imposters).forEach(id => { imposters[id].stop(); delete imposters[id]; delete stubRepos[id]; }); } /** * Deletes all imposters * @memberOf module:models/inMemoryImpostersRepository# * @returns {Object} - the deletion promise */ async function deleteAll () { const ids = Object.keys(imposters), promises = ids.map(id => imposters[id].stop()); ids.forEach(id => { delete imposters[id]; delete stubRepos[id]; }); await Promise.all(promises); } /** * Returns the stub repository for the given id * @memberOf module:models/inMemoryImpostersRepository# * @param {Number} id - the imposter's id * @returns {Object} - the stub repository */ function stubsFor (id) { // In practice, the stubsFor call occurs before the imposter is actually added... if (!stubRepos[String(id)]) { stubRepos[String(id)] = createStubsRepository(); } return stubRepos[String(id)]; } /** * Called at startup to load saved imposters. * Does nothing for in memory repository * @memberOf module:models/inMemoryImpostersRepository# * @returns {Object} - a promise */ async function loadAll () { return Promise.resolve(); } return { add, get, all, exists, del, stopAllSync, deleteAll, stubsFor, createStubsRepository, loadAll }; } module.exports = { create };