Best JavaScript code snippet using mountebank
filesystemBackedImpostersRepository.js
Source:filesystemBackedImpostersRepository.js
1'use strict';2/**3 * An abstraction for loading imposters from the filesystem4 * The root of the database is provided on the command line as --datadir5 * The file layout looks like this:6 *7 * /{datadir}8 * /30009 * /imposter.json10 * {11 * "protocol": "http",12 * "port": 3000,13 * "stubs: [{14 * "predicates": [{ "equals": { "path": "/" } }],15 * "meta": {16 * "dir": "stubs/{epoch-pid-counter}"17 * }18 * }]19 * }20 *21 * /stubs22 * /{epoch-pid-counter}23 * /meta.json24 * {25 * "responseFiles": ["responses/{epoch-pid-counter}.json"],26 * // An array of indexes into responseFiles which handle repeat behavior27 * "orderWithRepeats": [0],28 * // The next index into orderWithRepeats; incremented with each call to nextResponse()29 * "nextIndex": 030 * }31 *32 * /responses33 * /{epoch-pid-counter}.json34 * {35 * "is": { "body": "Hello, world!" }36 * }37 *38 * /matches39 * /{epoch-pid-counter}.json40 * { ... }41 *42 * /requests43 * /{epoch-pid-counter}.json44 * { ... }45 *46 * This structure is designed to minimize the amount of file locking and to maximize parallelism and throughput.47 *48 * The imposters.json file needs to be locked during imposter-level activities (e.g. adding a stub).49 * Readers do not lock; they just get the data at the time they read it. Since this file doesn't50 * contain transient state, there's no harm from a stale read. Writes (which happen for51 * stub changing operations, either for proxy response recording or stub API calls) grab a file lock52 * for both the read and the write. Writes to this file should be infrequent, except perhaps during53 * proxy recording. Newly added stubs may change the index of existing stubs in the stubs array, but54 * will never change the stub meta.dir, so it is always safe to hang on to it for subsequent operations.55 *56 * The stub meta.json needs to be locked to add responses or trigger the next response, but is57 * separated from the imposter.json so we can have responses from multiple stubs in parallel with no58 * lock conflict. Again, readers (e.g. to generate imposter JSON) do not need a lock because the responseFiles59 * array is mostly read-only, and even when it's not (adding responses during proxyAlways recording), there's60 * no harm from a stale read. Writers (usually generating the next response for a stub) grab a lock for the61 * read and the write. This should be the most common write across files, which is why the meta.json file62 * is small.63 *64 * In both cases where a file needs to be locked, an exponential backoff retry strategy is used. Inconsistent65 * reads of partially written files (which can happen by default with the system calls - fs.writeFile is not66 * atomic) are avoided by writing first to a temp file (during which time reads can happen to the original file)67 * and then renaming to the original file.68 *69 * New directories and filenames use a timestamp-based filename to allow creating them without synchronizing70 * with a read. Since multiple files (esp. requests) can be added during the same millisecond, a pid and counter71 * is tacked on to the filename to improve uniqueness. It doesn't provide the * ironclad guarantee a GUID72 * does -- two different processes on two different machines could in theory have the same pid and create files73 * during the same timestamp with the same counter, but the odds of that happening * are so small that it's not74 * worth giving up the easy time-based sortability based on filenames alone.75 *76 * Keeping all imposter information under a directory (instead of having metadata outside the directory)77 * allows us to remove the imposter by simply removing the directory.78 *79 * There are some extra checks on filesystem operations (atomicWriteFile) due to antivirus software, solar flares,80 * gremlins, etc. graceful-fs solves some of these, but apparently not all.81 *82 * @module83 */84const prometheus = require('prom-client'),85 metrics = {86 lockAcquireDuration: new prometheus.Histogram({87 name: 'mb_lock_acquire_duration_seconds',88 help: 'Time it takes to acquire a file lock',89 buckets: [0.1, 0.2, 0.5, 1, 3, 5, 10, 30],90 labelNames: ['caller']91 }),92 lockHoldDuration: new prometheus.Histogram({93 name: 'mb_lock_hold_duration_seconds',94 help: 'Time a file lock is held',95 buckets: [0.1, 0.2, 0.5, 1, 2],96 labelNames: ['caller']97 }),98 lockErrors: new prometheus.Counter({99 name: 'mb_lock_errors_total',100 help: 'Number of lock errors',101 labelNames: ['caller', 'code']102 })103 };104/**105 * Creates the repository106 * @param {Object} config - The database configuration107 * @param {String} config.datadir - The database directory108 * @param {Object} logger - The logger109 * @returns {Object}110 */111function create (config, logger) {112 let counter = 0,113 locks = 0;114 const Q = require('q'),115 imposterFns = {};116 function prettyError (err, filepath) {117 const errors = require('../util/errors');118 // I started getting EROFS after upgrading to OSX Catalina 10.15.6119 if (err.code === 'EACCES' || err.code === 'EROFS') {120 return errors.InsufficientAccessError({ path: filepath });121 }122 else {123 return err;124 }125 }126 function writeFile (filepath, obj) {127 const fs = require('fs-extra'),128 path = require('path'),129 dir = path.dirname(filepath),130 deferred = Q.defer();131 fs.ensureDir(dir, mkdirErr => {132 if (mkdirErr) {133 deferred.reject(prettyError(mkdirErr, dir));134 }135 else {136 fs.writeFile(filepath, JSON.stringify(obj, null, 2), err => {137 if (err) {138 deferred.reject(prettyError(err, filepath));139 }140 else {141 deferred.resolve(filepath);142 }143 });144 }145 });146 return deferred.promise;147 }148 function readFile (filepath, defaultContents) {149 const deferred = Q.defer(),150 fs = require('fs-extra'),151 errors = require('../util/errors');152 fs.readFile(filepath, 'utf8', (err, data) => {153 if (err && err.code === 'ENOENT') {154 if (typeof defaultContents === 'undefined') {155 logger.error(`Corrupted database: missing file ${filepath}`);156 deferred.reject(errors.DatabaseError('file not found', { details: err.message }));157 }158 else {159 deferred.resolve(defaultContents);160 }161 }162 else if (err) {163 deferred.reject(prettyError(err, filepath));164 }165 else {166 try {167 deferred.resolve(JSON.parse(data));168 }169 catch (parseErr) {170 logger.error(`Corrupted database: invalid JSON for ${filepath}`);171 deferred.reject(errors.DatabaseError(`invalid JSON in ${filepath}`, { details: parseErr.message }));172 }173 }174 });175 return deferred.promise;176 }177 function rename (oldPath, newPath) {178 const deferred = Q.defer(),179 fs = require('fs-extra');180 fs.rename(oldPath, newPath, err => {181 if (err) {182 deferred.reject(prettyError(err, oldPath));183 }184 else {185 deferred.resolve(newPath);186 }187 });188 return deferred.promise;189 }190 function ensureDir (filepath) {191 const fs = require('fs-extra'),192 path = require('path'),193 dir = path.dirname(filepath),194 deferred = Q.defer();195 fs.ensureDir(dir, err => {196 if (err) {197 deferred.reject(prettyError(err, dir));198 }199 else {200 deferred.resolve(dir);201 }202 });203 return deferred.promise;204 }205 function atomicWriteFile (filepath, obj, attempts = 1) {206 const tmpfile = filepath + '.tmp';207 return writeFile(tmpfile, obj)208 .then(() => rename(tmpfile, filepath))209 .catch(err => {210 if (err.code === 'ENOENT' && attempts < 15) {211 logger.debug(`Attempt ${attempts} failed with ENOENT error on atomic write of ${filepath}. Retrying...`);212 return Q.delay(10).then(() => atomicWriteFile(filepath, obj, attempts + 1));213 }214 else {215 return Q.reject(err);216 }217 });218 }219 function lockFile (filepath) {220 const locker = require('proper-lockfile'),221 options = {222 realpath: false,223 retries: {224 retries: 20,225 minTimeout: 10,226 maxTimeout: 5000,227 randomize: true,228 factor: 1.5229 },230 stale: 30000231 };232 // with realpath = false, the file doesn't have to exist, but the directory does233 return ensureDir(filepath)234 .then(() => locker.lock(filepath, options));235 }236 function readAndWriteFile (filepath, caller, transformer, defaultContents) {237 const locker = require('proper-lockfile'),238 currentLockId = locks,239 observeLockAcquireDuration = metrics.lockAcquireDuration.startTimer({ caller });240 locks += 1;241 return lockFile(filepath)242 .then(release => {243 const acquireLockSeconds = observeLockAcquireDuration(),244 observeLockHoldDuration = metrics.lockHoldDuration.startTimer({ caller });245 logger.debug(`Acquired file lock on ${filepath} for ${caller}-${currentLockId} after ${acquireLockSeconds}s`);246 return readFile(filepath, defaultContents)247 .then(original => transformer(original))248 .then(transformed => atomicWriteFile(filepath, transformed))249 .then(() => {250 const lockHeld = observeLockHoldDuration();251 logger.debug(`Released file lock on ${filepath} for ${caller}-${currentLockId} after ${lockHeld}s`);252 return release()253 .catch(err => {254 // Ignore lock already released errors255 if (err.code !== 'ERELEASED') {256 throw err;257 }258 });259 });260 })261 .catch(err => {262 metrics.lockErrors.inc({ caller, code: err.code });263 locker.unlock(filepath, { realpath: false }).catch(() => {});264 return Q.reject(err);265 });266 }267 function remove (path) {268 const deferred = Q.defer(),269 fs = require('fs-extra');270 fs.remove(path, err => {271 if (err) {272 deferred.reject(err);273 }274 else {275 deferred.resolve({});276 }277 });278 return deferred.promise;279 }280 function filenameFor (timestamp) {281 const epoch = timestamp.valueOf();282 counter += 1;283 return `${epoch}-${process.pid}-${counter}`;284 }285 function partsFrom (filename) {286 // format {epoch}-{pid}-{counter}287 const pattern = /^(\d+)-(\d+)-(\d+)\.json$/,288 parts = pattern.exec(filename);289 return {290 epoch: Number(parts[1]),291 pid: Number(parts[2]),292 counter: Number(parts[3])293 };294 }295 function timeSorter (first, second) {296 // format {epoch}-{pid}-{counter}297 // sort by epoch first, then pid, then counter to guarantee determinism for298 // files added during the same millisecond.299 const firstParts = partsFrom(first),300 secondParts = partsFrom(second);301 let result = firstParts.epoch - secondParts.epoch;302 if (result === 0) {303 result = firstParts.pid - secondParts.pid;304 }305 if (result === 0) {306 result = firstParts.counter - secondParts.counter;307 }308 return result;309 }310 function readdir (path) {311 const deferred = Q.defer(),312 fs = require('fs-extra');313 fs.readdir(path, (err, files) => {314 if (err && err.code === 'ENOENT') {315 // Nothing saved yet316 deferred.resolve([]);317 }318 else if (err) {319 deferred.reject(err);320 }321 else {322 deferred.resolve(files);323 }324 });325 return deferred.promise;326 }327 function loadAllInDir (path) {328 return readdir(path).then(files => {329 const promises = files330 .filter(file => file.indexOf('.json') > 0)331 .sort(timeSorter)332 .map(file => readFile(`${path}/${file}`));333 return Q.all(promises);334 });335 }336 function repeatsFor (response) {337 return response.repeat || 1;338 }339 function saveStubMetaAndResponses (stub, baseDir) {340 const stubDefinition = {341 meta: { dir: `stubs/${filenameFor(new Date())}` }342 },343 meta = {344 responseFiles: [],345 orderWithRepeats: [],346 nextIndex: 0347 },348 responses = stub.responses || [],349 promises = [];350 if (stub.predicates) {351 stubDefinition.predicates = stub.predicates;352 }353 for (let i = 0; i < responses.length; i += 1) {354 const responseFile = `responses/${filenameFor(new Date())}.json`;355 meta.responseFiles.push(responseFile);356 for (let repeats = 0; repeats < repeatsFor(responses[i]); repeats += 1) {357 meta.orderWithRepeats.push(i);358 }359 promises.push(writeFile(`${baseDir}/${stubDefinition.meta.dir}/${responseFile}`, responses[i]));360 }361 promises.push(writeFile(`${baseDir}/${stubDefinition.meta.dir}/meta.json`, meta));362 return Q.all(promises).then(() => stubDefinition);363 }364 function stubRepository (baseDir) {365 const imposterFile = `${baseDir}/imposter.json`;366 function metaPath (stubDir) {367 return `${baseDir}/${stubDir}/meta.json`;368 }369 function responsePath (stubDir, responseFile) {370 return `${baseDir}/${stubDir}/${responseFile}`;371 }372 function requestPath (request) {373 return `${baseDir}/requests/${filenameFor(Date.parse(request.timestamp))}.json`;374 }375 function matchPath (stubDir, match) {376 return `${baseDir}/${stubDir}/matches/${filenameFor(Date.parse(match.timestamp))}.json`;377 }378 function readHeader () {379 return readFile(imposterFile, { stubs: [] });380 }381 function readAndWriteHeader (caller, transformer) {382 return readAndWriteFile(imposterFile, caller, transformer, { stubs: [] });383 }384 function wrap (stub) {385 const helpers = require('../util/helpers'),386 cloned = helpers.clone(stub || {}),387 stubDir = stub ? stub.meta.dir : '';388 if (typeof stub === 'undefined') {389 return {390 addResponse: () => Q(),391 nextResponse: () => Q({392 is: {},393 stubIndex: () => Q(0)394 }),395 recordMatch: () => Q()396 };397 }398 delete cloned.meta;399 /**400 * Adds a response to the stub401 * @memberOf module:models/filesystemBackedImpostersRepository#402 * @param {Object} response - the new response403 * @returns {Object} - the promise404 */405 cloned.addResponse = response => {406 let responseFile;407 return readAndWriteFile(metaPath(stubDir), 'addResponse', meta => {408 const responseIndex = meta.responseFiles.length;409 responseFile = `responses/${filenameFor(new Date())}.json`;410 meta.responseFiles.push(responseFile);411 for (let repeats = 0; repeats < repeatsFor(response); repeats += 1) {412 meta.orderWithRepeats.push(responseIndex);413 }414 return writeFile(responsePath(stubDir, responseFile), response).then(() => meta);415 });416 };417 function stubIndex () {418 return readHeader().then(header => {419 for (let i = 0; i < header.stubs.length; i += 1) {420 if (header.stubs[i].meta.dir === stubDir) {421 return i;422 }423 }424 return 0;425 });426 }427 function createResponse (responseConfig) {428 const result = helpers.clone(responseConfig || { is: {} });429 result.stubIndex = stubIndex;430 return result;431 }432 /**433 * Returns the next response for the stub, taking into consideration repeat behavior and cycling back the beginning434 * @memberOf module:models/filesystemBackedImpostersRepository#435 * @returns {Object} - the promise436 */437 cloned.nextResponse = () => {438 let responseFile;439 return readAndWriteFile(metaPath(stubDir), 'nextResponse', meta => {440 const maxIndex = meta.orderWithRepeats.length,441 responseIndex = meta.orderWithRepeats[meta.nextIndex % maxIndex];442 responseFile = meta.responseFiles[responseIndex];443 meta.nextIndex = (meta.nextIndex + 1) % maxIndex;444 return Q(meta);445 }).then(() => readFile(responsePath(stubDir, responseFile)))446 .then(responseConfig => createResponse(responseConfig));447 };448 /**449 * Records a match for debugging purposes450 * @memberOf module:models/filesystemBackedImpostersRepository#451 * @param {Object} request - the request452 * @param {Object} response - the response453 * @param {Object} responseConfig - the config that generated the response454 * @param {Number} processingTime - the time to match the predicate and generate the full response455 * @returns {Object} - the promise456 */457 cloned.recordMatch = (request, response, responseConfig, processingTime) => {458 const match = {459 timestamp: new Date().toJSON(),460 request,461 response,462 responseConfig,463 processingTime464 };465 return writeFile(matchPath(stubDir, match), match);466 };467 return cloned;468 }469 /**470 * Returns the number of stubs for the imposter471 * @memberOf module:models/filesystemBackedImpostersRepository#472 * @returns {Object} - the promise473 */474 function count () {475 return readHeader().then(imposter => imposter.stubs.length);476 }477 /**478 * Returns the first stub whose predicates matches the filter479 * @memberOf module:models/filesystemBackedImpostersRepository#480 * @param {Function} filter - the filter function481 * @param {Number} startIndex - the index to to start searching482 * @returns {Object} - the promise483 */484 function first (filter, startIndex = 0) {485 return readHeader().then(header => {486 for (let i = startIndex; i < header.stubs.length; i += 1) {487 if (filter(header.stubs[i].predicates || [])) {488 return { success: true, stub: wrap(header.stubs[i]) };489 }490 }491 return { success: false, stub: wrap() };492 });493 }494 /**495 * Adds a new stub to imposter496 * @memberOf module:models/filesystemBackedImpostersRepository#497 * @param {Object} stub - the stub to add498 * @returns {Object} - the promise499 */500 function add (stub) { // eslint-disable-line no-shadow501 return saveStubMetaAndResponses(stub, baseDir).then(stubDefinition => {502 return readAndWriteHeader('addStub', header => {503 header.stubs.push(stubDefinition);504 return header;505 });506 });507 }508 /**509 * Inserts a new stub at the given index510 * @memberOf module:models/filesystemBackedImpostersRepository#511 * @param {Object} stub - the stub to add512 * @param {Number} index - the index to insert the new stub at513 * @returns {Object} - the promise514 */515 function insertAtIndex (stub, index) {516 return saveStubMetaAndResponses(stub, baseDir).then(stubDefinition => {517 return readAndWriteHeader('insertStubAtIndex', header => {518 header.stubs.splice(index, 0, stubDefinition);519 return header;520 });521 });522 }523 /**524 * Deletes the stub at the given index525 * @memberOf module:models/filesystemBackedImpostersRepository#526 * @param {Number} index - the index of the stub to delete527 * @returns {Object} - the promise528 */529 function deleteAtIndex (index) {530 let stubDir;531 return readAndWriteHeader('deleteStubAtIndex', header => {532 const errors = require('../util/errors');533 if (typeof header.stubs[index] === 'undefined') {534 return Q.reject(errors.MissingResourceError(`no stub at index ${index}`));535 }536 stubDir = header.stubs[index].meta.dir;537 header.stubs.splice(index, 1);538 return header;539 }).then(() => remove(`${baseDir}/${stubDir}`));540 }541 /**542 * Overwrites all stubs with a new list543 * @memberOf module:models/filesystemBackedImpostersRepository#544 * @param {Object} newStubs - the new list of stubs545 * @returns {Object} - the promise546 */547 function overwriteAll (newStubs) {548 return readAndWriteHeader('overwriteAllStubs', header => {549 header.stubs = [];550 return remove(`${baseDir}/stubs`).then(() => header);551 }).then(() => {552 let addSequence = Q(true);553 newStubs.forEach(stub => {554 addSequence = addSequence.then(() => add(stub));555 });556 return addSequence;557 });558 }559 /**560 * Overwrites the stub at the given index561 * @memberOf module:models/filesystemBackedImpostersRepository#562 * @param {Object} stub - the new stub563 * @param {Number} index - the index of the stub to overwrite564 * @returns {Object} - the promise565 */566 function overwriteAtIndex (stub, index) {567 return deleteAtIndex(index).then(() => insertAtIndex(stub, index));568 }569 function loadResponses (stub) {570 return readFile(metaPath(stub.meta.dir))571 .then(meta => Q.all(meta.responseFiles.map(responseFile =>572 readFile(responsePath(stub.meta.dir, responseFile)))));573 }574 function loadMatches (stub) {575 return loadAllInDir(`${baseDir}/${stub.meta.dir}/matches`);576 }577 /**578 * Returns a JSON-convertible representation579 * @memberOf module:models/filesystemBackedImpostersRepository#580 * @param {Object} options - The formatting options581 * @param {Boolean} options.debug - If true, includes debug information582 * @returns {Object} - the promise resolving to the JSON object583 */584 function toJSON (options = {}) {585 return readHeader().then(header => {586 const responsePromises = header.stubs.map(loadResponses),587 debugPromises = options.debug ? header.stubs.map(loadMatches) : [];588 return Q.all(responsePromises).then(stubResponses => {589 return Q.all(debugPromises).then(matches => {590 header.stubs.forEach((stub, index) => {591 stub.responses = stubResponses[index];592 if (options.debug && matches[index].length > 0) {593 stub.matches = matches[index];594 }595 delete stub.meta;596 });597 return header.stubs;598 });599 });600 });601 }602 function isRecordedResponse (response) {603 return response.is && typeof response.is._proxyResponseTime === 'number';604 }605 /**606 * Removes the saved proxy responses607 * @memberOf module:models/filesystemBackedImpostersRepository#608 * @returns {Object} - Promise609 */610 function deleteSavedProxyResponses () {611 return toJSON().then(allStubs => {612 allStubs.forEach(stub => {613 stub.responses = stub.responses.filter(response => !isRecordedResponse(response));614 });615 allStubs = allStubs.filter(stub => stub.responses.length > 0);616 return overwriteAll(allStubs);617 });618 }619 /**620 * Adds a request for the imposter621 * @memberOf module:models/filesystemBackedImpostersRepository#622 * @param {Object} request - the request623 * @returns {Object} - the promise624 */625 function addRequest (request) {626 const helpers = require('../util/helpers');627 const recordedRequest = helpers.clone(request);628 recordedRequest.timestamp = new Date().toJSON();629 return writeFile(requestPath(recordedRequest), recordedRequest);630 }631 /**632 * Returns the saved requests for the imposter633 * @memberOf module:models/filesystemBackedImpostersRepository#634 * @returns {Object} - the promise resolving to the array of requests635 */636 function loadRequests () {637 return loadAllInDir(`${baseDir}/requests`);638 }639 /**640 * Deletes the requests directory for an imposter641 * @memberOf module:models/filesystemBackedImpostersRepository#642 * @returns {Object} - Promise643 */644 function deleteSavedRequests () {645 return remove(`${baseDir}/requests`);646 }647 return {648 count,649 first,650 add,651 insertAtIndex,652 overwriteAll,653 overwriteAtIndex,654 deleteAtIndex,655 toJSON,656 deleteSavedProxyResponses,657 addRequest,658 loadRequests,659 deleteSavedRequests660 };661 }662 function imposterDir (id) {663 return `${config.datadir}/${id}`;664 }665 function headerFile (id) {666 return `${imposterDir(id)}/imposter.json`;667 }668 /**669 * Returns the stubs repository for the imposter670 * @memberOf module:models/filesystemBackedImpostersRepository#671 * @param {Number} id - the id of the imposter672 * @returns {Object} - the stubs repository673 */674 function stubsFor (id) {675 return stubRepository(imposterDir(id));676 }677 /**678 * Saves a reference to the imposter so that the functions679 * (which can't be persisted) can be rehydrated to a loaded imposter.680 * This means that any data in the function closures will be held in681 * memory.682 * @memberOf module:models/filesystemBackedImpostersRepository#683 * @param {Object} imposter - the imposter684 */685 function addReference (imposter) {686 const id = String(imposter.port);687 imposterFns[id] = {};688 Object.keys(imposter).forEach(key => {689 if (typeof imposter[key] === 'function') {690 imposterFns[id][key] = imposter[key];691 }692 });693 }694 function rehydrate (imposter) {695 const id = String(imposter.port);696 Object.keys(imposterFns[id]).forEach(key => {697 imposter[key] = imposterFns[id][key];698 });699 }700 /**701 * Adds a new imposter702 * @memberOf module:models/filesystemBackedImpostersRepository#703 * @param {Object} imposter - the imposter to add704 * @returns {Object} - the promise705 */706 function add (imposter) {707 const imposterConfig = imposter.creationRequest,708 stubs = imposterConfig.stubs || [],709 promises = stubs.map(stub => saveStubMetaAndResponses(stub, imposterDir(imposter.port)));710 delete imposterConfig.requests;711 return Q.all(promises).then(stubDefinitions => {712 imposterConfig.port = imposter.port;713 imposterConfig.stubs = stubDefinitions;714 return writeFile(headerFile(imposter.port), imposterConfig);715 }).then(() => {716 addReference(imposter);717 return imposter;718 });719 }720 /**721 * Gets the imposter by id722 * @memberOf module:models/filesystemBackedImpostersRepository#723 * @param {Number} id - the id of the imposter (e.g. the port)724 * @returns {Object} - the promise resolving to the imposter725 */726 function get (id) {727 return readFile(headerFile(id), null).then(header => {728 if (header === null) {729 return Q(null);730 }731 return stubsFor(id).toJSON().then(stubs => {732 header.stubs = stubs;733 rehydrate(header);734 return header;735 });736 });737 }738 /**739 * Gets all imposters740 * @memberOf module:models/filesystemBackedImpostersRepository#741 * @returns {Object} - all imposters keyed by port742 */743 function all () {744 return Q.all(Object.keys(imposterFns).map(get));745 }746 /**747 * Returns whether an imposter at the given id exists or not748 * @memberOf module:models/filesystemBackedImpostersRepository#749 * @param {Number} id - the id (e.g. the port)750 * @returns {boolean}751 */752 function exists (id) {753 return Q(Object.keys(imposterFns).indexOf(String(id)) >= 0);754 }755 function shutdown (id) {756 if (typeof imposterFns[String(id)] === 'undefined') {757 return Q();758 }759 const fn = imposterFns[String(id)].stop;760 delete imposterFns[String(id)];761 return fn ? fn() : Q();762 }763 /**764 * Deletes the imposter at the given id765 * @memberOf module:models/filesystemBackedImpostersRepository#766 * @param {Number} id - the id (e.g. the port)767 * @returns {Object} - the deletion promise768 */769 function del (id) {770 return get(id).then(imposter => {771 const promises = [shutdown(id)];772 if (imposter !== null) {773 promises.push(remove(imposterDir(id)));774 }775 return Q.all(promises).then(() => imposter);776 });777 }778 /**779 * Deletes all imposters synchronously; used during shutdown780 * @memberOf module:models/filesystemBackedImpostersRepository#781 */782 function stopAllSync () {783 Object.keys(imposterFns).forEach(shutdown);784 }785 /**786 * Deletes all imposters787 * @memberOf module:models/filesystemBackedImpostersRepository#788 * @returns {Object} - the deletion promise789 */790 function deleteAll () {791 const ids = Object.keys(imposterFns),792 dirs = ids.map(imposterDir),793 promises = ids.map(shutdown).concat(dirs.map(remove));794 // Remove only the directories for imposters we have a reference to795 return Q.all(promises)796 .then(() => readdir(config.datadir))797 .then(entries => {798 if (entries.length === 0) {799 return remove(config.datadir);800 }801 else {802 return Q();803 }804 });805 }806 /**807 * Loads all saved imposters at startup808 * @memberOf module:models/filesystemBackedImpostersRepository#809 * @param {Object} protocols - The protocol map, used to instantiate a new instance810 * @returns {Object} - a promise811 */812 function loadAll (protocols) {813 const fs = require('fs-extra');814 if (!config.datadir || !fs.existsSync(config.datadir)) {815 return Q();816 }817 const dirs = fs.readdirSync(config.datadir),818 promises = dirs.map(dir => {819 const imposterFilename = `${config.datadir}/${dir}/imposter.json`;820 if (!fs.existsSync(imposterFilename)) {821 logger.warn(`Skipping ${dir} during loading; missing imposter.json`);822 return Q();823 }824 const imposterConfig = JSON.parse(fs.readFileSync(imposterFilename)),825 protocol = protocols[imposterConfig.protocol];826 if (protocol) {827 logger.info(`Loading ${imposterConfig.protocol}:${dir} from datadir`);828 return protocol.createImposterFrom(imposterConfig)829 .then(imposter => addReference(imposter));830 }831 else {832 logger.error(`Cannot load imposter ${dir}; no protocol loaded for ${config.protocol}`);833 return Q();834 }835 });836 return Q.all(promises);837 }838 return {839 add,840 addReference,841 get,842 all,843 exists,844 del,845 stopAllSync,846 deleteAll,847 stubsFor,848 loadAll849 };850}...
mountebank-helper.js
Source:mountebank-helper.js
1const request = require('supertest')2let mountebankServer = null3function setUrl(url) {4 mountebankServer = request(url)5}6function createImposter(imposterConfig) {7 return mountebankServer8 .delete(`/imposters/${imposterConfig.port}`)9 .then(() => {10 return mountebankServer11 .post('/imposters')12 .set('Content-Type', 'application/json')13 .send(JSON.stringify(imposterConfig))14 })15}16function getImposterPort(imposterConfig) {17 return imposterConfig.port18}19function getRequests(port) {20 return mountebankServer.get(`/imposters/${port}`).then(response => {21 return {22 numberOfRequests: response.body.numberOfRequests,23 requests: response.body.requests24 }25 })26}27module.exports = {28 setUrl,29 createImposter,30 getImposterPort,31 getRequests...
Using AI Code Generation
1var mb = require('mountebank');2var imposter = mb.create({3 {4 {5 is: {6 headers: {7 },8 }9 }10 }11});12imposter.then(function (imposter) {13 console.log('imposter created');14});15var mb = require('mountebank');16var imposter = mb.create({17 {18 {19 is: {20 headers: {21 },22 }23 }24 }25});26imposter.then(function (imposter) {27 console.log('imposter created');28});29var frisby = require('frisby');30var mb = require('mountebank');31frisby.create('test imposter')32 .expectStatus(200)33 .expectHeader('Content-Type', 'text/html')34 .expectBodyContains('Hello World!')35 .afterJSON(function (json) {36 mb.stop(imposter);37 })38 .toss();39 ✓ should be OK (5ms)401 passing (17ms)41var frisby = require('frisby');42var mb = require('mountebank');43frisby.create('test imposter')44 .expectStatus(200)45 .expectHeader('Content-Type
Using AI Code Generation
1var imposterConfig = require('mountebank').imposterConfig;2var config = imposterConfig({3 stubs: [{4 predicates: [{5 equals: {6 }7 }],8 responses: [{9 is: {10 headers: {11 },12 }13 }]14 }]15});16config.create(function (error) {17 if (error) {18 console.error(error);19 }20 else {21 console.log('Imposter created');22 }23});24var imposterConfig = require('mountebank').imposterConfig;25var config = imposterConfig({26 stubs: [{27 predicates: [{28 equals: {29 }30 }],31 responses: [{32 is: {33 headers: {34 },35 }36 }]37 }]38});39config.create(function (error) {40 if (error) {41 console.error(error);42 }43 else {44 console.log('Imposter created');45 }46});47var config = imposterConfig({48 stubs: [{49 predicates: [{50 equals: {51 }52 }],53 responses: [{54 is: {55 headers: {56 },57 }58 }]59 }]60});61config.create(function (error) {62 if (error) {63 console.error(error);64 }65 else {66 console.log('Imposter created');67 }68});69var config = imposterConfig({70 stubs: [{71 predicates: [{72 equals: {73 }74 }],75 responses: [{76 is: {
Using AI Code Generation
1const mb = require('mountebank'),2 {3 {4 is: {5 headers: {6 },7 body: JSON.stringify({8 })9 }10 }11 }12 ];13mb.create(14 imposterConfig(protocol, port, stubs),15 function (error, imposter) {16 console.log(`Created imposter on port ${imposter.port}`);17 }18);19const mb = require('mountebank'),20 {21 {22 is: {23 headers: {24 },25 body: JSON.stringify({26 })27 }28 }29 }30 ];31mb.create(32 imposterConfig(protocol, port, stubs),33 function (error, imposter) {34 console.log(`Created imposter on port ${imposter.port}`);35 }36);37const mb = require('mountebank'),38 {39 {40 is: {41 headers: {42 },43 body: JSON.stringify({44 })45 }46 }47 }48 ];49mb.create(50 imposterConfig(protocol, port, stubs),51 function (error, imposter) {52 console.log(`Created imposter on port ${imposter.port}`);53 }54);55const mb = require('mountebank'),
Using AI Code Generation
1var imposterConfig = require('imposterConfig');2var options = {3 {4 { equals: { method: 'GET', path: '/test' } }5 { is: { statusCode: 200, body: 'Hello' } }6 }7};8imposterConfig(options, function (error, response) {9 if (error) {10 console.error(error);11 } else {12 console.log(response);13 }14});15var request = require('request');16module.exports = function (options, callback) {17 request({18 }, callback);19};20var imposterConfig = require('imposterConfig');21var options = {22 {23 { equals: { method: 'GET', path: '/test' } }24 { is: { statusCode: 200, body: 'Hello' } }25 }26};27imposterConfig(options, function (error, response) {28 if (error) {29 console.error(error);30 } else {31 console.log(response);32 }33});34var request = require('request');35module.exports = function (options, callback) {36 request({37 }, callback);38};39var imposterConfig = require('imposterConfig');40var options = {41 {42 { equals: { method: 'GET', path: '/test' } }43 { is: { statusCode: 200, body: 'Hello' } }
Using AI Code Generation
1var mb = require('mountebank');2var fs = require('fs');3var imposterConfig = {4 stubs: [{5 responses: [{6 is: {7 }8 }]9 }]10};11mb.create(imposterConfig)12 .then(function (imposter) {13 console.log(imposter.port);14 setTimeout(function () {15 imposter.stop();16 }, 10000);17 })18 .catch(function (error) {19 console.error(error);20 });21The imposter object returned by mb.create() has a few useful methods:22imposter.stop() - stops the imposter23imposter.get() - returns a promise for the current state of the imposter24imposter.addStub() - adds a new stub to the imposter25imposter.deleteStub() - deletes a stub from the imposter26imposter.addProxy() - adds a new proxy to the imposter27imposter.deleteProxy() - deletes a proxy from the imposter28imposter.addPredicate() - adds a new predicate to the imposter29imposter.deletePredicate() - deletes a predicate from the imposter30imposter.addResponse() - adds a new response to the imposter31imposter.deleteResponse() - deletes a response from the imposter32imposter.addBehavior() - adds a new behavior to the imposter33imposter.deleteBehavior() - deletes a behavior from the imposter34imposter.addDecorator() - adds a new decorator to the imposter35imposter.deleteDecorator() - deletes a decorator from the imposter36imposter.addRequest() - adds a new request to the imposter37imposter.deleteRequest() - deletes a request from the imposter38imposter.addResponse() - adds a new response to the imposter39imposter.deleteResponse() - deletes a response from the imposter40imposter.addResponse() - adds a new response to the imposter
Using AI Code Generation
1var http = require('http');2var fs = require('fs');3var path = require('path');4var imposterConfig = require('mountebank').imposterConfig;5var imposter = imposterConfig({6 stubs: [{7 responses: [{8 is: {9 }10 }]11 }]12});13imposter.create(function (error, result) {14 if (error) {15 console.error(error);16 process.exit(1);17 }18 console.log('Imposter created, with port ' + result.port);19 console.log('To stop the imposter, run "mb stop --port ' + result.port + '"');20 console.log('To view the imposter, run "mb view --port ' + result.port + '"');21 console.log('To delete the imposter, run "mb delete --port ' + result.port + '"');22 console.log('To test the imposter, run "node test.js"');23 var body = '';24 response.on('data', function (data) {25 body += data;26 });27 response.on('end', function () {28 console.log('Imposter responded with: ' + body);29 });30 });31});
Using AI Code Generation
1<% if (name) { %>2<% } else { %>3<% } %>4{5}6module.exports = {7};8export const port = 3000;9export const protocol = 'http';10export const name = 'Test';
Using AI Code Generation
1const mb = require('mountebank');2const config = require('./config.json');3const port = 4545;4mb.create({port: port, pidfile: 'mb.pid', logfile: 'mb.log'})5 .then(() => mb.imposterConfig({port: port, protocol: 'http', config: config}))6 .then(() => mb.imposterConfig({port: port, protocol: 'http', delete: true}))7 .then(() => mb.stop({pidfile: 'mb.pid'}))8 .catch((error) => {9 console.error(error);10 mb.stop({pidfile: 'mb.pid'});11 });12{13 {14 {15 "is": {16 "headers": {17 },18 }19 }20 }21}
Using AI Code Generation
1const mb = require('mountebank');2mb.create({ port: 2525, protocol: 'http' }).then(function (imposter) {3 const stub = {4 {5 is: {6 headers: {7 },8 body: JSON.stringify({ message: 'Hello World!' })9 }10 }11 };12 imposter.addStub(stub).then(function () {13 return imposter.get('/', { json: true });14 }).then(function (response) {15 console.log(response.body);16 }).finally(function () {17 imposter.stop();18 });19});20const mb = require('mountebank');21mb.create({ port: 2525, protocol: 'http' }).then(function (imposter) {22 const stub = {23 {24 is: {25 headers: {26 },27 body: JSON.stringify({ message: 'Hello World!' })28 }29 }30 };31 imposter.addStub(stub).then(function () {32 return imposter.get('/', { json: true });33 }).then(function (response) {34 console.log(response.body);35 }).finally(function () {36 imposter.stop();37 });38});
Learn to execute automation testing from scratch with LambdaTest Learning Hub. Right from setting up the prerequisites to run your first automation test, to following best practices and diving deeper into advanced test scenarios. LambdaTest Learning Hubs compile a list of step-by-step guides to help you be proficient with different test automation frameworks i.e. Selenium, Cypress, TestNG etc.
You could also refer to video tutorials over LambdaTest YouTube channel to get step by step demonstration from industry experts.
Get 100 minutes of automation test minutes FREE!!