cumulocity-cypress
Version:
Cypress commands for Cumulocity IoT
1,269 lines (1,252 loc) • 159 kB
JavaScript
'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) {