UNPKG

cumulocity-cypress

Version:
343 lines (337 loc) 15.3 kB
'use strict'; var httpcontroller = require('./httpcontroller-BmRpHFCn.js'); var _ = require('lodash'); var datefns = require('date-fns'); require('util'); require('express'); require('raw-body'); require('cookie-parser'); require('winston'); require('morgan'); require('set-cookie-parser'); require('cookie'); require('@c8y/client'); require('http-proxy-middleware'); require('fs'); require('path'); require('semver'); require('swagger-ui-express'); require('yaml'); require('debug'); function _interopNamespaceDefault(e) { var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var datefns__namespace = /*#__PURE__*/_interopNamespaceDefault(datefns); /** * Default implementation of C8yPactMatcher to match C8yPactRecord objects. Pacts * are matched by comparing the properties of the objects using property matchers. * If no property matcher is configured for a property, the property will be matched * by equality. Disable Cypress.c8ypact.config.strictMatching to ignore properties that are * missing in matched objects. In case objects do not match an C8yPactError is thrown. */ class C8yDefaultPactMatcher { constructor(propertyMatchers = { body: new C8yPactBodyMatcher(), requestBody: new C8yPactBodyMatcher(), duration: new C8yNumberMatcher(), date: new C8yIgnoreMatcher(), Authorization: new C8yIgnoreMatcher(), auth: new C8yIgnoreMatcher(), options: new C8yIgnoreMatcher(), createdObject: new C8yIgnoreMatcher(), location: new C8yIgnoreMatcher(), url: new C8yIgnoreMatcher(), "X-XSRF-TOKEN": new C8yIgnoreMatcher(), lastMessage: new C8yISODateStringMatcher(), }) { this.propertyMatchers = {}; this.propertyMatchers = propertyMatchers; } match(obj1, obj2, options) { if (obj1 === obj2) return true; const parents = options?.parents ?? []; const strictMatching = options?.strictMatching ?? false; const schemaMatcher = options?.schemaMatcher || C8yDefaultPactMatcher.schemaMatcher; const throwPactError = (message, key) => { const errorMessage = `Pact validation failed! ${message}`; const newErr = new Error(errorMessage); newErr.name = "C8yPactError"; if (options?.loggerProps) { options.loggerProps.error = errorMessage; options.loggerProps.key = key; options.loggerProps.keypath = keyPath(key); options.loggerProps.objects = key && _.isPlainObject(obj1) && _.isPlainObject(obj2) ? [_.pick(obj1, [key]), _.pick(obj2, [key])] : [obj1, obj2]; } throw newErr; }; const keyPath = (k) => { return `${[...parents, ...(k ? [k] : [])].join(" > ")}`; }; const isArrayOfPrimitives = (value) => { if (!_.isArray(value)) { return false; } const primitiveTypes = ["undefined", "boolean", "number", "string"]; return (value.filter((p) => primitiveTypes.includes(typeof p)).length === value.length); }; if (_.isString(obj1) && _.isString(obj2) && !_.isEqual(obj1, obj2)) { throwPactError(`"${keyPath()}" text did not match.`); } if (!_.isObject(obj1) || !_.isObject(obj2)) { throwPactError(`Expected 2 objects as input for matching, but got "${typeof obj1}" and ${typeof obj2}".`); } // get keys of objects without schema keys and schema keys separately const objectKeys = Object.keys(obj1).filter((k) => !k.startsWith("$")); const schemaKeys = Object.keys(obj2).filter((k) => k.startsWith("$")); // normalize pact keys and remove keys that have a schema defined // we do not want for example body and $body const pactKeys = Object.keys(obj2).reduce((acc, key) => { if (!schemaKeys.includes(`$${key}`)) { acc.push(key); } return acc; }, []); if (_.isEmpty(objectKeys) && _.isEmpty(pactKeys)) { return true; } const removeSchemaPrefix = (key) => key.startsWith("$") ? key.slice(1) : key; // if strictMatching is disabled, only check properties of the pact for object matching // strictMatching for schema matching is considered within the matcher -> schema.additionalProperties const keys = !strictMatching ? pactKeys : objectKeys; for (const key of keys) { // schema is always defined on the pact object - needs special consideration const isSchema = key.startsWith("$") || schemaKeys.includes(`$${key}`); const value = _.get(strictMatching || isSchema ? obj1 : obj2, removeSchemaPrefix(key)); const pact = _.get(strictMatching || isSchema ? obj2 : obj1, isSchema && !key.startsWith("$") ? `$${key}` : key); if (!(strictMatching ? pactKeys : objectKeys).includes(key) && !isSchema) { throwPactError(`"${keyPath(key)}" not found in ${strictMatching ? "pact" : "response"} object.`); } if (isSchema) { const errorKey = removeSchemaPrefix(key); if (!schemaMatcher) { throwPactError(`No schema matcher registered to validate "${keyPath(errorKey)}".`, errorKey); } try { if (!schemaMatcher.match(value, pact, strictMatching)) { throwPactError(`Schema for "${keyPath(errorKey)}" does not match.`, errorKey); } } catch (error) { throwPactError(`Schema for "${keyPath(errorKey)}" does not match. (${error})`, errorKey); } } else if (this.getPropertyMatcher(key, options?.ignoreCase) != null) { if (!strictMatching && !value) { continue; } try { const result = this.getPropertyMatcher(key, options?.ignoreCase)?.match(value, pact, _.extend(options, { parents: [...parents, key] })); if (!result) throw new Error(""); } catch (error) { // calling match recursively requires to pass the root error if (_.get(error, "name") === "C8yPactError") { throw error; } else { throwPactError(`Values for "${keyPath(key)}" do not match.${error != null ? " " + error : ""}`, key); } } } else if (isArrayOfPrimitives(value) && isArrayOfPrimitives(pact)) { const v = [value, pact].sort((a1, a2) => a2.length - a1.length); const diff = _.difference(v[0], v[1]); if (_.isEmpty(diff)) { continue; } else { throwPactError(`Array with key "${keyPath(key)}" has unexpected values "${diff}".`, key); } } else if (_.isArray(value) && _.isArray(pact)) { if (value.length !== pact.length) { throwPactError(`Array with key "${keyPath(key)}" has different lengths.`, key); } for (let i = 0; i < value.length; i++) { this.match(value[i], pact[i], _.extend(options, { parents: [...parents, key, `${i}`] })); } } else if (_.isObjectLike(value) && _.isObjectLike(pact)) { // if strictMatching is disabled, value1 and value2 have been swapped // swap back to ensure swapping in next iteration works as expected this.match(strictMatching ? value : pact, strictMatching ? pact : value, _.extend(options, { parents: [...parents, key] })); } else { if (value != null && pact != null && !_.isEqual(value, pact)) { throwPactError(`Values for "${keyPath(key)}" do not match.`, key); } } } return true; } /** * Returns the property matcher for the given property name. * @param key The property name to get the matcher for. * @param ignoreCase Whether to ignore the case of the property name. */ getPropertyMatcher(key, ignoreCase = false) { if (ignoreCase) { return httpcontroller.get_i(this.propertyMatchers, key); } return this.propertyMatchers[key]; } /** * Adds a new property matcher for the given property name. */ addPropertyMatcher(propertyName, matcher) { this.propertyMatchers[propertyName] = matcher; } /** * Removes the property matcher for the given property name. */ removePropertyMatcher(propertyName) { delete this.propertyMatchers[propertyName]; } } /** * Extends C8yDefaultPactMatcher with default property matchers for Cumulocity * response bodies. It has rules configured at least for the following properties: * id, statistics, lastUpdated, creationTime, next, self, password, owner, tenantId * and lastPasswordChange. It is registered for the properties body and requestBody. */ class C8yPactBodyMatcher extends C8yDefaultPactMatcher { constructor(propertyMatchers = {}) { super(propertyMatchers); this.addPropertyMatcher("id", new C8ySameTypeMatcher()); this.addPropertyMatcher("statistics", new C8yIgnoreMatcher()); this.addPropertyMatcher("lastUpdated", new C8yISODateStringMatcher()); this.addPropertyMatcher("creationTime", new C8yISODateStringMatcher()); this.addPropertyMatcher("next", new C8yIgnoreMatcher()); this.addPropertyMatcher("self", new C8yIgnoreMatcher()); this.addPropertyMatcher("password", new C8yIgnoreMatcher()); this.addPropertyMatcher("owner", new C8ySameTypeMatcher()); this.addPropertyMatcher("tenantId", new C8yIgnoreMatcher()); this.addPropertyMatcher("lastPasswordChange", new C8yISODateStringMatcher()); } } class C8yIdentifierMatcher { match(obj1, obj2) { [obj1, obj2].forEach((id) => { if (_.isString(id) === false || /^\d+$/.test(id) === false) { throw new Error(`Value "${id}" is not a valid identifier.`); } }); return true; } } class C8yNumberMatcher { match(obj1, obj2) { [obj1, obj2].forEach((n) => { if (!_.isNumber(n) || _.isNaN(n)) { throw new Error(`Value "${obj1}" is not a number.`); } }); return true; } } class C8yStringMatcher { match(obj1, obj2) { [obj1, obj2].forEach((s) => { if (!_.isString(s)) { throw new Error(`Value "${s}" is not a string.`); } }); return true; } } class C8yIgnoreMatcher { match() { return true; } } class C8ySameTypeMatcher { match(obj1, obj2) { const result = typeof obj1 === typeof obj2; if (!result) { throw new Error(`Values are not of same type. Expected ${typeof obj1} but got ${typeof obj2}`); } return result; } } class C8yISODateStringMatcher { match(obj1, obj2) { // validate regex as parseISO does not throw an error for invalid dates // and is not strict enough for our use case // https://regex101.com/library/6gJsuQ?filterFlavors=javascript&page=9 const isoRegex = new RegExp(/^((\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-((0[13578]|1[02])-(0[1-9]|[12]\d|3[01])|(0[469]|11)-(0[1-9]|[12]\d|30)|(02)-(0[1-9]|1\d|2[0-8])))T([01]\d|2[0-3]):[0-5]\d:[0-5]\d\.\d{3}([+-]([01]\d|2[0-3]):[0-5]\d|Z)$/); [obj1, obj2].forEach((obj) => { if (!_.isString(obj)) { throw new Error(`Value "${obj}" is not a string.`); } if (!isoRegex.test(obj)) { throw new Error(`Value "${obj}" is not a valid ISO date string.`); } }); const d1 = datefns__namespace.parseISO(obj1); const d2 = datefns__namespace.parseISO(obj2); return datefns__namespace.isValid(d1) && datefns__namespace.isValid(d2); } } exports.C8yDefaultPact = httpcontroller.C8yDefaultPact; exports.C8yDefaultPactPreprocessor = httpcontroller.C8yDefaultPactPreprocessor; exports.C8yDefaultPactRecord = httpcontroller.C8yDefaultPactRecord; exports.C8yPactHttpController = httpcontroller.C8yPactHttpController; exports.C8yPactHttpControllerDefaultMode = httpcontroller.C8yPactHttpControllerDefaultMode; exports.C8yPactHttpControllerDefaultRecordingMode = httpcontroller.C8yPactHttpControllerDefaultRecordingMode; exports.C8yPactHttpControllerLogLevel = httpcontroller.C8yPactHttpControllerLogLevel; exports.C8yPactModeValues = httpcontroller.C8yPactModeValues; exports.C8yPactPreprocessorDefaultOptions = httpcontroller.C8yPactPreprocessorDefaultOptions; exports.C8yPactRecordingModeValues = httpcontroller.C8yPactRecordingModeValues; exports.createPactRecord = httpcontroller.createPactRecord; exports.getEnvVar = httpcontroller.getEnvVar; exports.isCypressError = httpcontroller.isCypressError; exports.isCypressResponse = httpcontroller.isCypressResponse; exports.isIResult = httpcontroller.isIResult; exports.isOneOfStrings = httpcontroller.isOneOfStrings; exports.isPact = httpcontroller.isPact; exports.isPactError = httpcontroller.isPactError; exports.isPactRecord = httpcontroller.isPactRecord; exports.isValidPactId = httpcontroller.isValidPactId; exports.isWindowFetchResponse = httpcontroller.isWindowFetchResponse; exports.oauthLogin = httpcontroller.oauthLogin; exports.pactId = httpcontroller.pactId; exports.toCypressResponse = httpcontroller.toCypressResponse; exports.toPactRequest = httpcontroller.toPactRequest; exports.toPactResponse = httpcontroller.toPactResponse; exports.toPactSerializableObject = httpcontroller.toPactSerializableObject; exports.toSerializablePactRecord = httpcontroller.toSerializablePactRecord; exports.validatePactMode = httpcontroller.validatePactMode; exports.validatePactRecordingMode = httpcontroller.validatePactRecordingMode; exports.C8yDefaultPactMatcher = C8yDefaultPactMatcher; exports.C8yISODateStringMatcher = C8yISODateStringMatcher; exports.C8yIdentifierMatcher = C8yIdentifierMatcher; exports.C8yIgnoreMatcher = C8yIgnoreMatcher; exports.C8yNumberMatcher = C8yNumberMatcher; exports.C8yPactBodyMatcher = C8yPactBodyMatcher; exports.C8ySameTypeMatcher = C8ySameTypeMatcher; exports.C8yStringMatcher = C8yStringMatcher;