cumulocity-cypress
Version:
Cypress commands for Cumulocity IoT
1,362 lines (1,349 loc) • 88.2 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');
require('date-fns');
var setCookieParser = require('set-cookie-parser');
var libCookie = require('cookie');
var client = require('@c8y/client');
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 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);
/// <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",
];
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 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);
}
/// <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;
}
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;
}
/**
* 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 = response.body?.id;
if (newId) {
this.createdObject = 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);
this.createdObject = pathSegments.pop();
}
catch {
// do nothing
}
}
}
}
}
/**
* 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) };
}
}
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);
}
}
const keysToObfuscate = mapSensitiveKeys(obj, o.obfuscate ?? []);
const keysToRemove = mapSensitiveKeys(obj, o.ignore ?? []);
this.handleObfuscation(obj, keysToObfuscate, obfuscationPattern);
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);
}
}
}
removeCookie(obj, keyParts) {
const { name, keyPath, cookieHeader } = this.getCookieObject(obj, keyParts);
if (!cookieHeader)
return;
if (!name) {
_$2.unset(obj, keyPath);
return;
}
const cookies = libCookie__namespace.parse(cookieHeader);
delete cookies[name];
const remainingCookies = Object.entries(cookies);
if (remainingCookies.length === 0) {
_$2.unset(obj, keyPath);
}
else {
const v = remainingCookies
.map(([name, value]) => `${name}=${value}`)
.join("; ");
_$2.set(obj, keyPath, v);
}
}
filterValidKeys(obj, keys) {
return _$2.without(keys, ...this.reservedKeys);
}
obfuscateKey(obj, key, pattern) {
const keyParts = key.split(".");
const p = pattern ?? C8yDefaultPactPreprocessor.defaultObfuscationPattern;
if (this.hasKey(keyParts, "set-cookie")) {
this.obfuscateSetCookie(obj, keyParts, p);
}
else if (this.hasKey(keyParts, "cookie")) {
this.obfuscateCookie(obj, keyParts, p);
}
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) {
if (_$2.get(currentObj, currentKey) != null) {
_$2.set(currentObj, currentKey, p);
}
}
else {
processKeyPath(target, restKeys);
}
};
processKeyPath(obj, keyParts);
}
}
obfuscateSetCookie(obj, keyParts, obfuscationPattern) {
const { name, keyPath, cookieHeader } = this.getCookieObject(obj, keyParts);
if (!cookieHeader)
return;
const cookies = setCookieParser__namespace.parse(cookieHeader, { decodeValues: false }) ?? [];
if (cookies.length) {
const fixedCookies = cookies.reduce((acc, cookie) => {
const n = name?.toLowerCase();
const shouldObfuscate = !n || (n && n === cookie.name?.toLowerCase());
const cookieValue = shouldObfuscate
? obfuscationPattern ?? ""
: cookie.value;
acc.push(libCookie__namespace.serialize(cookie.name, cookieValue, cookie));
return acc;
}, []);
_$2.set(obj, keyPath, fixedCookies);
}
}
obfuscateCookie(obj, keyParts, obfuscationPattern) {
const { name, keyPath, cookieHeader } = this.getCookieObject(obj, keyParts);
if (!cookieHeader)
return;
const cookies = libCookie__namespace.parse(cookieHeader);
Object.keys(cookies).forEach((cookieName) => {
if (name != null && cookieName.toLowerCase() !== name.toLowerCase())
return;
cookies[cookieName] = obfuscationPattern;
});
const result = Object.entries(cookies)
.map(([n, v]) => `${n}=${v}`)
.join("; ");
_$2.set(obj, keyPath, result);
}
resolveOptions(options) {
return _$2.defaults(options, this.options, C8yPactPreprocessorDefaultOptions);
}
hasKey(keyPath, key) {
return ((_$2.isArray(keyPath) ? keyPath : keyPath.split(".")).filter((k) => k.toLowerCase() === key.toLowerCase()).length > 0);
}
getCookieObject(obj, keyParts) {
let name = undefined;
const l = _$2.last(keyParts)?.toLowerCase();
if (l !== "cookie" && l !== "set-cookie") {
name = _$2.last(keyParts);
keyParts = keyParts.slice(0, -1);
}
const keyPath = keyParts.join(".");
const cookieHeader = _$2.get(obj, keyPath);
return { name, keyPath, cookieHeader };
}
}
C8yDefaultPactPreprocessor.defaultObfuscationPattern = C8yPactPreprocessorDefaultOptions.obfuscationPattern;
/**
* 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 (_$2.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}}'.`);
}
}
/**
* Converts the given object to a Cypress.Response.
* @param obj The object to convert.
* @param duration The duration of the request.
* @param fetchOptions The fetch options used for the request.
* @param url The URL of the request.
* @param schema The schema of the response.
*/
function toCypressResponse$1(obj, duration = 0, fetchOptions = {}, url, schema) {
if (!obj)
return undefined;
if (typeof isPactRecord === "function" && isPactRecord(obj)) {
return obj.toCypressResponse();
}
let fetchResponse;
if (isIResult(obj)) {
fetchResponse = obj.res;
}
else if (isWindowFetchResponse(obj)) {
fetchResponse = obj;
}
else {
fetchResponse = obj;
}
if ("responseObj" in fetchResponse) {
return _$2.get(fetchResponse, "responseObj");
}
return {
status: fetchResponse.status,
isOkStatusCode: fetchResponse.ok ||
(fetchResponse.status > 199 && fetchResponse.status < 300),
statusText: fetchResponse.statusText,
headers: Object.fromEntries(fetchResponse.headers || []),
requestHeaders: fetchOptions.headers,
duration: duration,
...(url && { url: toUrlString(url) }),
allRequestResponses: [],
body: fetchResponse.data,
requestBody: fetchResponse.requestBody,
method: fetchResponse.method || "GET",
...(schema && { $body: schema }),
};
}
/**
* Checks if the given object is a window.Response.
* @param obj The object to check.
*/
function isWindowFetchResponse(obj) {
return (obj != null &&
_$2.isObjectLike(obj) &&
"status" in obj &&
"statusText" in obj &&
"headers" in obj &&
"body" in obj &&
"url" in obj &&
_$2.isFunction(_$2.get(obj, "json")) &&
_$2.isFunction(_$2.get(obj, "arrayBuffer")));
}
/**
* Checks if the given object is an IResult.
* @param obj The object to check.
*/
function isIResult(obj) {
return (obj != null &&
_$2.isObjectLike(obj) &&
"data" in obj &&
"res" in obj &&
isWindowFetchResponse(obj.res));
}
/**
* Checks if the given object is a CypressError.
* @param error The object to check.
* @returns True if the object is a CypressError, false otherwise.
*/
function isCypressError(error) {
return _$2.isError(error) && _$2.get(error, "name") === "CypressError";
}
function getAuthCookies(response) {
let setCookie = response.headers.getSetCookie;
let cookieHeader;
if (typeof response.headers.getSetCookie === "function") {
cookieHeader = response.headers.getSetCookie();
}
else {
if (typeof response.headers.get === "function") {
setCookie = response.headers.get("set-cookie");
if (_$2.isString(setCookie)) {
cookieHeader = setCookieParser__namespace.splitCookiesString(setCookie);
}
else if (_$2.isArrayLike(setCookie)) {
cookieHeader = setCookie;
}
}
else {
if (_$2.isPlainObject(response.headers)) {
cookieHeader = get_i(response.headers, "set-cookie");
}
}
}
if (!cookieHeader)
return undefined;
let authorization = undefined;
let xsrfToken = undefined;
setCookieParser__namespace.parse(cookieHeader || []).forEach((c) => {
if (_$2.isEqual(c.name.toLowerCase(), "authorization")) {
authorization = c.value;
}
if (_$2.isEqual(c.name.toLowerCase(), "xsrf-token")) {
xsrfToken = c.value;
}
});
// This method is intended for use on server environments (for example Node.js).
// Browsers block frontend JavaScript code from accessing the Set-Cookie header,
// as required by the Fetch spec, which defines Set-Cookie as a forbidden
// response-header name that must be filtered out from any response exposed to frontend code.
// https://developer.mozilla.org/en-US/docs/Web/API/Headers/getSetCookie
if (!authorization) {
authorization =
getCookieValue("authorization") || getCookieValue("Authorization");
if (_$2.isEmpty(authorization)) {
authorization = undefined;
}
}
if (!xsrfToken) {
xsrfToken = getCookieValue("XSRF-TOKEN") || getCookieValue("xsrf-token");
if (_$2.isEmpty(xsrfToken)) {
xsrfToken = undefined;
}
}
return { authorization, xsrfToken };
}
async function oauthLogin(auth, baseUrl) {
if (!auth || !auth.user || !auth.password) {
const error = new Error("Authentication required. oauthLogin requires user and password for authentication.");
error.name = "C8yPactError";
throw error;
}
if (!baseUrl) {
const error = new Error("Base URL required. oauthLogin requires absolute url for login.");
error.name = "C8yPactError";
throw error;
}
let tenant = auth.tenant;
if (!tenant) {
const fetchClient = new client.FetchClient(baseUrl);
const credentials = new client.BasicAuth(auth);
fetchClient.setAuth(credentials);
const res = await fetchClient.fetch("/tenant/currentTenant");
credentials.logout();
if (res.status !== 200) {
const error = new Error(`Getting tenant id failed for ${baseUrl} with status code ${res.status}. Use env variable or pass it as part of auth object.`);
error.name = "C8yPactError";
throw error;
}
const { name } = await res.json();
tenant = name;
}
const url = `/tenant/oauth?tenant_id=${tenant}`;
const params = new URLSearchParams({
grant_type: "PASSWORD",
username: auth.user || "",
password: auth.password || "",
...(auth.tfa && { tfa_code: auth.tfa }),
});
const fetchClient = new client.FetchClient(baseUrl);
const res = await fetchClient.fetch(url, {
method: "POST",
body: params.toString(),
headers: {
"content-type": "application/x-www-form-urlencoded;charset=UTF-8",
},
});
if (res.status !== 200) {
const error = new Error(`Logging in to ${baseUrl} failed for user "${auth.user}" with status code ${res.status}.`);
error.name = "C8yPactError";
throw error;
}
const cookies = getAuthCookies(res);
const { authorization, xsrfToken } = _$2.pick(cookies, [
"authorization",
"xsrfToken",
]);
auth = {
...auth,
...(authorization && { bearer: authorization }),
...(xsrfToken && { xsrfToken: xsrfToken }),
};
return auth;
}
// from c8y/client FetchClient
function getCookieValue(name) {
if (typeof document === "undefined")
return undefined;
const value = document.cookie.match("(^|;)\\s*" + name + "\\s*=\\s*([^;]+)");
return value ? value.pop() : "";
}
const C8yPactHttpControllerLogLevel = [
"info",
"debug",
"warn",
"error",
];
const C8yPactHttpControllerDefaultMode = "forward";
const C8yPactHttpControllerDefaultRecordingMode = "append";
function createMiddleware(c8yctrl, options = {}) {
const ignoredPaths = options.ignoredPaths || ["/c8yctrl"];
return wrapPathIgnoreHandler(httpProxyMiddleware.createProxyMiddleware({
target: options.baseUrl || c8yctrl.baseUrl,
changeOrigin: true,
cookieDomainRewrite: "",
selfHandleResponse: true,
logger: options.logger || c8yctrl.logger,
followRedirects: false,
on: {
proxyReq: createRequestHandler(c8yctrl, options.auth),
proxyRes: httpProxyMiddleware.responseInterceptor(createResponseInterceptor(c8yctrl, options.errorHandler)),
},
}), ignoredPaths);
}
/**
* Wraps a RequestHandler to ignore certain paths. For paths matching items in the
* `ignoredPaths` parameter, the handler will call `next()` immediately and not call
* the wrapped handler. For matching `startsWith` is used.
* @param handler The RequestHandler to wrap
* @param ignoredPaths The paths to ignore using exact match
* @returns The RequestHandler wrapper
*/
function wrapPathIgnoreHandler(handler, ignoredPaths) {
return (req, res, next) => {
if (ignoredPaths.filter((p) => req.path.startsWith(p)).length > 0) {
next();
}
else {
handler(req, res, next);
// disabled calling the handler in Promise.
// new Promise((resolve, reject) => {
// handler(req, res, (err) => (err ? reject(err) : resolve(null)));
// })
// .then(() => {
// next