UNPKG

cumulocity-cypress

Version:
1,269 lines (1,252 loc) 159 kB
'use strict'; var _ = require('lodash'); var require$$1 = require('util'); var require$$2 = require('node:os'); var express = require('express'); var require$$4 = require('raw-body'); var require$$5 = require('cookie-parser'); var winston = require('winston'); var morgan = require('morgan'); var setCookieParser = require('set-cookie-parser'); var libCookie = require('cookie'); require('@c8y/client'); var datefns = require('date-fns'); var require$$12 = require('cross-fetch'); var require$$13 = 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'); var glob = require('glob'); function commonjsRequire(path) { throw new Error('Could not dynamically require "' + path + '". Please configure the dynamicRequireTargets or/and ignoreDynamicRequires option of @rollup/plugin-commonjs appropriately for this require call to work.'); } var c8yctrl = {}; var hasRequiredC8yctrl; function requireC8yctrl () { if (hasRequiredC8yctrl) return c8yctrl; hasRequiredC8yctrl = 1; var _$3 = _; var util = require$$1; var os = require$$2; var express$1 = express; var getRawBody = require$$4; var cookieParser = require$$5; var winston$1 = winston; var morgan$1 = morgan; var setCookieParser$1 = setCookieParser; var libCookie$1 = libCookie; var datefns$1 = datefns; var fetch = require$$12; var httpProxyMiddleware = require$$13; var fs$1 = fs; var path$1 = path; var semver$1 = semver; var swaggerUi$1 = swaggerUi; var yaml$1 = yaml; var debug$1 = debug; var glob$1 = glob; 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(_$3); var setCookieParser__namespace = /*#__PURE__*/_interopNamespaceDefault(setCookieParser$1); var libCookie__namespace = /*#__PURE__*/_interopNamespaceDefault(libCookie$1); var datefns__namespace = /*#__PURE__*/_interopNamespaceDefault(datefns$1); var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs$1); var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path$1); var semver__namespace = /*#__PURE__*/_interopNamespaceDefault(semver$1); var yaml__namespace = /*#__PURE__*/_interopNamespaceDefault(yaml$1); var glob__namespace = /*#__PURE__*/_interopNamespaceDefault(glob$1); function isURL(obj) { return obj instanceof URL; } function removeBaseUrlFromString(url, baseUrl) { if (!url || !baseUrl) { return url; } let normalizedBaseUrl = _$3.clone(baseUrl); while (normalizedBaseUrl.endsWith("/")) { normalizedBaseUrl = normalizedBaseUrl.slice(0, -1); } let result = url.replace(normalizedBaseUrl, ""); if (_$3.isEmpty(result)) { result = "/"; } return result; } function removeBaseUrlFromRequestUrl(record, baseUrl) { if (!record?.request?.url || !baseUrl || !_$3.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 || !_$3.isString(url) || _$3.isEmpty(url)) return false; return /^https?:\/\//i.test(url); } /** * Converts the given URL to a string. * @param url The URL or RequestInfo to convert. * @returns The URL as a string. */ function toUrlString(url) { if (_$3.isString(url)) { return url; } else if (url instanceof URL) { return url.toString(); } else if (url instanceof Request) { return url.url; } else { throw new Error(`Type for URL not supported. Expected URL, string or Request, but found $'{typeof 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. * * @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 inputStr = _$3.isArray(path) ? null : path; const keys = _$3.isArray(path) ? path.filter((k) => !_$3.isEmpty(k)) : path.split(/[.[\]]/g).filter((k) => !_$3.isEmpty(k)); let current = obj; const resolved = []; for (const key of keys) { if (current === null || current === undefined) return undefined; if (_$3.isArray(current)) { const index = parseInt(key); if (!isNaN(index)) { if (index >= 0 && index < current.length) { resolved.push(key); current = current[index]; } else { return undefined; // index out of bounds } } else if (current.length > 0 && _$3.isString(current[0])) { const matchedIndex = current.findIndex((item) => _$3.isString(item) && item.toLowerCase() === key.toLowerCase()); if (matchedIndex !== -1) { resolved.push(String(matchedIndex)); current = current[matchedIndex]; } else { return undefined; } } else if (current.length > 0 && _$3.isObjectLike(current[0])) { // For arrays of objects, resolve case through the first element so // the caller gets the correctly-cased key without needing an index. const matchingKey = Object.keys(current[0]).find((k) => k.toLowerCase() === key.toLowerCase()); if (matchingKey !== undefined) { resolved.push(matchingKey); current = current[0][matchingKey]; } else { return undefined; } } else { return undefined; } continue; } if (_$3.isObjectLike(current)) { const matchingKey = Object.keys(current).find((k) => k.toLowerCase() === key.toLowerCase()); if (matchingKey !== undefined) { resolved.push(matchingKey); current = current[matchingKey]; } else { return undefined; } } else { return undefined; } } // Fast path: array input or no brackets in input — plain dot-joined output if (!inputStr || !inputStr.includes("[")) return resolved.join("."); // Mirror bracket vs. dot notation from the input when building the output. // Walk the original string in parallel with the resolved keys: wherever the // input had `[key]` we emit `[resolvedKey]`, otherwise `.resolvedKey`. let result = ""; let pos = 0; for (let i = 0; i < resolved.length; i++) { // skip separators (dot after a `]`, or the `]` itself) while (pos < inputStr.length && (inputStr[pos] === "." || inputStr[pos] === "]")) pos++; const useBracket = inputStr[pos] === "["; if (useBracket) pos++; // skip `[` // skip past the key characters in the input while (pos < inputStr.length && inputStr[pos] !== "." && inputStr[pos] !== "[" && inputStr[pos] !== "]") pos++; if (i === 0) result = resolved[i]; else result += useBracket ? `[${resolved[i]}]` : `.${resolved[i]}`; } return result; } /** * 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. * * This function supports deep access to cookie and set-cookie headers, e.g. * `requestHeaders.cookie.authorization`. Cookie headers are parsed and the value * of the specified cookie is returned. If the cookie is not found, undefined is returned. * * @example * get_i(obj, "obj.key.token") * get_i(obj, ["obj", "key", "token"]) * get_i(obj, "obj.key[0].token") * get_i(obj, "obj.key.0.token") * get_i(obj, "requestHeaders.cookie.authorization") * get_i(obj, "requestHeaders.set-cookie.authorization") * * @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; // Handle case where obj itself is an array of strings with a single key lookup const keys = _$3.isArray(keyPath) ? keyPath.filter((k) => !_$3.isEmpty(k)) : keyPath.split(/[.[\]]/g).filter((k) => !_$3.isEmpty(k)); if (keys.length === 1 && _$3.isArray(obj) && obj.length > 0 && _$3.isString(obj[0])) { const matchedString = obj.find((item) => _$3.isString(item) && item.toLowerCase() === keys[0].toLowerCase()); if (matchedString !== undefined) { return matchedString; } } const sensitivePath = toSensitiveObjectKeyPath(obj, keyPath); let direct = undefined; // Try direct access first if we have a valid path if (sensitivePath != null) { direct = _$3.get(obj, sensitivePath); if (direct !== undefined) return direct; } // Handle cookie and set-cookie deep access, e.g. requestHeaders.cookie.authorization if (!keys || keys.length === 0) return undefined; const indexOfKey = (arr, val) => arr.findIndex((k) => k.toLowerCase() === val.toLowerCase()); const cookieIdx = indexOfKey(keys, "cookie"); const setCookieIdx = indexOfKey(keys, "set-cookie"); // Helper to resolve the real path up to a certain index (inclusive) const resolvePathUpTo = (idx) => { const part = keys.slice(0, idx + 1); return toSensitiveObjectKeyPath(obj, part) ?? part.join("."); }; // requestHeaders.cookie.<name> if (cookieIdx >= 0) { const parentPath = resolvePathUpTo(cookieIdx); const cookieHeader = parentPath ? _$3.get(obj, parentPath) : undefined; const cookieName = keys[cookieIdx + 1]; if (cookieHeader == null) return undefined; if (!cookieName) return cookieHeader; // return full header if no name // Parse Cookie header string into key/value if (_$3.isString(cookieHeader)) { const parsed = libCookie__namespace.parse(cookieHeader); const matchKey = Object.keys(parsed).find((k) => k.toLowerCase() === cookieName.toLowerCase()); return matchKey ? parsed[matchKey] : undefined; } return undefined; } // headers.set-cookie.<name> if (setCookieIdx >= 0) { const parentPath = resolvePathUpTo(setCookieIdx); const setCookieHeader = parentPath ? _$3.get(obj, parentPath) : undefined; const cookieName = keys[setCookieIdx + 1]; if (setCookieHeader == null) return undefined; if (!cookieName) return setCookieHeader; // return full header if no name // Parse Set-Cookie header (array or string) const headerInput = _$3.isString(setCookieHeader) ? setCookieParser__namespace.splitCookiesString(setCookieHeader) : setCookieHeader; const cookies = setCookieParser__namespace.parse(headerInput, { decodeValues: false, }); const found = (cookies || []).find((c) => c?.name?.toLowerCase() === cookieName.toLowerCase()); return found?.value; } // Handle arrays of strings with case-insensitive matching // For paths like "headers.authorization" where headers is ["Content-Type", "Authorization"] for (let i = 0; i < keys.length; i++) { const parentPath = resolvePathUpTo(i); const parentValue = parentPath ? _$3.get(obj, parentPath) : undefined; if (_$3.isArray(parentValue) && parentValue.length > 0 && _$3.isString(parentValue[0])) { const searchKey = keys[i + 1]; if (searchKey) { const index = parseInt(searchKey); if (isNaN(index)) { // Non-numeric key, try to find case-insensitive match in string array const matchedString = parentValue.find((item) => _$3.isString(item) && item.toLowerCase() === searchKey.toLowerCase()); // Only return a match when this segment is the final path segment if (matchedString !== undefined && i + 1 === keys.length - 1) { return matchedString; } } } } } return direct; } /** * 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 || !_$3.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 _$2 = _$3 || ___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 || !_$2.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) => _$2.words(_$2.deburr(v), /[a-zA-Z0-9_-]+/g).join("_")) .join(suiteSeparator); if (value != null && _$2.isArray(value)) { result = value.map((v) => normalize(v)).join(suiteSeparator); } else if (value != null && _$2.isString(value)) { result = normalize(value); } if (result == null || _$2.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 (!_$2.isString(mode) || _$2.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 (!_$2.isString(mode) || _$2.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 (_$2.isObjectLike(obj) && "info" in obj && _$2.isObjectLike(_$2.get(obj, "info")) && "records" in obj && _$2.isArray(_$2.get(obj, "records")) && _$2.every(_$2.get(obj, "records"), isPactRecord) && _$2.isFunction(_$2.get(obj, "nextRecord")) && _$2.isFunction(_$2.get(obj, "nextRecordMatchingRequest")) && _$2.isFunction(_$2.get(obj, "appendRecord")) && _$2.isFunction(_$2.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 (_$2.isObjectLike(obj) && "request" in obj && _$2.isObjectLike(_$2.get(obj, "request")) && "response" in obj && _$2.isObjectLike(_$2.get(obj, "response")) && _$2.isFunction(_$2.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 (_$2.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 _$2.isError(error) && _$2.get(error, "name") === "C8yPactError"; } function isDefined(value) { return !_$2.isUndefined(value); } /** * Converts a Cypress.Response to a C8yPactRequest. */ function toPactRequest(response) { if (!response) return response; const result = _$2.pickBy(_$2.mapKeys(_$2.pick(response, ["url", "method", "requestHeaders", "requestBody"]), (v, k) => { if (_$2.isEqual(k, "requestHeaders")) return "headers"; if (_$2.isEqual(k, "requestBody")) return "body"; return k; }), isDefined); if (_$2.isEmpty(result)) return undefined; return result; } /** * Converts a Cypress.Response to a C8yPactResponse. */ function toPactResponse(response) { if (!response) return response; const result = _$2.pickBy(_$2.pick(response, [ "status", "statusText", "body", "headers", "duration", "isOkStatusCode", "allRequestResponses", "$body", ]), isDefined); if (_$2.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 = _$2.camelCase(name).replace(/^c8Y/i, "c8y"); const camelCasedPlainName = _$2.camelCase(plainName); return (getForName(name) || getForName(camelCasedName) || getForName(plainName) || getForName(camelCasedPlainName)); } function isOneOfStrings(value, values) { if (!_$2.isString(value) || _$2.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 (_$3.isObjectLike(obj) && (("user" in obj && "password" in obj) || "token" in obj)); } // map from case insensitive auth type to C8yAuthOptionType function getAuthType(auth) { const type = _$3.isString(auth) ? auth.toLowerCase() : auth?.type?.toLowerCase(); if (type === "bearerauth") { return "BearerAuth"; } if (type === "basicauth") { return "BasicAuth"; } if (type === "cookieauth") { return "CookieAuth"; } return undefined; } function toPactAuthObject(obj) { return _$3.pick(obj, C8yPactAuthObjectKeys); } function isPactAuthObject(obj) { return (_$3.isObjectLike(obj) && ("user" in obj || "token" in obj) && ("userAlias" in obj || "type" in obj || "token" in obj) && Object.keys(obj).every((key) => ["token", ...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(requestOrParams, response, options, auth, createdObject, modifiedResponse, id) { // Handle object parameter style if (isC8yDefaultPactRecordInit(requestOrParams)) { const params = requestOrParams; this.request = params.request; this.response = params.response; if (params.options) this.options = params.options; if (params.auth) this.auth = params.auth; if (params.createdObject) this.createdObject = params.createdObject; if (params.modifiedResponse) this.modifiedResponse = params.modifiedResponse; if (params.id) this.id = params.id; } else { // Handle individual parameter style this.request = requestOrParams; this.response = response; if (options) this.options = options; if (auth) this.auth = auth; if (createdObject) this.createdObject = createdObject; if (modifiedResponse) this.modifiedResponse = modifiedResponse; if (id) this.id = id; } if (this.request?.method?.toLowerCase() === "post") { const newId = getCreatedObjectId(this.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 auth The auth information to use. * @param client The C8yClient for options and auth information. * @param id The optional ID for the pact record. */ static from(obj, auth, client, id) { // if (obj == null) return obj; if ("request" in obj && "response" in obj) { return new C8yDefaultPactRecord(_$3.get(obj, "request"), _$3.get(obj, "response"), _$3.get(obj, "options") || {}, _$3.get(obj, "auth"), _$3.get(obj, "createdObject"), _$3.get(obj, "modifiedResponse"), id || _$3.get(obj, "id")); } const r = _$3.cloneDeep(obj); return new C8yDefaultPactRecord(toPactRequest(r) || {}, toPactResponse(r) || {}, client?._options, isAuthOptions(auth) || isPactAuthObject(auth) ? toPactAuthObject(auth) : client?._auth ? toPactAuthObject(client?._auth) : undefined, undefined, undefined, id || _$3.get(obj, "id")); } /** * Returns the date of the response. */ date() { const date = _$3.get(this.response, "headers.date"); if ((date && _$3.isString(date)) || _$3.isNumber(date) || _$3.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 = _$3.cloneDeep(this.response); _$3.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 = getAuthType(this.auth); if (type != null) 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 = _$3.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, options.id); } /** * Type guard to check if an object is C8yDefaultPactRecordInit */ function isC8yDefaultPactRecordInit(obj) { return (obj && typeof obj === "object" && "request" in obj && "response" in obj && obj.request != null && obj.response != null); } /** * 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 = _$3.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 (_$3.isString(obj)) { pact = JSON.parse(obj); } else if (_$3.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 && !_$3.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. * If an id is provided, the record is looked up by requestId or record id * and the cursor is advanced to the position after the matched record. * If no id is provided, the next record by sequential index is returned. */ nextRecord(id) { if (id) { const matches = this.records.filter((r) => r.options?.requestId === id || r.id === id); if (!matches.length) return null; const currentIndex = Math.max(0, this.getIndexForKey(id)); const result = matches[Math.min(currentIndex, matches.length - 1)]; this.requestIndexMap[id] = currentIndex + 1; const recordsIndex = this.records.indexOf(result); if (recordsIndex >= 0) { this.recordIndex = recordsIndex + 1; } return result; } if (this.recordIndex >= this.records.length) { return null; } return this.records[this.recordIndex++]; } currentRecordIndex() { return 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 = _$3.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 _$3.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 ? _$3.lowerCase(req.method) === _$3.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; } const matchingProperties = ["request", "response"]; const p = _$3.pick(record, matchingProperties); options?.preprocessor?.apply(p); if (p.request == null) { p.request = {}; } if (p.response == null) { p.response = {}; } const result = { ...p, ..._$3.omit(record, matchingProperties) }; return result; } 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"]; return { ..._$3.pick(pact, keysToSave) }; } /** * Error thrown when a C8yPactMatcher fails to match two objects. * Contains the actual and expected values, the key that failed to match and * the key path of the property that failed to match. * The key path is a string representation of the path to the property that failed to match. * For example: "body > id" for a property "id" in the "body" object. * This error is used to provide detailed information about the match failure. */ class C8yPactMatchError extends Error { constructor(message, options) { super(message); this.name = "C8yPactMatchError"; this.actual = options.actual; this.expected = options.expected; this.key = options.key; this.keyPath = options.keyPath; this.schema = options.schema; if (Error.captureStackTrace) { Error.captureStackTrace(this, C8yPactMatchError); } } } /** * 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(), }, options) { this.propertyMatchers = {}; this.propertyMatchers = propertyMatchers; this.options = options; } match(obj1, obj2, options) { if (obj1 === obj2) return true; options = _$3.defaults({}, options, this.options, C8yDefaultPactMatcher.options); const parents = options?.parents ?? []; const strictMatching = options?.strictMatching ?? false; const ignorePrimitiveArrayOrder = options?.ignorePrimitiveArrayOrder ?? true; const matchSchemaAndObject = options?.matchSchemaAndObject ?? false; const schemaMatcher = options?.schemaMatcher || C8yDefaultPactMatcher.schemaMatcher; const addLoggerProps = (props, message, key) => { if (options?.loggerProps) { options.loggerProps.error = message; options.loggerProps.key = key; options.loggerProps.keypath = keyPath(key); options.loggerProps.objects = key && _$3.isPlainObject(obj1) && _$3.isPlainObject(obj2) ? [_$3.pick(obj1, [key]), _$3.pick(obj2, [key])] : [obj1, obj2]; } }; const throwPactError = (message, key) => { const newErr = new C8yPactMatchError(`Pact validation failed${options?.requestId ? ` for request ${options.requestId}` : ""}! ${message}`, { actual: obj1, expected: obj2, ...(key != null ? { key, keyPath: keyPath(key) } : {}), }); addLoggerProps(options?.loggerProps, newErr.message, key); throw newErr; }; const throwSchemaError = (message, key, schema, value) => { const newErr = new C8yPactMatchError(`Pact validation failed${options?.requestId ? ` for request ${options.requestId}` : ""}! ${message}`, { actual: value ?? obj1, expected: schema ?? obj2, key, keyPath: keyPath(key), schema: schema, }); addLoggerProps(options?.loggerProps, newErr.message, key); throw newErr; }; const keyPath = (k) => { if (_$3.isArray(k)) { const segments = k.map((segment) => segment.toString()); return segments.join(" > "); } return `${[...parents, ...(k ? [k] : [])].join(" > ")}`; }; const isArrayOfPrimitivesOrNull = (value) => { if (!_$3.isArray(value)) { return false; } const primitiveTypes = ["undefined", "boolean", "number", "string"]; return (value.filter((p) => primitiveTypes.includes(typeof p) || p === null) .length === value.length); }; const matchArraysOfPrimitives = (value, pact, parents) => { if (value.length !== pact.length) { throwPactError(`Arrays with key "${keyPath(parents)}" have different lengths.`, keyPath(parents)); } const diff = []; const sortedValue = ignorePrimitiveArrayOrder ? [...value].sort() : [...value]; const sortedPact = ignorePrimitiveArrayOrder ? [...pact].sort() : [...pact]; for (let i = 0; i < sortedValue.length; i++) { if (i >= sortedValue.length || i >= sortedPact.length || sortedValue[i] !== sortedPact[i]) { diff.push(i); } } if (diff.length === 0) { return; } else { throwPactError(`Arrays with key "${keyPath(parents)}" have mismatches at indices "${diff}".`, keyPath(parents)); } }; if (_$3.isString(obj1) && _$3.isString(obj2) && !_$3.isEqual(obj1, obj2)) { throwPactError(`"${keyPath()}" text did not match.`); } if (!_$3.isObject(obj1) || !_$3.isObject(obj2)) { throwPactError(`Expected 2 objects as input for matching, but got "${typeof obj1}" and ${typeof obj2}".`); } if (_$3.isArray(obj1) && _$3.isArray(obj2)) { if (obj1.length !== obj2.length) { throwPactError(`Arrays at "${_$3.isEmpty(parents) ? "root" : keyPath()}" have different lengths.`); } } if (_$3.isArray(obj1) !== _$3.isArray(obj2)) { throwPactError(`Type mismatch at "${_$3.isEmpty(parents) ? "root" : keyPath()}". Expected ${_$3.isArray(obj2) ? "array" : "object"} but got ${_$3.isArray(obj1) ? "array" : "object"}.`); } // get keys of objects without schema keys and schema keys separately const objectKeys = Object.keys(obj1).filter((k) => !this.isSchemaMatcherKey(k)); const schemaKeys = Object.keys(obj2).filter((k) => this.isSchemaMatcherKey(k)); // 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 (_$3.isEmpty(objectKeys) && _$3.isEmpty(pactKeys)) { return true; } const removeSchemaPrefix = (key) => this.isSchemaMatcherKey(key) ? key.slice(1) : key; const findActualKey = (obj, keyToFind) => { if (!options?.ignoreCase) return keyToFind; if (obj == null || !_$3.isObject(obj)) return keyToFind; const actualKey = Object.keys(obj).find((k) => k.toLowerCase() === keyToFind.toLowerCase()); return actualKey ?? keyToFind; }; // 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 === false ? pactKeys : objectKeys; // When strictMatching is enabled, also ensure every pact key is present in the response. // The main loop (iterating objectKeys) only catches extra response keys not in the pact; // this extra pass catches pact keys that are absent from the response. if (strictMatching === true) { for (const pactKey of pactKeys) {