cumulocity-cypress
Version:
Cypress commands for Cumulocity IoT
350 lines (349 loc) • 13.8 kB
JavaScript
import _ from "lodash";
import * as setCookieParser from "set-cookie-parser";
import * as libCookie from "cookie";
export 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;
}
export function sanitizeStringifiedObject(obj) {
if (!_.isString(obj)) {
return obj;
}
const regex = /((?:"password"|'password'|password|"token"|'token'|token)\s*:\s*["']?)(.*?)(["']|,|\s|}|$)/gi;
return obj.replace(regex, "$1***$3");
}
/**
* 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
*/
export function toSensitiveObjectKeyPath(obj, path) {
if (!obj)
return undefined;
const inputStr = _.isArray(path) ? null : path;
const keys = _.isArray(path)
? path.filter((k) => !_.isEmpty(k))
: path.split(/[.[\]]/g).filter((k) => !_.isEmpty(k));
let current = obj;
const resolved = [];
for (const key of keys) {
if (current === null || current === undefined)
return undefined;
if (_.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 && _.isString(current[0])) {
const matchedIndex = current.findIndex((item) => _.isString(item) && item.toLowerCase() === key.toLowerCase());
if (matchedIndex !== -1) {
resolved.push(String(matchedIndex));
current = current[matchedIndex];
}
else {
return undefined;
}
}
else if (current.length > 0 && _.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 (_.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
*/
export 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 = _.isArray(keyPath)
? keyPath.filter((k) => !_.isEmpty(k))
: keyPath.split(/[.[\]]/g).filter((k) => !_.isEmpty(k));
if (keys.length === 1 && _.isArray(obj) && obj.length > 0 && _.isString(obj[0])) {
const matchedString = obj.find((item) => _.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 = _.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 ? _.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 (_.isString(cookieHeader)) {
const parsed = libCookie.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
? _.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 = _.isString(setCookieHeader)
? setCookieParser.splitCookiesString(setCookieHeader)
: setCookieHeader;
const cookies = setCookieParser.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 ? _.get(obj, parentPath) : undefined;
if (_.isArray(parentValue) && parentValue.length > 0 && _.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) => _.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;
}
/**
* Returns the shortest unique prefixes for the given words. The prefixes are
* unique in the sense that they are not prefixes of any other word in the list.
*
* @param words The list of words to find the prefixes for.
* @returns The list of shortest unique prefixes.
*/
export function shortestUniquePrefixes(words) {
class TrieNode {
constructor() {
this.children = new Map();
this.isEndOfWord = false;
this.count = 0;
}
}
const insertWord = (root, word) => {
let currentNode = root;
for (let i = 0; i < word.length; i++) {
const char = word[i];
if (!currentNode?.children.has(char)) {
currentNode?.children.set(char, new TrieNode());
}
currentNode = currentNode?.children.get(char);
if (currentNode) {
currentNode.count++;
}
}
if (currentNode) {
currentNode.isEndOfWord = true;
}
};
const root = new TrieNode();
const prefixes = [];
// Build the trie with all words
for (const word of words) {
insertWord(root, word);
}
// Find the shortest unique prefix for each word
for (const word of words) {
if (word.length === 0) {
prefixes.push("");
continue;
}
let currentNode = root;
let prefix = "";
let foundUniquePrefix = false;
for (let i = 0; i < word.length; i++) {
const char = word[i];
prefix += char;
currentNode = currentNode?.children.get(char);
// If this node has a count of 1, it means this prefix is unique
if (currentNode && currentNode.count === 1) {
prefixes.push(prefix);
foundUniquePrefix = true;
break;
}
}
// If no unique prefix is found, use the entire word
if (!foundUniquePrefix) {
prefixes.push(word);
}
}
return prefixes;
}
export function getLastDefinedValue(data, index) {
const value = _.findLast(data.slice(0, index + 1), (item) => !_.isUndefined(item));
if (value !== undefined) {
return value;
}
return undefined;
}
/**
* Converts a value to an array. If the value is an array, it is returned as is.
* @param value The value to convert to an array
* @returns The value as an array if it is not already an array
*/
export function to_array(value) {
if (value == null)
return undefined;
if (_.isArray(value))
return value;
return [value];
}
/**
* 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
*/
export function to_boolean(input, defaultValue) {
if (input == null || !_.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;
}
export function buildTestHierarchy(objects, hierarchyfn) {
const tree = {};
objects.forEach((item) => {
const titles = hierarchyfn(item);
if (titles) {
let currentNode = tree;
const protectedKeys = ["__proto__", "constructor", "prototype"];
titles?.forEach((title, index) => {
if (!protectedKeys.includes(title)) {
if (!currentNode[title]) {
currentNode[title] = index === titles.length - 1 ? item : {};
}
currentNode = currentNode[title];
}
});
}
});
return tree;
}