cumulocity-cypress
Version:
Cypress commands for Cumulocity IoT
1,288 lines (1,278 loc) • 268 kB
JavaScript
'use strict';
var path = require('path');
var fs = require('fs');
var debug = require('debug');
var odiffBin = require('odiff-bin');
var WebSocket = require('ws');
var chokidar = require('chokidar');
var fetch = require('cross-fetch');
var glob = require('glob');
var _$4 = require('lodash');
var setCookieParser = require('set-cookie-parser');
var libCookie = require('cookie');
var yaml = require('yaml');
var util = require('util');
var os = require('node:os');
var express = require('express');
var getRawBody = require('raw-body');
var cookieParser = require('cookie-parser');
var winston = require('winston');
var morgan = require('morgan');
require('@c8y/client');
require('date-fns');
var httpProxyMiddleware = require('http-proxy-middleware');
var semver = require('semver');
var swaggerUi = require('swagger-ui-express');
var Ajv = require('ajv');
var addFormats = require('ajv-formats');
require('ajv/lib/refs/json-schema-draft-06.json');
var $RefParser = require('@apidevtools/json-schema-ref-parser');
var url = require('url');
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 path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
var glob__namespace = /*#__PURE__*/_interopNamespaceDefault(glob);
var ___namespace = /*#__PURE__*/_interopNamespaceDefault(_$4);
var setCookieParser__namespace = /*#__PURE__*/_interopNamespaceDefault(setCookieParser);
var libCookie__namespace = /*#__PURE__*/_interopNamespaceDefault(libCookie);
var yaml__namespace = /*#__PURE__*/_interopNamespaceDefault(yaml);
var semver__namespace = /*#__PURE__*/_interopNamespaceDefault(semver);
function isURL(obj) {
return obj instanceof URL;
}
function removeBaseUrlFromString(url, baseUrl) {
if (!url || !baseUrl) {
return url;
}
let normalizedBaseUrl = _$4.clone(baseUrl);
while (normalizedBaseUrl.endsWith("/")) {
normalizedBaseUrl = normalizedBaseUrl.slice(0, -1);
}
let result = url.replace(normalizedBaseUrl, "");
if (_$4.isEmpty(result)) {
result = "/";
}
return result;
}
function removeBaseUrlFromRequestUrl(record, baseUrl) {
if (!record?.request?.url || !baseUrl || !_$4.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 || !_$4.isString(url) || _$4.isEmpty(url))
return false;
return /^https?:\/\//i.test(url);
}
/**
* Validates the base URL and throws an error if the base URL is not an absolute URL. This
* is required as commands expect an absolute URL as baseUrl. Will not fail for undefined values.
* `Cypress.config().baseUrl` is validated by Cypress itself and throw an error.
*
* @param baseUrl The url to validate.
*/
function validateBaseUrl(baseUrl) {
if (baseUrl != null && !isAbsoluteURL(baseUrl)) {
const error = new Error(`Invalid value for base url. '${baseUrl}' must be an absolute URL or undefined.`);
error.name = "C8yPactError";
throw error;
}
}
/**
* Normalizes a URL to ensure it has a protocol and proper trailing slash.
* If no protocol is present, HTTPS is added by default.
* If the URL has no path component, a trailing slash is appended.
*
* @param url - The URL string to normalize
* @returns The normalized URL with HTTPS protocol and trailing slash if appropriate, or undefined for invalid input
*/
function normalizeBaseUrl(url) {
if (!url || !_$4.isString(url)) {
return undefined;
}
const trimmedUrl = url.trim();
if (!trimmedUrl) {
return undefined;
}
let normalizedUrl;
// Check if URL already has a protocol
if (/^https?:\/\//i.test(trimmedUrl)) {
normalizedUrl = trimmedUrl;
}
else {
// Add https:// if no protocol is present
normalizedUrl = `https://${trimmedUrl}`;
}
try {
const urlObj = new URL(normalizedUrl);
// remove all components other than protocol, host
normalizedUrl = `${urlObj.protocol}//${urlObj.host}`;
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to normalize base url ${url}. ${errorMessage}`);
}
return normalizedUrl;
}
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 = _$4.isArray(path) ? null : path;
const keys = _$4.isArray(path)
? path.filter((k) => !_$4.isEmpty(k))
: path.split(/[.[\]]/g).filter((k) => !_$4.isEmpty(k));
let current = obj;
const resolved = [];
for (const key of keys) {
if (current === null || current === undefined)
return undefined;
if (_$4.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 && _$4.isString(current[0])) {
const matchedIndex = current.findIndex((item) => _$4.isString(item) && item.toLowerCase() === key.toLowerCase());
if (matchedIndex !== -1) {
resolved.push(String(matchedIndex));
current = current[matchedIndex];
}
else {
return undefined;
}
}
else if (current.length > 0 && _$4.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 (_$4.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 = _$4.isArray(keyPath)
? keyPath.filter((k) => !_$4.isEmpty(k))
: keyPath.split(/[.[\]]/g).filter((k) => !_$4.isEmpty(k));
if (keys.length === 1 && _$4.isArray(obj) && obj.length > 0 && _$4.isString(obj[0])) {
const matchedString = obj.find((item) => _$4.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 = _$4.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 ? _$4.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 (_$4.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
? _$4.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 = _$4.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 ? _$4.get(obj, parentPath) : undefined;
if (_$4.isArray(parentValue) && parentValue.length > 0 && _$4.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) => _$4.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 || !_$4.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 _$3 = _$4 || ___namespace;
const C8yPactModeValues = [
"record",
"recording",
"apply",
"forward",
"disabled",
"mock",
];
const C8yPactRecordingModeValues = [
"refresh",
"append",
"new",
"replace",
];
const C8yPactObjectKeys = ["records", "info", "id"];
/**
* 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) => _$3.words(_$3.deburr(v), /[a-zA-Z0-9_-]+/g).join("_"))
.join(suiteSeparator);
if (value != null && _$3.isArray(value)) {
result = value.map((v) => normalize(v)).join(suiteSeparator);
}
else if (value != null && _$3.isString(value)) {
result = normalize(value);
}
if (result == null || _$3.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 (!_$3.isString(mode) ||
_$3.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;
}
}
}
/**
* 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 (_$3.isObjectLike(obj) &&
"info" in obj &&
_$3.isObjectLike(_$3.get(obj, "info")) &&
"records" in obj &&
_$3.isArray(_$3.get(obj, "records")) &&
_$3.every(_$3.get(obj, "records"), isPactRecord) &&
_$3.isFunction(_$3.get(obj, "nextRecord")) &&
_$3.isFunction(_$3.get(obj, "nextRecordMatchingRequest")) &&
_$3.isFunction(_$3.get(obj, "appendRecord")) &&
_$3.isFunction(_$3.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 (_$3.isObjectLike(obj) &&
"request" in obj &&
_$3.isObjectLike(_$3.get(obj, "request")) &&
"response" in obj &&
_$3.isObjectLike(_$3.get(obj, "response")) &&
_$3.isFunction(_$3.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 (_$3.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));
}
function isDefined(value) {
return !_$3.isUndefined(value);
}
/**
* Converts a Cypress.Response to a C8yPactRequest.
*/
function toPactRequest(response) {
if (!response)
return response;
const result = _$3.pickBy(_$3.mapKeys(_$3.pick(response, ["url", "method", "requestHeaders", "requestBody"]), (v, k) => {
if (_$3.isEqual(k, "requestHeaders"))
return "headers";
if (_$3.isEqual(k, "requestBody"))
return "body";
return k;
}), isDefined);
if (_$3.isEmpty(result))
return undefined;
return result;
}
/**
* Converts a Cypress.Response to a C8yPactResponse.
*/
function toPactResponse(response) {
if (!response)
return response;
const result = _$3.pickBy(_$3.pick(response, [
"status",
"statusText",
"body",
"headers",
"duration",
"isOkStatusCode",
"allRequestResponses",
"$body",
]), isDefined);
if (_$3.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 = _$3.camelCase(name).replace(/^c8Y/i, "c8y");
const camelCasedPlainName = _$3.camelCase(plainName);
return (getForName(name) ||
getForName(camelCasedName) ||
getForName(plainName) ||
getForName(camelCasedPlainName));
}
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;
}
const _$2 = _$4 || ___namespace;
/**
* Default implementation of C8yPactFileAdapter which loads and saves C8yPact objects
* Provide location of the files using folder option. Default location is
* cypress/fixtures/c8ypact folder.
*
* This adapter supports loading of JSON and YAML pact files (.json, .yaml, .yml). When
* saviing pact files, it saves them as JSON files (.json).
*
* By using C8yPactAdapterOptions you can enable loading of JavaScript pact files (.js, .cjs).
* Use with caution, as this can lead to security issues if the files are not trusted.
*/
class C8yPactDefaultFileAdapter {
/**
* Creates an instance of C8yPactDefaultFileAdapter.
*
* @param folder - The folder where pact files are stored. Can be an absolute or relative path.
* @param options - Optional configuration for the adapter.
* @param options.enableJavaScript - If true, enables loading of JavaScript pact files (.js, .cjs). Defaults to false.
*/
constructor(folder, options) {
this.fileExtension = "json";
this.folder = path__namespace.isAbsolute(folder)
? folder
: this.toAbsolutePath(folder);
this.enabledExtensions = [`.${this.fileExtension}`, ".yaml", ".yml"];
if (options?.enableJavaScript) {
this.enabledExtensions.push(".js", ".cjs");
}
this.id = options?.id || "fileadapter";
this.log = debug(`c8y:${this.id}`);
}
description() {
return `C8yPactDefaultFileAdapter: ${this.folder}`;
}
getFolder() {
return this.folder;
}
loadPacts() {
const pactObjects = this.loadPactObjects();
this.log(`loadPacts() - ${pactObjects.length} pact files from ${this.folder}`);
return pactObjects.reduce((acc, obj) => {
if (!obj?.info?.id)
return acc;
acc[obj.info.id] = obj;
return acc;
}, {});
}
loadPact(id) {
this.log(`loadPact() - ${id}`);
const pId = pactId(id);
if (pId == null) {
this.log(`loadPact() - invalid pact id ${id} -> ${pId}`);
return null;
}
if (!this.folder || !fs__namespace.existsSync(this.folder)) {
this.log(`loadPact() - folder ${this.folder} does not exist`);
return null;
}
// Try to find the file with different supported extensions
const extensions = this.enabledExtensions;
let loadedPact = null;
for (const ext of extensions) {
const file = path__namespace.join(this.folder, `${pId}${ext}`);
if (fs__namespace.existsSync(file)) {
try {
loadedPact = this.loadPactFromFile(file);
if (loadedPact) {
this.log(`loadPact() - ${file} loaded successfully`);
return loadedPact;
}
}
catch (error) {
this.log(`loadPact() - error loading ${file}: ${error}`);
}
}
}
this.log(`loadPact() - no valid pact file found for id ${pId}`);
return null;
}
pactExists(id) {
const pId = pactId(id);
return this.enabledExtensions.some((ext) => fs__namespace.existsSync(path__namespace.join(this.folder, `${pId}${ext}`)));
}
savePact(pact) {
this.createFolderRecursive(this.folder);
const pId = pactId(pact.id);
if (pId == null) {
this.log(`savePact() - invalid pact id ${pact.id} -> ${pId}`);
return;
}
const file = path__namespace.join(this.folder, `${pId}.${this.fileExtension}`);
this.log(`savePact() - write ${file} (${pact.records?.length || 0} records)`);
try {
fs__namespace.writeFileSync(file, safeStringify({
id: pact.id,
info: pact.info,
records: pact.records,
}, 2), "utf-8");
}
catch (error) {
console.error(`Failed to save pact.`, error);
}
}
deletePact(id) {
const pId = pactId(id);
if (pId == null) {
this.log(`deletePact() - invalid pact id ${id} -> ${pId}`);
return;
}
const filePath = path__namespace.join(this.folder, `${pId}.${this.fileExtension}`);
if (fs__namespace.existsSync(filePath)) {
fs__namespace.unlinkSync(filePath);
this.log(`deletePact() - deleted ${filePath}`);
}
else {
this.log(`deletePact() - ${filePath} does not exist`);
}
}
readPactFiles() {
this.log(`readPactFiles() - ${this.folder}`);
if (!this.folder || !fs__namespace.existsSync(this.folder)) {
this.log(`readPactFiles() - ${this.folder} does not exist`);
return [];
}
const pacts = this.loadPactObjects();
return pacts.map((pact) => {
return safeStringify(pact);
});
}
/**
* @deprecated Use readPactFiles() instead.
*/
readJsonFiles() {
this.log(`readJsonFiles() - ${this.folder}`);
if (!this.folder || !fs__namespace.existsSync(this.folder)) {
this.log(`readJsonFiles() - ${this.folder} does not exist`);
return [];
}
const jsonFiles = glob__namespace.sync(path__namespace.join(this.folder, `*.${this.fileExtension}`));
this.log(`readJsonFiles() - reading ${jsonFiles.length} ${this.fileExtension} files from ${this.folder}`);
const pacts = jsonFiles.map((file) => {
return fs__namespace.readFileSync(file, "utf-8");
});
return pacts;
}
deleteJsonFiles() {
if (!this.folder || !fs__namespace.existsSync(this.folder)) {
this.log(`deleteJsonFiles() - ${this.folder} does not exist`);
return;
}
const jsonFiles = glob__namespace.sync(path__namespace.join(this.folder, `*.${this.fileExtension}`));
this.log(`deleteJsonFiles() - deleting ${jsonFiles.length} ${this.fileExtension} files from ${this.folder}`);
jsonFiles.forEach((file) => {
fs__namespace.unlinkSync(file);
});
}
loadPactObjects() {
this.log(`loadPactObjects() - ${this.folder}`);
if (!this.folder || !fs__namespace.existsSync(this.folder)) {
this.log(`loadPactObjects() - ${this.folder} does not exist`);
return [];
}
// Find all files with supported extensions
const combinedPattern = path__namespace.join(this.folder, `*{${this.enabledExtensions.join(",")}}`);
const allFiles = glob__namespace.sync(combinedPattern);
this.log(`loadPactObjects() - reading ${allFiles.length} files from ${this.folder}`);
// Load and parse each file based on its extension
const pactObjects = allFiles
.map((file) => {
try {
return this.loadPactFromFile(file);
}
catch (error) {
this.log(`loadPactObjects() - error loading ${file}: ${error}`);
return null;
}
})
.filter(Boolean);
this.log(`loadPactObjects() - loaded ${pactObjects.length} valid pact objects`);
return pactObjects;
}
loadPactFromFile(filePath) {
if (!fs__namespace.existsSync(filePath)) {
this.log(`loadPactFromFile() - file does not exist: ${filePath}`);
return null;
}
const extension = path__namespace.extname(filePath).toLowerCase();
// Check if the extension is enabled
if (!this.enabledExtensions.includes(extension)) {
this.log(`loadPactFromFile() - file extension ${extension} is not supported or enabled for loading: ${filePath}`);
return null;
}
const content = fs__namespace.readFileSync(filePath, "utf-8");
try {
// Handle different file formats
if (extension === `.${this.fileExtension}`) {
// Load JSON file
return JSON.parse(content);
}
else if (extension === ".yaml" || extension === ".yml") {
// Load YAML file
return yaml__namespace.parse(content);
}
else if (extension === ".js" || extension === ".cjs") {
// CommonJS modules (.js, .cjs) can use require
const absolutePath = path__namespace.isAbsolute(filePath)
? filePath
: path__namespace.resolve(process.cwd(), filePath);
try {
// Clear cache if needed
if (require.cache && require.cache[require.resolve(absolutePath)]) {
delete require.cache[require.resolve(absolutePath)];
}
// eslint-disable-next-line @typescript-eslint/no-require-imports
const pactModule = require(absolutePath);
return pactModule.default || pactModule;
}
catch (error) {
this.log(`loadPactFromFile() - error loading ${extension} file ${absolutePath}: ${error}`);
}
}
}
catch (error) {
this.log(`loadPactFromFile() - error parsing file ${filePath}: ${error}`);
}
return null;
}
createFolderRecursive(f) {
this.log(`createFolderRecursive() - ${f}`);
if (!f || !_$2.isString(f))
return undefined;
const absolutePath = !path__namespace.isAbsolute(f) ? this.toAbsolutePath(f) : f;
if (f !== absolutePath) {
this.log(`createFolderRecursive() - resolved ${f} to ${absolutePath}`);
}
if (fs__namespace.existsSync(f))
return undefined;
const result = fs__namespace.mkdirSync(absolutePath, { recursive: true });
if (result) {
this.log(`createFolderRecursive() - created ${absolutePath}`);
}
return result;
}
toAbsolutePath(f) {
return path__namespace.isAbsolute(f) ? f : path__namespace.resolve(process.cwd(), f);
}
isNodeError(error, type) {
return error instanceof type;
}
}
/**
* C8yPactHARFileAdapter converts between C8yPact format and HAR (HTTP Archive) format.
* This allows using external HAR tooling with C8yPact recordings.
*
* This adapter extends C8yPactDefaultFileAdapter to reuse folder management and utility
* methods, but only supports .har file extension for reading and writing.
*
* When saving, pacts are converted to HAR format. When loading, HAR files are converted
* back to C8yPact format. Some metadata may be stored in the comment fields to preserve
* C8yPact-specific information.
*/
class C8yPactHARFileAdapter extends C8yPactDefaultFileAdapter {
constructor(folder) {
// Call parent constructor without JavaScript support
super(folder, { enableJavaScript: false, id: "harfileadapter" });
this.id = "harfileadapter";
// Override enabled extensions to only support HAR files
this.fileExtension = "har";
this.enabledExtensions = [`.${this.fileExtension}`];
}
description() {
return `C8yPactHarFileAdapter: ${this.folder}`;
}
savePact(pact) {
this.createFolderRecursive(this.folder);
const pId = pactId(pact.id);
if (pId == null) {
this.log(`savePact() - invalid pact id ${pact.id} -> ${pId}`);
return;
}
const file = path__namespace.join(this.folder, `${pId}.${this.fileExtension}`);
this.log(`savePact() - write ${file} (${pact.records?.length || 0} records)`);
try {
const har = this.pactToHAR(pact);
fs__namespace.writeFileSync(file, safeStringify(har, 2), "utf-8");
}
catch (error) {
console.error(`Failed to save pact as HAR.`, error);
}
}
/**
* Override parent's loadPactFromFile to handle HAR format conversion.
* This is called by parent's loadPactObjects for each .har file found.
*/
loadPactFromFile(filePath) {
if (!fs__namespace.existsSync(filePath)) {
this.log(`loadPactFromFile() - file does not exist: ${filePath}`);
return null;
}
const extension = path__namespace.extname(filePath).toLowerCase();
// Only handle .har files
if (extension !== `.${this.fileExtension}`) {
this.log(`loadPactFromFile() - file extension ${extension} is not supported: ${filePath}`);
return null;
}
try {
const harContent = fs__namespace.readFileSync(filePath, "utf-8");
const har = JSON.parse(harContent);
const filename = path__namespace.basename(filePath, `.${this.fileExtension}`);
const pact = this.harToPact(har, filename);
if (pact) {
this.log(`loadPactFromFile() - ${filePath} loaded successfully`);
}
return pact;
}
catch (error) {
this.log(`loadPactFromFile() - error loading ${filePath}: ${error}`);
return null;
}
}
/**
* Override parent's loadPactObjects to use simpler glob pattern for .har files.
* The parent's brace expansion pattern doesn't work well with single extensions.
*/
loadPactObjects() {
this.log(`loadPactObjects() - ${this.folder}`);
if (!this.folder || !fs__namespace.existsSync(this.folder)) {
this.log(`loadPactObjects() - ${this.folder} does not exist`);
return [];
}
const harFiles = glob__namespace.sync(path__namespace.join(this.folder, `*.${this.fileExtension}`));
this.log(`loadPactObjects() - reading ${harFiles.length} .${this.fileExtension} files from ${this.folder}`);
const pactObjects = harFiles
.map((file) => {
try {
return this.loadPactFromFile(file);
}
catch (error) {
this.log(`loadPactObjects() - error loading ${file}: ${error}`);
return null;
}
})
.filter(Boolean);
this.log(`loadPactObjects() - loaded ${pactObjects.length} valid pact objects`);
return pactObjects;
}
/**
* Convert a C8yPact object to HAR format
*/
pactToHAR(pact) {
const entries = (pact.records || []).map((record) => {
const request = record.request;
const response = record.response;
// Parse URL to extract query string parameters and ensure absolute URL
const requestUrl = request.url || "";
let absoluteUrl = requestUrl;
let queryString = [];
try {
// Parse URL with baseUrl to ensure it's absolute
const urlObj = new URL(requestUrl, pact.info?.baseUrl || "http://localhost");
absoluteUrl = urlObj.href;
queryString = Array.from(urlObj.searchParams.entries()).map(([name, value]) => ({ name, value }));
}
catch {
// If URL parsing fails, try to make it absolute if it starts with /
if (requestUrl.startsWith("/") && pact.info?.baseUrl) {
try {
const baseUrl = pact.info.baseUrl.replace(/\/$/, "");
absoluteUrl = baseUrl + requestUrl;
}
catch {
// Keep original URL if all fails
}
}
}
// Convert headers from object to HAR format
const requestHeaders = request.headers
? Object.entries(request.headers).flatMap(([name, value]) => {
if (Array.isArray(value)) {
return value.map((v) => ({ name, value: String(v) }));
}
return [{ name, value: String(value) }];
})
: [];
const responseHeaders = response.headers
? Object.entries(response.headers).flatMap(([name, value]) => {
if (Array.isArray(value)) {
return value.map((v) => ({ name, value: String(v) }));
}
return [{ name, value: String(value) }];
})
: [];
// Handle request body
let postData;
let requestBodySize = 0;
if (request.body != null || request.$body != null) {
const bodyData = request.$body || request.body;
const headers = request.headers;
const contentType = headers?.["content-type"] ||
headers?.["Content-Type"] ||
"application/json";
const bodyText = typeof bodyData === "string" ? bodyData : safeStringify(bodyData);
requestBodySize = bodyText ? bodyText.length : 0;
postData = {
mimeType: String(contentType),
text: bodyText,
};
}
// Handle response body
const responseBody = response.$body || response.body;
const respHeaders = response.headers;
const responseContentType = respHeaders?.["content-type"] ||
respHeaders?.["Content-Type"] ||
"application/json";
const responseText = typeof responseBody === "string"
? responseBody
: safeStringify(responseBody);
const responseContent = {
size: responseText ? responseText.length : 0,
mimeType: String(responseContentType),
text: responseText,
};
// Create the HAR entry with C8yPact metadata in comments
const entry = {
startedDateTime: new Date().toISOString(),
time: response.duration || 0,
request: {
method: String(request.method || "GET").toUpperCase(),
url: absoluteUrl,
httpVersion: "HTTP/1.1",
cookies: [],
headers: requestHeaders,
queryString: queryString,
postData: postData,
headersSize: -1,
bodySize: requestBodySize,
},
response: {
status: response.status || 200,
statusText: response.statusText || "",
httpVersion: "HTTP/1.1",
cookies: [],
headers: responseHeaders,
content: responseContent,
redirectURL: "",
headersSize: -1,
bodySize: responseContent.size,
},
cache: {},
timings: {
send: -1,
wait: response.duration || 0,
receive: -1,
},
comment: safeStringify({
c8ypact: {
id: record.id,
auth: record.auth,
options: record.options,
createdObject: record.createdObject,
},
}),
};
return entry;
});
const har = {
log: {
version: "1.2",
creator: {
name: pact.info?.producer
? typeof pact.info.producer === "string"
? pact.info.producer
: pact.info.producer.name
: "C8yPact",
version: pact.info?.version?.c8ypact || "1.0.0",
},
entries: entries,
comment: safeStringify({
c8ypact: {
id: pact.id,
info: {
...pact.info,
// Don't duplicate large fields that are in entries
},
},
}),
},
};
return har;
}
/**
* Convert a HAR format to C8yPact object
*/
harToPact(har, id) {
try {
// Extract C8yPact metadata from comment if available
let pactMetadata = {};
try {
if (har.log.comment) {
const parsed = JSON.parse(har.log.comment);
pactMetadata = parsed.c8ypact || {};
}
}
catch {
// Ignore comment parsing errors
}
const baseUrl = pactMetadata.info?.baseUrl;
const pactId = pactMetadata.id || id;
const records = har.log.entries.map((entry) => {
// Extract C8yPact metadata from entry comment if available
let recordMetadata = {};
try {
if (entry.comment) {
const parsed = JSON.parse(entry.comment);
recordMetadata = parsed.c8ypact || {};
}
}
catch {
// Ignore comment parsing errors
}
// Convert HAR headers to object format
const requestHeaders = {};
entry.request.headers.forEach((header) => {
requestHeaders[header.name] = header.value;
});
const responseHeaders = {};
entry.response.headers.forEach((header) => {
responseHeaders[header.name] = header.value;
});
// Parse request body
let requestBody;
if (entry.request.postData?.text) {
try {
// Try to parse as JSON
if (entry.request.postData.mimeType.includes("application/json")) {
requestBody = JSON.parse(entry.request.postData.text);
}
else {
requestBody = entry.request.postData.text;
}
}
catch {
requestBody = entry.request.postData.text;
}
}
// Parse response body
let responseBody;
if (entry.response.content.text) {
try {
// Try to parse as JSON
if (entry.response.content.mimeType.includes("application/json")) {
responseBody = JSON.parse(entry.response.content.text);
}
else {
responseBody = entry.response.content.text;
}
}
catch {
responseBody = entry.response.content.text;
}
}
return {
id: recordMetadata.id,
request: {
method: entry.request.method,
url: removeBaseUrlFromString(entry.request.url, baseUrl),
headers: requestHeaders,
body: requestBody,
},
response: {
status: entry.response.status,
statusText: entry.response.statusText,
headers: responseHeaders,
body: responseBody,
duration: entry.time,
isOkStatusCode: entry.response.status >= 200 && entry.response.status < 300,
},
auth: recordMetadata.auth,
options: recordMetadata.options,
createdObject: recordMetadata.createdObject,
};
});
// Reconstruct pact info from HAR metadata
const info = {
...pactMetadata.info,
id: pactId,
producer: {
name: har.log.creator.name,
version: har.log.creator.version,
},
version: { c8ypact: har.log.creator.version },
baseUrl: pactMetadata.info?.baseUrl || "",
};
const pact = {
id: pactId,
info: info,
records: records,
};
return pact;
}
catch (error) {
this.log(`harToPact() - error converting HAR to pact: ${error}`);
return null;
}
}
}
/// <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 (_$4.isObjectLike(obj) &&
(("user" in obj && "password" in obj) || "token" in obj));
}
// map from case insensitive auth type to C8yAuthOptionType
function getAuthType(auth) {
const type = _$4.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 _$4.pick(obj, C8yPactAuthObjectKeys);
}
function isPactAuthObject(obj) {
return (_$4.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;
}
/**
* Extracts the authentication options from a JWT token.
* @param jwtToken The JWT token to extract the authentication options from.
* @returns The extracted authentication options.
*/
function getAuthOptionsFromJWT(jwtToken) {
try {
const payload = JSON.parse(atob(jwtToken.split(".")[1]));
// Remove all characters not valid in JWT tokens (base64url: A-Z, a-z, 0-9, -, _, .)
const cleanedToken = jwtToken?.replace(/[^A-Za-z0-9\-_.]/g, "");
return {
token: cleanedToken,
xsrfToken: payload.xsrfToken,
tenant: payload.ten,
user: payload.sub,
baseUrl: normalizeBaseUrl(payload.aud ?? payload.iss),
};
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to decode JWT token: ${message}`);
}
}
/**
* 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