mountebank-test
Version:
Over the wire test doubles
184 lines (157 loc) • 7.05 kB
JavaScript
;
var utils = require('util'),
Q = require('q'),
exceptions = require('../util/errors'),
helpers = require('../util/helpers'),
combinators = require('../util/combinators'),
ResponseResolver = require('./responseResolver');
function create (options) {
function stubForResponse (originalStub, response, withPredicates) {
// Each dry run only validates the first response, so we
// explode the number of stubs to dry run each response separately
var clonedStub = helpers.clone(originalStub),
clonedResponse = helpers.clone(response);
clonedStub.responses = [clonedResponse];
// If the predicates don't match the test request, we won't dry run
// the response (although the predicates will be dry run). We remove
// the predicates to account for this scenario.
if (!withPredicates) {
delete clonedStub.predicates;
}
// we've already validated waits and don't want to add latency to validation
if (clonedResponse._behaviors && clonedResponse._behaviors.wait) {
delete clonedResponse._behaviors.wait;
}
return clonedStub;
}
function dryRun (stub, encoding, logger) {
// Need a well-formed proxy response in case a behavior decorator expects certain fields to exist
var dryRunProxy = { to: function () { return Q(options.testProxyResponse); } },
dryRunLogger = {
debug: combinators.noop,
info: combinators.noop,
warn: combinators.noop,
error: logger.error
},
resolver = ResponseResolver.create(dryRunProxy, combinators.identity),
stubsToValidateWithPredicates = stub.responses.map(function (response) {
return stubForResponse(stub, response, true);
}),
stubsToValidateWithoutPredicates = stub.responses.map(function (response) {
return stubForResponse(stub, response, false);
}),
stubsToValidate = stubsToValidateWithPredicates.concat(stubsToValidateWithoutPredicates),
dryRunRepositories = stubsToValidate.map(function (stubToValidate) {
var stubRepository = options.StubRepository.create(resolver, false, encoding);
stubRepository.addStub(stubToValidate);
return stubRepository;
});
return Q.all(dryRunRepositories.map(function (stubRepository) {
var testRequest = options.testRequest;
testRequest.isDryRun = true;
return stubRepository.resolve(testRequest, dryRunLogger);
}));
}
function addDryRunErrors (stub, encoding, errors, logger) {
var deferred = Q.defer();
try {
dryRun(stub, encoding, logger).done(deferred.resolve, function (reason) {
reason.source = reason.source || JSON.stringify(stub);
errors.push(reason);
deferred.resolve();
});
}
catch (error) {
errors.push(exceptions.ValidationError('malformed stub request', {
data: error.message,
source: error.source || stub
}));
deferred.resolve();
}
return deferred.promise;
}
function addInvalidWaitErrors (stub, errors) {
var hasInvalidWait = stub.responses.some(function (response) {
return response._behaviors && response._behaviors.wait &&
(typeof response._behaviors.wait !== 'number' || response._behaviors.wait < 0);
});
if (hasInvalidWait) {
errors.push(exceptions.ValidationError("'wait' value must be an integer greater than or equal to 0", {
source: stub
}));
}
}
function hasStubInjection (stub) {
var hasResponseInjections = utils.isArray(stub.responses) && stub.responses.some(function (response) {
var hasDecorator = response._behaviors && response._behaviors.decorate;
return response.inject || hasDecorator;
}),
hasPredicateInjections = Object.keys(stub.predicates || {}).some(function (predicate) {
return stub.predicates[predicate].inject;
});
return hasResponseInjections || hasPredicateInjections;
}
function addStubInjectionErrors (stub, errors) {
if (!options.allowInjection && hasStubInjection(stub)) {
errors.push(exceptions.InjectionError(
'JavaScript injection is not allowed unless mb is run with the --allowInjection flag', { source: stub }));
}
}
function errorsForStub (stub, encoding, logger) {
var errors = [],
deferred = Q.defer();
if (!utils.isArray(stub.responses) || stub.responses.length === 0) {
errors.push(exceptions.ValidationError("'responses' must be a non-empty array", {
source: stub
}));
}
else {
addInvalidWaitErrors(stub, errors);
}
addStubInjectionErrors(stub, errors);
if (errors.length > 0) {
// no sense in dry-running if there are already problems;
// it will just add noise to the errors
deferred.resolve(errors);
}
else {
addDryRunErrors(stub, encoding, errors, logger).done(function () {
deferred.resolve(errors);
});
}
return deferred.promise;
}
function errorsForRequest (request) {
var errors = [],
hasRequestInjection = request.endOfRequestResolver && request.endOfRequestResolver.inject;
if (!options.allowInjection && hasRequestInjection) {
errors.push(exceptions.InjectionError(
'JavaScript injection is not allowed unless mb is run with the --allowInjection flag',
{ source: request.endOfRequestResolver }));
}
return errors;
}
function validate (request, logger) {
var stubs = request.stubs || [],
encoding = request.mode === 'binary' ? 'base64' : 'utf8',
validationPromises = stubs.map(function (stub) { return errorsForStub(stub, encoding, logger); }),
deferred = Q.defer();
validationPromises.push(Q(errorsForRequest(request)));
if (options.additionalValidation) {
validationPromises.push(Q(options.additionalValidation(request)));
}
Q.all(validationPromises).done(function (errorsForAllStubs) {
var allErrors = errorsForAllStubs.reduce(function (stubErrors, accumulator) {
return accumulator.concat(stubErrors);
}, []);
deferred.resolve({ isValid: allErrors.length === 0, errors: allErrors });
});
return deferred.promise;
}
return {
validate: validate
};
}
module.exports = {
create: create
};