cumulocity-cypress
Version:
Cypress commands for Cumulocity IoT
1,297 lines (1,285 loc) • 101 kB
JavaScript
'use strict';
var _$2 = require('lodash');
var util = require('util');
var express = require('express');
var getRawBody = require('raw-body');
var cookieParser = require('cookie-parser');
var winston = require('winston');
var morgan = require('morgan');
var datefns = require('date-fns');
var setCookieParser = require('set-cookie-parser');
var libCookie = require('cookie');
var fetch = require('cross-fetch');
var buffer = require('buffer');
var httpProxyMiddleware = require('http-proxy-middleware');
var fs = require('fs');
var path = require('path');
var semver = require('semver');
var swaggerUi = require('swagger-ui-express');
var yaml = require('yaml');
var debug = 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 ___namespace = /*#__PURE__*/_interopNamespaceDefault(_$2);
var datefns__namespace = /*#__PURE__*/_interopNamespaceDefault(datefns);
var setCookieParser__namespace = /*#__PURE__*/_interopNamespaceDefault(setCookieParser);
var libCookie__namespace = /*#__PURE__*/_interopNamespaceDefault(libCookie);
var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
var semver__namespace = /*#__PURE__*/_interopNamespaceDefault(semver);
function isURL(obj) {
return obj instanceof URL;
}
function removeBaseUrlFromString(url, baseUrl) {
if (!url || !baseUrl) {
return url;
}
let normalizedBaseUrl = _$2.clone(baseUrl);
while (normalizedBaseUrl.endsWith("/")) {
normalizedBaseUrl = normalizedBaseUrl.slice(0, -1);
}
let result = url.replace(normalizedBaseUrl, "");
if (_$2.isEmpty(result)) {
result = "/";
}
return result;
}
function removeBaseUrlFromRequestUrl(record, baseUrl) {
if (!record?.request?.url || !baseUrl || !_$2.isString(baseUrl)) {
return;
}
record.request.url = removeBaseUrlFromString(record.request.url, baseUrl);
}
/**
* Checks if the given URL is an absolute URL.
* @param url The URL to check.
* @returns True if the URL is an absolute URL, false otherwise.
*/
function isAbsoluteURL(url) {
if (!url || !_$2.isString(url) || _$2.isEmpty(url))
return false;
return /^https?:\/\//i.test(url);
}
function safeStringify(obj, indent = 2) {
let cache = [];
const retVal = JSON.stringify(obj, (key, value) => typeof value === "object" && value !== null
? cache.includes(value)
? undefined
: cache.push(value) && value
: value, indent);
cache = [];
return retVal;
}
/**
* Gets the case-sensitive path for a given case-insensitive path. The path is
* assumed to be a dot-separated string. If the path is an array, it is assumed
* to be a list of keys.
*
* The function will go over all keys and return the actual case-sensitive path
* up to the first mismatch.
*
* @param obj The object to query
* @param path The case-insensitive path to find
* @returns The actual case-sensitive path if found, undefined otherwise
*/
function toSensitiveObjectKeyPath(obj, path) {
if (!obj)
return undefined;
const keys = _$2.isArray(path) ? path : path.split(/[.[\]]/g);
let current = obj;
const actualPath = [];
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (_$2.isEmpty(key))
continue;
if (current === null || current === undefined) {
return undefined;
}
if (_$2.isArray(current)) {
const index = parseInt(key);
if (!isNaN(index)) {
if (index >= 0 && index < current.length) {
current = current[index];
actualPath.push(key);
continue;
}
}
actualPath.push(...keys.slice(i));
return actualPath.join(".");
}
// Handle object case with case-insensitive matching
if (_$2.isObjectLike(current)) {
const matchingKey = Object.keys(current).find((k) => k.toLowerCase() === key.toLowerCase());
if (matchingKey !== undefined) {
current = current[matchingKey];
actualPath.push(matchingKey);
}
else {
actualPath.push(...keys.slice(i));
break;
}
}
else {
actualPath.push(...keys.slice(i));
break;
}
}
return actualPath.join(".");
}
/**
* Gets the value of a case-insensitive key path from an object. The path is
* assumed to be a dot-separated string. If the path is an array, it is assumed
* to be a list of keys.
*
* @example
* geti(obj, "obj.key.token")
* geti(obj, ["obj", "key", "token"])
* geti(obj, "obj.key[0].token")
* geti(obj, "obj.key.0.token")
*
* @param obj The object to query
* @param keyPath The case-insensitive key path to find
* @returns The value of the key path if found, undefined otherwise
*/
function get_i(obj, keyPath) {
if (obj == null || keyPath == null)
return undefined;
const sensitivePath = toSensitiveObjectKeyPath(obj, keyPath);
if (sensitivePath == null)
return undefined;
return _$2.get(obj, sensitivePath);
}
/**
* Converts a string value to a boolean. Supported values are "true", "false", "1", and "0".
* @param input The input string to convert to a boolean
* @param defaultValue The default value to return if the input is not a valid boolean string
* @returns The boolean value of the input string or the default value if the input is not a valid boolean string
*/
function to_boolean(input, defaultValue) {
if (input == null || !_$2.isString(input))
return defaultValue;
const booleanString = input.toString().toLowerCase();
if (booleanString == "true" || booleanString === "1")
return true;
if (booleanString == "false" || booleanString === "0")
return false;
return defaultValue;
}
/// <reference types="cypress" />
// workaround for lodash import in Cypress nodejs typescript runtime and browser
const _$1 = _$2 || ___namespace;
const C8yPactModeValues = [
"record",
"recording",
"apply",
"forward",
"disabled",
"mock",
];
const C8yPactRecordingModeValues = [
"refresh",
"append",
"new",
"replace",
];
const C8yPactObjectKeys = ["records", "info", "id"];
function isValidPactId(value) {
if (value == null || value.length > 1000 || !_$1.isString(value))
return false;
const validPactIdRegex = /^[a-zA-Z0-9_-]+(__[a-zA-Z0-9_-]+)*$/;
return validPactIdRegex.test(value);
}
/**
* Creates an C8yPactID for a given string or array of strings.
* @param value The string or array of strings to convert to a pact id.
* @returns The pact id.
*/
function pactId(value) {
let result = "";
const suiteSeparator = "__";
const normalize = (value) => value
.split(suiteSeparator)
.map((v) => _$1.words(_$1.deburr(v), /[a-zA-Z0-9_-]+/g).join("_"))
.join(suiteSeparator);
if (value != null && _$1.isArray(value)) {
result = value.map((v) => normalize(v)).join(suiteSeparator);
}
else if (value != null && _$1.isString(value)) {
result = normalize(value);
}
if (result == null || _$1.isEmpty(result)) {
return !value ? value : undefined;
}
return result;
}
/**
* Validate the given pact mode. Throws an error if the mode is not supported
* or undefined.
* @param mode The pact mode to validate.
*/
function validatePactMode(mode) {
if (mode != null) {
const values = Object.values(C8yPactModeValues);
if (!_$1.isString(mode) ||
_$1.isEmpty(mode) ||
!values.includes(mode.toLowerCase())) {
const error = new Error(`Unsupported pact mode: "${mode}". Supported values are: ${values.join(", ")} or undefined.`);
error.name = "C8yPactError";
throw error;
}
}
}
/**
* Validate the given pact recording mode. Throws an error if the mode is not supported
* or undefined.
* @param mode The pact recording mode to validate.
*/
function validatePactRecordingMode(mode) {
if (mode != null) {
const keys = Object.values(C8yPactRecordingModeValues);
if (!_$1.isString(mode) ||
_$1.isEmpty(mode) ||
!keys.includes(mode.toLowerCase())) {
const error = new Error(`Unsupported recording mode: "${mode}". Supported values are: ${keys.join(", ")} or undefined.`);
error.name = "C8yPactError";
throw error;
}
}
}
/**
* Checks if the given object is a C8yPact. This also includes checking
* all records to be valid C8yPactRecord instances.
*
* @param obj The object to check.
* @returns True if the object is a C8yPact, false otherwise.
*/
function isPact(obj) {
return (_$1.isObjectLike(obj) &&
"info" in obj &&
_$1.isObjectLike(_$1.get(obj, "info")) &&
"records" in obj &&
_$1.isArray(_$1.get(obj, "records")) &&
_$1.every(_$1.get(obj, "records"), isPactRecord) &&
_$1.isFunction(_$1.get(obj, "nextRecord")) &&
_$1.isFunction(_$1.get(obj, "nextRecordMatchingRequest")) &&
_$1.isFunction(_$1.get(obj, "appendRecord")) &&
_$1.isFunction(_$1.get(obj, "replaceRecord")));
}
/**
* Checks if the given object is a C8yPactRecord.
*
* @param obj The object to check.
* @returns True if the object is a C8yPactRecord, false otherwise.
*/
function isPactRecord(obj) {
return (_$1.isObjectLike(obj) &&
"request" in obj &&
_$1.isObjectLike(_$1.get(obj, "request")) &&
"response" in obj &&
_$1.isObjectLike(_$1.get(obj, "response")) &&
_$1.isFunction(_$1.get(obj, "toCypressResponse")));
}
/**
* Checks if the given object is a Cypress.Response.
*
* @param obj The object to check.
* @returns True if the object is a Cypress.Response, false otherwise.
*/
function isCypressResponse(obj) {
return (_$1.isObjectLike(obj) &&
"body" in obj &&
"status" in obj &&
"headers" in obj &&
"requestHeaders" in obj &&
"duration" in obj &&
"url" in obj &&
"isOkStatusCode" in obj &&
// not a window.Response or Client.FetchResponse
!("ok" in obj || "arrayBuffer" in obj));
}
/**
* Checks if the given object is a C8yPactError. A C8yPactError is an error
* with the name "C8yPactError".
*
* @param error The object to check.
* @returns True if the object is a C8yPactError, false otherwise.
*/
function isPactError(error) {
return _$1.isError(error) && _$1.get(error, "name") === "C8yPactError";
}
function isDefined(value) {
return !_$1.isUndefined(value);
}
/**
* Converts a Cypress.Response to a C8yPactRequest.
*/
function toPactRequest(response) {
if (!response)
return response;
const result = _$1.pickBy(_$1.mapKeys(_$1.pick(response, ["url", "method", "requestHeaders", "requestBody"]), (v, k) => {
if (_$1.isEqual(k, "requestHeaders"))
return "headers";
if (_$1.isEqual(k, "requestBody"))
return "body";
return k;
}), isDefined);
if (_$1.isEmpty(result))
return undefined;
return result;
}
/**
* Converts a Cypress.Response to a C8yPactResponse.
*/
function toPactResponse(response) {
if (!response)
return response;
const result = _$1.pickBy(_$1.pick(response, [
"status",
"statusText",
"body",
"headers",
"duration",
"isOkStatusCode",
"allRequestResponses",
"$body",
]), isDefined);
if (_$1.isEmpty(result))
return undefined;
return result;
}
/**
* Returns the value of the environment variable with the given name. The function
* tries to find the value in the global `process.env` or `Cypress.env()`. If `env`
* is provided, the function uses the given object as environment.
*
* The function tries to find the value in the following order:
* - `name`
* - `camelCase(name)`
* - `CYPRESS_name`
* - `name.replace(/^C8Y_/i, "")`
* - `CYPRESS_camelCase(name)`
* - `CYPRESS_camelCase(name.replace(/^C8Y_/i, ""))`
*
* @param name The name of the environment variable.
* @param env The environment object to use. Default is `process.env` or `Cypress.env()`
*
* @returns The value of the environment variable or `undefined` if not found.
*/
function getEnvVar(name, env) {
if (!name)
return undefined;
const e = env ||
(typeof window !== "undefined" && window.Cypress
? Cypress.env()
: process.env);
function getFromEnv(key) {
return e[key];
}
function getForName(name) {
return getFromEnv(name) || getFromEnv(`CYPRESS_${name}`);
}
const plainName = name.replace(/^C8Y_/i, "");
const camelCasedName = _$1.camelCase(name).replace(/^c8Y/i, "c8y");
const camelCasedPlainName = _$1.camelCase(plainName);
return (getForName(name) ||
getForName(camelCasedName) ||
getForName(plainName) ||
getForName(camelCasedPlainName));
}
function isOneOfStrings(value, values) {
if (!_$1.isString(value) || _$1.isEmpty(value))
return false;
return values.includes(value.toLowerCase());
}
function getCreatedObjectId(response) {
let newId = response?.body?.id;
if (newId) {
return newId;
}
else {
const location = get_i(response, "headers.location");
if (isAbsoluteURL(location)) {
try {
const url = new URL(location);
const pathSegments = url?.pathname.split("/").filter(Boolean);
newId = pathSegments?.pop();
if (newId != null) {
return decodeURIComponent(newId);
}
}
catch {
// do nothing
}
}
}
return undefined;
}
/// <reference types="cypress" />
const C8yPactAuthObjectKeys = [
"userAlias",
"user",
"type",
];
/**
* Checks if the given object is a C8yAuthOptions.
*
* @param obj The object to check.
* @param options Options to check for additional properties.
* @returns True if the object is a C8yAuthOptions, false otherwise.
*/
function isAuthOptions(obj) {
return _$2.isObjectLike(obj) && "user" in obj && "password" in obj;
}
function toPactAuthObject(obj) {
return _$2.pick(obj, C8yPactAuthObjectKeys);
}
function isPactAuthObject(obj) {
return (_$2.isObjectLike(obj) &&
"user" in obj &&
("userAlias" in obj || "type" in obj) &&
Object.keys(obj).every((key) => C8yPactAuthObjectKeys.includes(key)));
}
function normalizeAuthHeaders(headers) {
// required to fix inconsistencies between c8yclient and interceptions
// using lowercase and uppercase. fix here.
const xsrfTokenHeader = Object.keys(headers || {}).find((key) => key.toLowerCase() === "x-xsrf-token");
const authorizationHeader = Object.keys(headers || {}).find((key) => key.toLowerCase() === "authorization");
if (xsrfTokenHeader && xsrfTokenHeader !== "X-XSRF-TOKEN") {
headers["X-XSRF-TOKEN"] = headers[xsrfTokenHeader];
delete headers[xsrfTokenHeader];
}
if (authorizationHeader && authorizationHeader !== "Authorization") {
headers["Authorization"] = headers[authorizationHeader];
delete headers[authorizationHeader];
}
return headers;
}
/**
* Default implementation of C8yPactRecord. Use C8yDefaultPactRecord.from to create
* a C8yPactRecord from a Cypress.Response object or an C8yPactRecord object.
*/
class C8yDefaultPactRecord {
constructor(request, response, options, auth, createdObject, modifiedResponse) {
this.request = request;
this.response = response;
if (options)
this.options = options;
if (auth)
this.auth = auth;
if (createdObject)
this.createdObject = createdObject;
if (modifiedResponse)
this.modifiedResponse = modifiedResponse;
if (request?.method?.toLowerCase() === "post") {
const newId = getCreatedObjectId(response);
if (newId) {
this.createdObject = newId;
}
}
}
/**
* Creates a C8yPactRecord from a Cypress.Response or an C8yPactRecord object.
* @param obj The Cypress.Response<any> or C8yPactRecord object.
* @param client The C8yClient for options and auth information.
*/
static from(obj, auth, client) {
// if (obj == null) return obj;
if ("request" in obj && "response" in obj) {
return new C8yDefaultPactRecord(_$2.get(obj, "request"), _$2.get(obj, "response"), _$2.get(obj, "options") || {}, _$2.get(obj, "auth"), _$2.get(obj, "createdObject"), _$2.get(obj, "modifiedResponse"));
}
const r = _$2.cloneDeep(obj);
return new C8yDefaultPactRecord(toPactRequest(r) || {}, toPactResponse(r) || {}, client?._options, isAuthOptions(auth) || isPactAuthObject(auth)
? toPactAuthObject(auth)
: client?._auth
? toPactAuthObject(client?._auth)
: undefined);
}
/**
* Returns the date of the response.
*/
date() {
const date = _$2.get(this.response, "headers.date");
if ((date && _$2.isString(date)) || _$2.isNumber(date) || _$2.isDate(date)) {
const result = new Date(date);
if (!isNaN(result.getTime())) {
return result;
}
}
return null;
}
/**
* Converts the C8yPactRecord to a Cypress.Response object.
*/
toCypressResponse() {
const result = _$2.cloneDeep(this.response);
_$2.extend(result, {
...(result.status && {
isOkStatusCode: result.status > 199 && result.status < 300,
}),
...(this.request.headers && {
requestHeaders: Object.fromEntries(Object.entries(this.request.headers || [])),
}),
...(this.request.url && { url: this.request.url }),
...(result.allRequestResponses && { allRequestResponses: [] }),
...(this.request.body && { requestBody: this.request.body }),
method: this.request.method || this.response.method || "GET",
});
return result;
}
hasRequestHeader(key) {
return Object.keys(this.request.headers ?? {})
.map((k) => k.toLowerCase())
.includes(key?.toLowerCase());
}
authType() {
const type = this.auth?.type;
if (type === "BasicAuth" || type === "CookieAuth") {
return type;
}
if (this.hasRequestHeader("x-xsrf-token")) {
return "CookieAuth";
}
if (this.hasRequestHeader("authorization")) {
return "BasicAuth";
}
return undefined;
}
}
function createPactRecord(response, client, options = {}) {
let auth = undefined;
const envUser = options.loggedInUser;
const envAlias = options.loggedInUserAlias;
const envType = options.authType;
const envAuth = {
...(envUser && { user: envUser }),
...(envAlias && { userAlias: envAlias }),
...(envAlias && { type: envType ?? "CookieAuth" }),
};
if (client?._auth) {
// do not pick the password. passwords must not be stored in the pact.
auth = _$2.defaultsDeep(client._auth, envAuth);
if (client._auth.constructor != null) {
if (!auth) {
auth = { type: client._auth.constructor.name };
}
else {
auth.type = client._auth.constructor.name;
}
}
}
if (!auth && (envUser || envAlias)) {
auth = envAuth;
}
// only store properties that need to be exposed. do not store password.
auth = auth ? toPactAuthObject(auth) : auth;
return C8yDefaultPactRecord.from(response, auth, client);
}
/**
* Default implementation of C8yPact. Use C8yDefaultPact.from to create a C8yPact from
* a Cypress.Response object, a serialized pact as string or an object implementing the
* C8yPact interface. Note, objects implementing the C8yPact interface may not provide
* all required functions and properties.
*/
class C8yDefaultPact {
constructor(records, info, id) {
this.recordIndex = 0;
this.iteratorIndex = 0;
this.requestIndexMap = {};
this.records = records;
this.info = info;
this.id = id;
}
/**
* Creates a C8yPact from a Cypress.Response object, a serialized pact as string
* or an object containing the pact records and info object. Throws an error if
* the input can not be converted to a C8yPact.
* @param obj The Cypress.Response, string or object to create a pact from.
* @param info The C8yPactInfo object containing additional information for the pact.
* @param client The optional C8yClient for options and auth information.
*/
static from(...args) {
const obj = args[0];
if (!obj) {
throw new Error("Can not create pact from null or undefined.");
}
if (isCypressResponse(obj)) {
const info = args && args.length > 1 ? args[1] : undefined;
if (!info) {
throw new Error(`Can not create pact from response without C8yPactInfo.`);
}
const client = args[2];
const r = _$2.cloneDeep(obj);
const pactRecord = new C8yDefaultPactRecord(toPactRequest(r) || {}, toPactResponse(r) || {}, client?._options, client?._auth ? toPactAuthObject(client?._auth) : undefined);
removeBaseUrlFromRequestUrl(pactRecord, info.baseUrl);
return new C8yDefaultPact([pactRecord], info, info.id);
}
else {
let pact;
if (_$2.isString(obj)) {
pact = JSON.parse(obj);
}
else if (_$2.isObjectLike(obj)) {
pact = obj;
}
else {
throw new Error(`Can not create pact from ${typeof obj}.`);
}
// required to map the record object to a C8yPactRecord here as this can
// not be done in the plugin
pact.records = pact.records?.map((record) => {
return new C8yDefaultPactRecord(record.request, record.response, record.options || {}, record.auth, record.createdObject);
});
const result = new C8yDefaultPact(pact.records, pact.info, pact.id);
if (!isPact(result)) {
throw new Error(`Invalid pact object. Can not create pact from ${typeof obj}.`);
}
return result;
}
}
clearRecords() {
this.records = [];
this.requestIndexMap = {};
this.recordIndex = 0;
this.iteratorIndex = 0;
}
appendRecord(record, skipIfExists = false) {
if (skipIfExists) {
if (!record.request.url)
return false;
const matches = this.getRecordsMatchingRequest(record.request);
if (matches && !_$2.isEmpty(matches))
return false;
}
this.records.push(record);
return true;
}
replaceRecord(record) {
const key = this.indexMapKey(record.request, this.info.baseUrl);
if (!key)
return false;
const matches = this.getRecordsMatchingRequest(record.request);
if (!matches) {
this.appendRecord(record);
}
else {
const currentIndex = Math.max(0, this.getIndexForKey(key));
const match = matches[currentIndex];
if (!match) {
this.appendRecord(record);
}
else {
const index = this.records.indexOf(match);
if (index >= 0) {
this.records[index] = record;
this.setIndexForKey(key, currentIndex + 1);
}
else {
return false;
}
}
}
return true;
}
/**
* Returns the next pact record or null if no more records are available.
*/
nextRecord() {
if (this.recordIndex >= this.records.length) {
return null;
}
return this.records[this.recordIndex++];
}
nextRecordMatchingRequest(request, baseUrl) {
if (!request?.url)
return null;
const key = this.indexMapKey(request, baseUrl);
if (!key)
return null;
const matches = this.getRecordsMatchingRequest(request);
if (!matches)
return null;
const currentIndex = Math.max(0, this.getIndexForKey(key));
const result = matches[Math.min(currentIndex, matches.length - 1)];
this.requestIndexMap[key] = currentIndex + 1;
return result;
}
getIndexForKey(key) {
return this.requestIndexMap[key] || -1;
}
setIndexForKey(key, index) {
this.requestIndexMap[key] = index;
}
indexMapKey(request, baseUrl) {
if (!request.url)
return undefined;
const url = this.normalizeUrl(request.url, undefined, baseUrl);
const method = _$2.lowerCase(request.method || "get");
return `${method}:${url}`;
}
normalizeUrl(url, parametersToRemove, baseUrl) {
const urlObj = isURL(url)
? url
: new URL(decodeURIComponent(url), this.info.baseUrl);
const p = parametersToRemove ||
this.info.requestMatching?.ignoreUrlParameters ||
[];
p.forEach((name) => {
urlObj.searchParams.delete(name);
});
if (!baseUrl) {
return decodeURIComponent(urlObj.pathname + urlObj.search + urlObj.hash);
}
return decodeURIComponent(urlObj.toString()?.replace(this.info.baseUrl, "")?.replace(baseUrl, ""));
}
matchUrls(url1, url2, baseUrl) {
if (!url1 || !url2)
return false;
const ignoreParameters = this.info.requestMatching?.ignoreUrlParameters || [];
const n1 = this.normalizeUrl(url1, ignoreParameters, baseUrl);
const n2 = this.normalizeUrl(url2, ignoreParameters, baseUrl);
return _$2.isEqual(n1, n2);
}
// debugging and test purposes only
getRequesIndex(key) {
return this.requestIndexMap[key] || 0;
}
/**
* Returns the pact record for the given request or null if no record is found.
* Currently only url and method are used for matching.
* @param req The request to use for matching.
*/
getRecordsMatchingRequest(req, baseUrl) {
const records = this.records.filter((record) => {
return (record.request?.url &&
req.url &&
this.matchUrls(record.request.url, req.url, baseUrl) &&
(req.method != null
? _$2.lowerCase(req.method) === _$2.lowerCase(record.request.method)
: true));
});
return records.length ? records : null;
}
/**
* Returns an iterator for the pact records to iterate records using `for (const record of pact) {...}`.
*/
[Symbol.iterator]() {
return {
next: () => {
if (this.iteratorIndex < this.records.length) {
return { value: this.records[this.iteratorIndex++], done: false };
}
else {
return { value: null, done: true };
}
},
};
}
}
function toSerializablePactRecord(response, options = {}) {
const recordOptions = {
loggedInUser: options?.loggedInUser,
loggedInUserAlias: options?.loggedInUserAlias,
authType: options?.authType,
};
const record = createPactRecord(response, options?.client, recordOptions);
removeBaseUrlFromRequestUrl(record, options.baseUrl);
if (options?.modifiedResponse &&
isCypressResponse(options?.modifiedResponse)) {
const modifiedPactRecord = createPactRecord(options.modifiedResponse, options?.client, recordOptions);
record.modifiedResponse = modifiedPactRecord.response;
}
options?.preprocessor?.apply(record);
return record;
}
async function toPactSerializableObject(response, info, options = {}) {
if (options.baseUrl == null) {
options.baseUrl = info.baseUrl;
}
const record = toSerializablePactRecord(response, options);
const pact = new C8yDefaultPact([record], info, info.id);
const keysToSave = ["id", "info", "records"];
try {
await Promise.all(pact.records
.filter((record_1) => record_1.response.body &&
!record_1.response.$body &&
_$2.isObjectLike(record_1.response.body))
.map((record_2) => options?.schemaGenerator
?.generate(record_2.response.body, { name: "body" })
.then((schema) => {
record_2.response.$body = schema;
return record_2;
})));
return { ..._$2.pick(pact, keysToSave) };
}
catch (error) {
console.error(error);
return { ..._$2.pick(pact, keysToSave) };
}
}
/**
* 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 matchSchemaAndObject = options?.matchSchemaAndObject ??
C8yDefaultPactMatcher.matchSchemaAndObject;
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 && _$2.isPlainObject(obj1) && _$2.isPlainObject(obj2)
? [_$2.pick(obj1, [key]), _$2.pick(obj2, [key])]
: [obj1, obj2];
}
throw newErr;
};
const keyPath = (k) => {
return `${[...parents, ...(k ? [k] : [])].join(" > ")}`;
};
const isArrayOfPrimitives = (value) => {
if (!_$2.isArray(value)) {
return false;
}
const primitiveTypes = ["undefined", "boolean", "number", "string"];
return (value.filter((p) => primitiveTypes.includes(typeof p)).length ===
value.length);
};
if (_$2.isString(obj1) && _$2.isString(obj2) && !_$2.isEqual(obj1, obj2)) {
throwPactError(`"${keyPath()}" text did not match.`);
}
if (!_$2.isObject(obj1) || !_$2.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 = matchSchemaAndObject === true
? Object.keys(obj2)
: Object.keys(obj2).reduce((acc, key) => {
if (!schemaKeys.includes(`$${key}`)) {
acc.push(key);
}
return acc;
}, []);
if (_$2.isEmpty(objectKeys) && _$2.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 = _$2.get(strictMatching || isSchema ? obj1 : obj2, removeSchemaPrefix(key));
let pact = _$2.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);
}
if (!matchSchemaAndObject) {
continue;
}
pact = _$2.get(strictMatching ? obj2 : obj1, key);
}
if (this.getPropertyMatcher(key, options?.ignoreCase) != null) {
if (!strictMatching && !value) {
continue;
}
try {
const result = this.getPropertyMatcher(key, options?.ignoreCase)?.match(value, pact, _$2.extend(options, { parents: [...parents, key] }));
if (!result)
throw new Error("");
}
catch (error) {
// calling match recursively requires to pass the root error
if (_$2.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 = _$2.difference(v[0], v[1]);
if (_$2.isEmpty(diff)) {
continue;
}
else {
throwPactError(`Array with key "${keyPath(key)}" has unexpected values "${diff}".`, key);
}
}
else if (_$2.isArray(value) && _$2.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], _$2.extend(options, { parents: [...parents, key, `${i}`] }));
}
}
else if (_$2.isObjectLike(value) && _$2.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, _$2.extend(options, { parents: [...parents, key] }));
}
else {
if (value != null && pact != null && !_$2.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 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];
}
}
C8yDefaultPactMatcher.matchSchemaAndObject = false;
/**
* 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 (_$2.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 (!_$2.isNumber(n) || _$2.isNaN(n)) {
throw new Error(`Value "${obj1}" is not a number.`);
}
});
return true;
}
}
class C8yStringMatcher {
match(obj1, obj2) {
[obj1, obj2].forEach((s) => {
if (!_$2.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 (!_$2.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);
}
}
const C8yPactPreprocessorDefaultOptions = {
ignore: [
"request.headers.accept-encoding",
"response.headers.cache-control",
"response.headers.content-length",
"response.headers.content-encoding",
"response.headers.transfer-encoding",
"response.headers.keep-alive",
],
obfuscate: [
"request.headers.cookie.authorization",
"request.headers.cookie.XSRF-TOKEN",
"request.headers.authorization",
"request.headers.X-XSRF-TOKEN",
"response.headers.set-cookie.authorization",
"response.headers.set-cookie.XSRF-TOKEN",
"response.body.password",
"response.body.users.password",
],
obfuscationPattern: "****",
ignoreCase: true,
};
/**
* Default implementation of C8yPactPreprocessor. Preprocessor for C8yPact objects
* that can be used to obfuscate or remove sensitive data from the pact objects.
* Use C8ypactPreprocessorOptions to configure the preprocessor.
*
* Removes cookies and set-cookie headers by appending the key to the `cookie` or `set-cookie`
* key as for example `headers.cookie.authorization` or `headers.set-cookie.authorization`.
*/
class C8yDefaultPactPreprocessor {
constructor(options) {
this.reservedKeys = ["id", "pact", "info", "records"];
this.options = this.resolveOptions(options);
}
apply(obj, options) {
if (!obj || !_$2.isObjectLike(obj))
return;
const objs = "records" in obj ? _$2.get(obj, "records") : [obj];
if (!_$2.isArray(objs))
return;
const o = this.resolveOptions(options);
const ignoreCase = o.ignoreCase;
const obfuscationPattern = o.obfuscationPattern;
const mapSensitiveKeys = (mapObject, keys) => keys.map((k) => ignoreCase === true ? toSensitiveObjectKeyPath(mapObject, k) ?? k : k);
objs.forEach((obj) => {
if (o?.pick != null) {
const keepPaths = [];
if (_$2.isPlainObject(o.pick)) {
Object.entries(o.pick ?? {}).forEach(([parentKey, childKeys]) => {
if (_$2.isEmpty(childKeys))
keepPaths.push(parentKey);
childKeys.forEach((childKey) => {
keepPaths.push(`${parentKey}.${childKey}`);
});
});
this.filterObjectByKeepPaths(obj, keepPaths, ignoreCase);
}
else if (_$2.isArray(o.pick)) {
this.applyKeepArray(obj, o.pick);
}
}
if (o?.regexReplace != null) {
Object.entries(o.regexReplace ?? {}).forEach(([key, value]) => {
const patterns = Array.isArray(value) ? value : [value];
const keysToReplace = mapSensitiveKeys(obj, [key]);
keysToReplace.forEach((k) => {
let v = _$2.get(obj, k);
if (v != null) {
// Apply each regex pattern in sequence
for (const pattern of patterns) {
try {
const regex = parseRegexReplace(pattern);
v = performRegexReplace(v, regex);
}
catch {
// ignore invalid regex
}
}
_$2.set(obj, k, v);
}
});
});
}
const keysToObfuscate = mapSensitiveKeys(obj, o.obfuscate ?? []);
this.handleObfuscation(obj, keysToObfuscate, obfuscationPattern);
const keysToRemove = mapSensitiveKeys(obj, o.ignore ?? []);
this.handleRemoval(obj, keysToRemove);
});
}
filterObjectByKeepPaths(obj, keepPaths, ignoreCase = false) {
const prepKey = (key) => key != null && ignoreCase === true ? key.toLowerCase() : key;
const shouldKeep = (keyPath) => {
return keepPaths
.map((k) => prepKey(k))
.some((keepPath) => prepKey(keyPath) === keepPath ||
keepPath?.startsWith(`${prepKey(keyPath)}.`));
};
const recursiveFilter = (currentObj, currentPath) => {
if (!_$2.isObject(currentObj))
return;
Object.keys(currentObj).forEach((key) => {
const fullPath = currentPath ? `${currentPath}.${key}` : key;
if (!shouldKeep(fullPath)) {
_$2.unset(obj, fullPath);
}
else if (!keepPaths.includes(fullPath)) {
recursiveFilter(_$2.get(currentObj, key), fullPath);
}
});
};
recursiveFilter(obj, "");
}
applyKeepArray(obj, keep) {
if (keep == null || _$2.isEmpty(keep))
return;
if (_$2.isObjectLike(obj)) {
const keysToRemove = Object.keys(obj).filter((childKey) => !keep.includes(childKey.toLowerCase()));
keysToRemove.forEach((childKey) => {
_$2.unset(obj, childKey);
});
}
}
handleObfuscation(obj, keysToObfuscate, obfuscationPattern) {
const validKeys = this.filterValidKeys(obj, keysToObfuscate);
validKeys.forEach((key) => {
this.obfuscateKey(obj, key, obfuscationPattern);
});
}
handleRemoval(obj, keysToRemove) {
const validKeys = this.filterValidKeys(obj, keysToRemove);
validKeys.forEach((key) => {
this.removeKey(obj, key);
});
}
removeKey(obj, key) {
const keyPath = key.split(".");
if (this.hasKey(keyPath, "set-cookie")) {
this.removeSetCookie(obj, keyPath);
}
else if (this.hasKey(keyPath, "cookie")) {
this.removeCookie(obj, keyPath);
}
else {
const processKeyPath = (currentObj, remainingKeyParts) => {
if (!currentObj || remainingKeyParts.length === 0)
return;
const [_currentKey, ...restKeys] = remainingKeyParts;
const currentKey = toSensitiveObjectKeyPath(currentObj, _currentKey) ?? _currentKey;
const target = _$2.get(currentObj, currentKey);
if (_$2.isArray(target)) {
// If the current key points to an array, process each element
target.forEach((item) => processKeyPath(item, restKeys));
}
else if (restKeys.length === 0) {
_$2.unset(currentObj, currentKey);
}
else {
processKeyPath(target, restKeys);
}
};
processKeyPath(obj, keyPath);
}
}
removeSetCookie(obj, keyParts) {
const { name, keyPath, cookieHeader } = this.getCookieObject(obj, keyParts);
if (!cookieHeader)
return;
if (!name) {
_$2.unset(obj, keyPath);
return;
}
const cookies = setCookieParser__namespace.parse(cookieHeader, { decodeValues: false }) ?? [];
if (cookies.length) {
const filteredCookies = cookies
.filter((cookie) => cookie.name.toLowerCase() !== name.toLowerCase())
.map((cookie) => libCookie__namespace.serialize(cookie.name, cookie.value, cookie));
if (filteredCookies.length === 0) {
_$2.unset(obj, keyPath);
}
else {
_$2.set(obj, keyPath, filteredCookies);