UNPKG

cumulocity-cypress

Version:
1,362 lines (1,349 loc) 88.2 kB
'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