UNPKG

@luminati-io/mountebank

Version:

Over the wire test doubles

243 lines (212 loc) 9.27 kB
'use strict'; const responseResolver = require('./responseResolver'), childProcess = require('child_process'), fsExtra = require('fs-extra'), path = require('path'), errors = require('../util/errors.js'), Imposter = require('./imposter.js'), helpers = require('../util/helpers.js'), tcpServer = require('./tcp/tcpServer.js'), httpServer = require('./http/httpServer.js'), httpsServer = require('./https/httpsServer.js'), smtpServer = require('./smtp/smtpServer.js'); /** * Abstracts the protocol configuration between the built-in in-memory implementations and out of process * implementations * @module */ /** * Loads the imposter creation functions for all built in and custom protocols * @param builtInProtocols {Object} - the in-memory protocol implementations that ship with mountebank * @param customProtocols {Object} - custom out-of-process protocol implementations * @param options {Object} - command line configuration * @param isAllowedConnection {Function} - a function that determines whether the connection is allowed or not for security verification * @param mbLogger {Object} - the logger * @param impostersRepository {Object} - the imposters repository * @returns {Object} - a map of protocol name to creation functions */ // eslint-disable-next-line max-params function load (builtInProtocols, customProtocols, options, isAllowedConnection, mbLogger, impostersRepository) { function inProcessCreate (createProtocol) { return async (creationRequest, logger, responseFn) => { const server = await createProtocol(creationRequest, logger, responseFn), stubs = impostersRepository.stubsFor(server.port), resolver = responseResolver.create(stubs, server.proxy); return { port: server.port, metadata: server.metadata, stubs: stubs, resolver: resolver, close: server.close, encoding: server.encoding || 'utf8' }; }; } function outOfProcessCreate (protocolName, config) { function customFieldsFor (creationRequest) { const fields = {}, commonFields = ['protocol', 'port', 'name', 'recordRequests', 'stubs', 'defaultResponse']; Object.keys(creationRequest).forEach(key => { if (commonFields.indexOf(key) < 0) { fields[key] = creationRequest[key]; } }); return fields; } return (creationRequest, logger) => new Promise((res, rej) => { let isPending = true; const { spawn } = childProcess, command = config.createCommand.split(' ')[0], args = config.createCommand.split(' ').splice(1), port = creationRequest.port, commonArgs = { port, callbackURLTemplate: options.callbackURLTemplate, loglevel: options.loglevel, allowInjection: options.allowInjection }, configArgs = helpers.merge(commonArgs, customFieldsFor(creationRequest)), resolve = obj => { isPending = false; res(obj); }, reject = err => { isPending = false; rej(err); }; if (typeof creationRequest.defaultResponse !== 'undefined') { configArgs.defaultResponse = creationRequest.defaultResponse; } const allArgs = args.concat(JSON.stringify(configArgs)), imposterProcess = spawn(command, allArgs); let closeCalled = false; imposterProcess.on('error', error => { const message = `Invalid configuration for protocol "${protocolName}": cannot run "${config.createCommand}"`; reject(errors.ProtocolError(message, { source: config.createCommand, details: error })); }); imposterProcess.once('exit', code => { if (code !== 0 && isPending) { const message = `"${protocolName}" start command failed (exit code ${code})`; reject(errors.ProtocolError(message, { source: config.createCommand })); } else if (!closeCalled) { logger.error("Uh oh! I've crashed! Expect subsequent requests to fail."); } }); function resolveWithMetadata (possibleJSON) { let metadata = {}; try { metadata = JSON.parse(possibleJSON); } catch (error) { /* do nothing */ } let serverPort = creationRequest.port; if (metadata.port) { serverPort = metadata.port; delete metadata.port; } const callbackURL = options.callbackURLTemplate.replace(':port', serverPort), encoding = metadata.encoding || 'utf8', stubs = impostersRepository.stubsFor(serverPort), resolver = responseResolver.create(stubs, undefined, callbackURL); delete metadata.encoding; resolve({ port: serverPort, metadata: metadata, stubs, resolver, encoding, close: callback => { closeCalled = true; imposterProcess.once('exit', callback); imposterProcess.kill(); } }); } function log (message) { if (message.indexOf(' ') > 0) { const words = message.split(' '), level = words[0], rest = words.splice(1).join(' ').trim(); if (['debug', 'info', 'warn', 'error'].indexOf(level) >= 0) { logger[level](rest); } } } imposterProcess.stdout.on('data', data => { const lines = data.toString('utf8').trim().split(/\r?\n/); lines.forEach(line => { if (isPending) { resolveWithMetadata(line); } log(line); }); }); imposterProcess.stderr.on('data', logger.error); }); } function createImposter (Protocol, creationRequest) { return Imposter.create(Protocol, creationRequest, mbLogger.baseLogger, options, isAllowedConnection); } const result = {}; Object.keys(builtInProtocols).forEach(key => { result[key] = builtInProtocols[key]; result[key].createServer = inProcessCreate(result[key].create); result[key].createImposterFrom = creationRequest => createImposter(result[key], creationRequest); }); Object.keys(customProtocols).forEach(key => { result[key] = customProtocols[key]; result[key].createServer = outOfProcessCreate(key, result[key]); result[key].createImposterFrom = creationRequest => createImposter(result[key], creationRequest); }); return result; } function isBuiltInProtocol (protocol) { return ['tcp', 'smtp', 'http', 'https'].indexOf(protocol) >= 0; } function loadCustomProtocols (protofile, logger) { if (typeof protofile === 'undefined') { return {}; } const filename = path.resolve(path.relative(process.cwd(), protofile)); if (fsExtra.existsSync(filename)) { try { const customProtocols = require(filename); Object.keys(customProtocols).forEach(proto => { if (isBuiltInProtocol(proto)) { logger.warn(`Using custom ${proto} implementation instead of the built-in one`); } else { logger.info(`Loaded custom protocol ${proto}`); } }); return customProtocols; } catch (e) { logger.error(`${protofile} contains invalid JSON -- no custom protocols loaded`); return {}; } } else { return {}; } } function loadProtocols (options, baseURL, logger, isAllowedConnection, imposters) { const builtInProtocols = { tcp: tcpServer, http: httpServer, https: httpsServer, smtp: smtpServer }, customProtocols = loadCustomProtocols(options.protofile, logger), config = { callbackURLTemplate: `${baseURL}/imposters/:port/_requests`, recordRequests: options.mock, recordMatches: options.debug, loglevel: options.log.level, allowInjection: options.allowInjection, host: options.host }; return load(builtInProtocols, customProtocols, config, isAllowedConnection, logger, imposters); } module.exports = { load, loadProtocols };