node-consumer-pact-validation
Version:
A consumer pact generator written for pure nodeJS
241 lines (219 loc) • 7.29 kB
JavaScript
;
var AssertionError = require('assertion-error');
var _ = require('lodash');
var v2Ruleset = require(__dirname + '/lib/pact-v2-ruleset');
var queryString = require('query-string');
/**
* Private API
* Takes an object and lower-cases its' keys
* @param {Object} o Expected to be a set of headers
* @return {Object} New object with lowercase keys
*/
function makeObjectKeysLC (o){
var keys = Object.keys(o);
var lcObject = {};
keys.forEach(function(k){
lcObject[k.toLowerCase()] = o[k];
});
return lcObject;
}
/**
* Private API:
* removes whitespace after commas (for headers),
* so "alligators, hippos" -> "alligators,hippos"
* @param {Object} h A set of headers, either expected or actual
* @return {Object} A set of headers with whitespace removed
*/
function stripHeaderWhitespace (h){
var n = {};
var keys = Object.keys(h);
keys.forEach(function(k){
n[k] = h[k].replace(/,[ ]*/g, ',');
});
return n;
}
/**
* Private API:
* checks of the methods are equal, returns error object if not.
*/
function checkMethod(expected, actual){
if(!expected || !actual){
return;
}
if(actual.toLowerCase() !== expected.toLowerCase()){
return {
error: "HTTP Methods are not equal",
actual: actual,
expected: expected,
showDiff: true
};
}
}
/**
* Private api:
* Checks the path given
* @param {String} actual The actual url path, ie "/foo/bar/123"
* @param {String} expected The expecte url path
*/
function checkPath(expected, actual){
if(!expected && expected !== ""){
return;
}
if(actual.toLowerCase() !== expected.toLowerCase()){
return {
error: "Url paths are not equal",
actual: actual,
expected: expected,
showDiff: true
};
}
}
/**
* Private api:
* Checks the query string given. Note this is case sensitive.
* @param {String} actual query string
* @param {String} expected query string
*/
function checkQuery(expected, actual){
if(!expected){
return;
}
//Ignore trailing ampersands
var actualObj = actual ? queryString.parse(actual.replace(/\&$/, '')) : {};
var expectedObj = queryString.parse(expected);
if(! _.isEqual(actualObj, expectedObj)){
return {
error: "Query parameters do not match",
expected: expectedObj,
actual: actualObj
};
}
}
/**
* Private api:
* Checks the validity of the body. Actual should be strictly equal to the expected if
* using the simple (v1) matching rules. Otherwise apply the regex and type-checking of pact v2.
*/
function checkBody (expected, actual, matchingRules){
if(!expected){
return;
}
if(!matchingRules){ //Use v1 style simple matching
if(! _.isEqual(actual, expected)){
return {
error: "Body does not match expectation: ",
expected: expected,
actual: actual
};
}
}
else {
var paths = Object.keys(matchingRules);
var failures = paths.map(function(path){
return v2Ruleset(path, matchingRules[path], actual, expected);
}).filter(function(f){
return f !== false;
});
if(failures.length){
return failures[0];
}
}
}
/**
* Private api:
* Checks the validity of the headers being sent. Note that this isn't a strict equality check:
* - Header keys are expected to be case insensitive
* - Header values, when comma delimited, are whitespace insensitive
* - Additional, non-expected headers being sent are ignored - Expectation is that headers asserted are a subset of those sent.
*/
function checkHeaders(expected, actual, matchingRules){
if(!expected){
return;
}
//V1 Simple matching
if(!matchingRules){
// Make header key casing insensitive
var lcExpected = stripHeaderWhitespace(makeObjectKeysLC(expected));
var lcActual = stripHeaderWhitespace(makeObjectKeysLC(actual));
//Check all required headers are present:
var expectedKeys = Object.keys(lcExpected);
var failures = expectedKeys.map(function(k){
if(lcExpected[k] !== lcActual[k]){
return {
error: "Headers do not match expectation:",
expected: expected,
actual: actual
};
}
});
if(failures.length){
return failures[0];
}
}
else { //V2 Regex and more complex rule matching
var paths = Object.keys(matchingRules);
var failures = paths.map(function(path){
return v2Ruleset(path, matchingRules[path], actual, expected);
});
if(failures.length){
return failures[0];
}
}
}
/**
* Public API:
* Main consumer pact verification entrypoint. Given a set of expectations it will either return
* the verified object or throw.
*
* @param {Object} expected The expected request. Object must include method, path, query,
* headers and body elements as per https://github.com/pact-foundation/pact-specification/blob/version-2/testcases/request/body/matches.json
* @param {Object} actual Similarly, the actual request.
* @param {object} matchingRules Optional, The v2 specification of extended matching rules such as type, regex and min/max requirements.
* @param {String} comment Optional, A note about the test, used for identifying what's failing
* @return {Object} return verified object if no assertions fail. Throws in the event of an assertion failure.
*/
function verify(expected, actual, matchingRules, comment){
var errors = [];
errors.push(checkMethod(expected.method, actual.method));
errors.push(checkPath(expected.path, actual.path));
errors.push(checkQuery(expected.query, actual.query));
errors.push(checkBody(expected.body, actual.body, matchingRules));
errors.push(checkHeaders(expected.headers, actual.headers, matchingRules));
//Clean out the 'undefined' from the array leaving only valid errors
errors = errors.filter(function(e){
return !!e;
});
errors.forEach(function(e){
var message = comment ? comment + " - " + e.error : e.error;
throw new AssertionError(message, {
expected: e.expected,
actual: e.actual,
showDiff: true
});
});
//All good, pass on the expectations so they may be published
return expected;
}
module.exports = function(settings){
return function verifyPact (expected, actual, matchingRules, comment){
if(!expected){
throw {
error: "Expectation can't be undefined when verifying pacts"
};
}
if(!actual){
throw {
error: "Actual results can't be undefined when verifying pacts"
};
}
return {
consumer: {
name: settings.consumer
},
provider: {
name: settings.provider
},
interactions: verify(expected, actual, matchingRules, comment)
};
};
};