pactum-matchers
Version:
collection of json matchers for contract testing
359 lines (327 loc) • 11.3 kB
JavaScript
const { isPrimitive, getType } = require('./helpers');
const patterns = require('./patterns');
function compare(actual, expected, rules, path, inverse) {
const regex_rules = getRegExRules(rules);
return _compare(actual, expected, rules, regex_rules, path, inverse);
}
function _compare(actual, expected, rules, regex_rules, path, inverse) {
const rule = getCurrentPathRule(rules, regex_rules, path);
if (rule) {
compareWithRule(actual, expected, rules, regex_rules, path, rule, inverse);
} else {
typeCompare(actual, expected, path);
arrayCompare(actual, expected, rules, regex_rules, path);
objectCompare(actual, expected, rules, regex_rules, path);
valueCompare(actual, expected, path);
}
return '';
}
function escapeRegex(string) {
return string.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
}
function getRegExRules(rules) {
const regex_rules = {};
Object.keys(rules).forEach(rule => {
if (rule == '__proto__' || rule == 'constructor' || rule == 'prototype') {
return;
}
let regex_rule = escapeRegex(rule);
regex_rule = regex_rule.replace(/\\\[\\\*\\\]/g, '\\\[\\d+\\\]');
if (regex_rule.endsWith('.\\*')) {
regex_rules[regex_rule.slice(0, regex_rule.length - 2) + '[^.]+$'] = rules[rule];
} else {
regex_rules[regex_rule + '$'] = rules[rule];
}
});
return regex_rules;
}
function getCurrentPathRuleUsingRegEx(regex_rules, path) {
const rules = Object.keys(regex_rules);
let fall_back_rule = null;
for (let i = 0; i < rules.length; i++) {
const rule = rules[i];
if (rule == '__proto__' || rule == 'constructor' || rule == 'prototype') {
return;
}
const rx = new RegExp(rule);
if (rx.test(path)) {
if (rule.endsWith('.[^.]+$')) {
fall_back_rule = regex_rules[rule];
} else {
return regex_rules[rule];
}
}
}
return fall_back_rule;
}
function getCurrentPathRule(rules, regex_rules, path) {
if (rules[path]) return rules[path];
const genericPath = path.replace(/\[\d+\]/g, '[*]');
if (rules[genericPath]) return rules[genericPath];
let dotIndex = path.lastIndexOf('.');
const allPropsPath = `${path.slice(0, dotIndex)}.*`;
if (rules[allPropsPath]) return rules[allPropsPath];
dotIndex = genericPath.lastIndexOf('.');
const allPropsGenericPath = `${genericPath.slice(0, dotIndex)}.*`;
if (rules[allPropsGenericPath]) return rules[allPropsGenericPath];
if (rules[genericPath]) return rules[genericPath];
return getCurrentPathRuleUsingRegEx(regex_rules, path)
}
function compareWithRule(actual, expected, rules, regex_rules, path, rule, inverse) {
switch (rule.match) {
case 'type':
compareWithRuleType(actual, expected, rules, regex_rules, path, rule);
break;
case 'regex':
compareWithRuleRegex(actual, rule, path, inverse);
break;
case 'oneOf':
compareWithRuleOneOf(actual, rule, path);
break;
case 'expr':
compareWithRuleExpr(actual, rule, path);
break;
case 'string':
compareWithString(actual, rule, path);
break;
case 'uuid':
compareWithUUID(actual, rule, path);
break;
case 'email':
compareWithEmail(actual, rule, path);
break;
case 'any':
compareWithAny(actual, rule, path);
break;
case 'int':
compareWithInt(actual, rule, path);
break;
case 'float':
compareWithFloat(actual, rule, path);
break;
case 'gt':
compareWithGt(actual, expected, rule, path);
break;
case 'gte':
compareWithGte(actual, expected, rule, path);
break;
case 'lt':
compareWithLt(actual, expected, rule, path);
break;
case 'lte':
compareWithLte(actual, expected, rule, path);
break;
case 'not_includes':
compareNotIncludes(actual, expected, rule, path);
break;
case 'not_null':
compareWithNotNull(actual, path);
break;
case 'not_equals':
compareWithNotEquals(actual, expected, path);
break;
case 'iso':
compareWithISO(actual, expected, path);
break;
}
}
function compareWithRuleType(actual, expected, rules, regex_rules, path, rule) {
typeCompare(actual, expected, path);
const type = getType(expected);
if (type === 'array') {
if (typeof rule.min !== 'undefined') {
if (actual.length < rule.min) {
throw `Json doesn't have 'array' with min length of '${rule.min}' at '${path}' but found 'array' with length '${actual.length}'`;
}
if (rule.min > 0 || typeof expected[0] !== 'undefined') {
for (let i = 0; i < actual.length; i++) {
_compare(actual[i], expected[0], rules, regex_rules, `${path}[${i}]`);
}
}
} else {
arrayCompare(actual, expected, rules, regex_rules, path);
}
} else if (type === 'object') {
objectCompare(actual, expected, rules, regex_rules, path);
}
}
function compareWithRuleRegex(actual, rule, path, inverse) {
if (inverse) {
return;
}
const regex = new RegExp(rule.regex);
if (!regex.test(actual)) {
throw `Json doesn't match with "${rule.regex}" at "${path}" but found "${actual}"`;
}
}
function compareWithRuleOneOf(actual, rule, path) {
const values = rule.value;
let found = false;
for (let i = 0; i < values.length; i++) {
found = actual === values[i];
if (found) break;
}
if (!found) {
throw `Json doesn't have one of the expected values at "${path}" but found "${actual}"`;
}
}
function compareWithRuleExpr(actual, rule, path) {
const expr = rule.expr;
const expression = expr.replace('$V', 'actual');
if (eval(expression) !== true) {
throw `Json doesn't fulfil expression '${expression.replace('actual', path).trim()}'`;
}
}
function compareWithString(actual, rule, path) {
const type = getType(actual);
if (type !== 'string') {
throw `Json doesn't have type 'string' at '${path}' but found '${type}'`;
}
if (actual.length === 0) {
throw `Json have an empty string at '${path}'`;
}
}
function compareWithUUID(actual, rule, path) {
const pattern = patterns.UUID;
if (!pattern.test(actual)) {
throw `Json doesn't match with "UUID" pattern at "${path}" but found "${actual}"`;
}
}
function compareWithAny(actual, rule, path) {
const types = [Number, String, Boolean, Object, Symbol, null, undefined]
const type = getType(actual);
if (type in types) {
throw `Json doesn't have type 'any' at '${path}' but found '${type}'`;
}
}
function compareWithInt(actual, rule, path) {
const type = getType(actual);
if (type !== 'number') {
throw `Json doesn't have type 'number' at '${path}' but found '${type}'`;
} else {
const pattern = patterns.INT;
if (!pattern.test(actual)) {
throw `Json doesn't have 'integer' number at '${path}' but found '${actual}'`;
}
}
}
function compareWithFloat(actual, rule, path) {
const type = getType(actual);
if (type !== 'number') {
throw `Json doesn't have type 'number' at '${path}' but found '${type}'`;
} else {
const pattern = patterns.FLOAT;
if (!pattern.test(actual)) {
throw `Json doesn't have 'float' number at '${path}' but found '${actual}'`;
}
}
}
function compareWithGt(actual, expected, rule, path) {
const type = getType(actual);
if (type !== 'number') {
throw `Json doesn't have type 'number' at '${path}' but found '${type}'`;
} else if (!(actual > expected)) {
throw `Json doesn't have 'greater' value than '${expected}' at '${path}' but found '${actual}'`;
}
}
function compareWithGte(actual, expected, rule, path) {
const type = getType(actual);
if (type !== 'number') {
throw `Json doesn't have type 'number' at '${path}' but found '${type}'`;
} else if (!(actual >= expected)) {
throw `Json doesn't have 'greater or equal' value than '${expected}' at '${path}' but found '${actual}'`;
}
}
function compareWithLt(actual, expected, rule, path) {
const type = getType(actual);
if (type !== 'number') {
throw `Json doesn't have type 'number' at '${path}' but found '${type}'`;
} else if (!(actual < expected)) {
throw `Json doesn't have 'lesser' value than '${expected}' at '${path}' but found '${actual}'`;
}
}
function compareWithLte(actual, expected, rule, path) {
const type = getType(actual);
if (type !== 'number') {
throw `Json doesn't have type 'number' at '${path}' but found '${type}'`;
} else if (!(actual <= expected)) {
throw `Json doesn't have 'lesser or equal' value than '${expected}' at '${path}' but found '${actual}'`;
}
}
function compareWithEmail(actual, rule, path) {
const pattern = patterns.EMAIL;
if (!pattern.test(actual)) {
throw `Json doesn't match with "EMAIL" pattern at "${path}" but found "${actual}"`;
}
}
function compareNotIncludes(actual, expected_values, rule, path) {
const expected_type = getType(expected_values);
if (expected_type !== 'array') {
expected_values = [expected_values];
}
for (const expected of expected_values) {
const actual_type = getType(actual);
if (actual_type === 'object') {
if (typeof actual[expected] !== 'undefined') {
throw `Json has a property of "${expected}" at "${path}"`;
}
} else if (actual_type === 'array') {
if (actual.includes(expected)) {
throw `Json has an element "${expected}" in an array at "${path}"`;
}
} else {
throw `Json doesn't have a "object" at "${path}"`;
}
}
}
function compareWithNotNull(actual, path) {
if (actual === null) {
throw `Json has a "null" at "${path}"`;
}
}
function compareWithNotEquals(actual, expected, path) {
if (actual === expected) {
throw `Json have a value '${expected}' at '${path}'`;
}
}
function typeCompare(actual, expected, path) {
const actualType = getType(actual);
const expectedType = getType(expected);
if (actualType !== expectedType) {
throw `Json doesn't have type '${expectedType}' at '${path}' but found '${actualType}'`;
}
}
function arrayCompare(actual, expected, rules, regex_rules, path) {
if (getType(expected) === 'array') {
if (actual.length !== expected.length) {
throw `Json doesn't have 'array' with length '${expected.length}' at '${path}' but found 'array' with length '${actual.length}'`;
}
for (let i = 0; i < expected.length; i++) {
_compare(actual[i], expected[i], rules, regex_rules, `${path}[${i}]`);
}
}
}
function objectCompare(actual, expected, rules, regex_rules, path) {
if (getType(expected) === 'object') {
for (const prop in expected) {
if (!Object.prototype.hasOwnProperty.call(actual, prop)) {
throw `Json doesn't have property '${prop}' at '${path}'`;
}
_compare(actual[prop], expected[prop], rules, regex_rules, `${path}.${prop}`);
}
}
}
function valueCompare(actual, expected, path) {
if (isPrimitive(expected)) {
if (actual !== expected) throw `Json doesn't have a value '${expected}' at '${path}' but found '${actual}'`;
}
}
function compareWithISO(actual, rule, path) {
const pattern = patterns.ISO;
if (!pattern.test(actual)) {
throw `Json doesn't match with "ISO" date format at "${path}" but found "${actual}"`;
}
}
module.exports = {
compare
};