cumulocity-cypress
Version:
Cypress commands for Cumulocity IoT
1,340 lines (1,328 loc) • 51.2 kB
JavaScript
'use strict';
var _$1 = require('lodash');
var setCookieParser = require('set-cookie-parser');
var libCookie = require('cookie');
var client = require('@c8y/client');
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(_$1);
var setCookieParser__namespace = /*#__PURE__*/_interopNamespaceDefault(setCookieParser);
var libCookie__namespace = /*#__PURE__*/_interopNamespaceDefault(libCookie);
/**
* 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 = _$1.isArray(path) ? null : path;
const keys = _$1.isArray(path)
? path.filter((k) => !_$1.isEmpty(k))
: path.split(/[.[\]]/g).filter((k) => !_$1.isEmpty(k));
let current = obj;
const resolved = [];
for (const key of keys) {
if (current === null || current === undefined)
return undefined;
if (_$1.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 && _$1.isString(current[0])) {
const matchedIndex = current.findIndex((item) => _$1.isString(item) && item.toLowerCase() === key.toLowerCase());
if (matchedIndex !== -1) {
resolved.push(String(matchedIndex));
current = current[matchedIndex];
}
else {
return undefined;
}
}
else if (current.length > 0 && _$1.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 (_$1.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 = _$1.isArray(keyPath)
? keyPath.filter((k) => !_$1.isEmpty(k))
: keyPath.split(/[.[\]]/g).filter((k) => !_$1.isEmpty(k));
if (keys.length === 1 && _$1.isArray(obj) && obj.length > 0 && _$1.isString(obj[0])) {
const matchedString = obj.find((item) => _$1.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 = _$1.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 ? _$1.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 (_$1.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
? _$1.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 = _$1.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 ? _$1.get(obj, parentPath) : undefined;
if (_$1.isArray(parentValue) && parentValue.length > 0 && _$1.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) => _$1.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 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
*/
function to_array(value) {
if (value == null)
return undefined;
if (_$1.isArray(value))
return value;
return [value];
}
function isURL(obj) {
return obj instanceof URL;
}
function relativeURL(url) {
try {
const u = isURL(url) ? url : new URL(url);
return u.pathname + u.search;
}
catch {
return undefined;
}
}
function urlForBaseUrl(baseUrl, relativeOrAbsoluteUrl) {
if (relativeOrAbsoluteUrl) {
try {
const url = new URL(relativeOrAbsoluteUrl, baseUrl);
return url.toString();
}
catch {
// no-op
}
}
else {
return baseUrl;
}
return relativeOrAbsoluteUrl;
}
function removeBaseUrlFromString(url, baseUrl) {
if (!url || !baseUrl) {
return url;
}
let normalizedBaseUrl = _$1.clone(baseUrl);
while (normalizedBaseUrl.endsWith("/")) {
normalizedBaseUrl = normalizedBaseUrl.slice(0, -1);
}
let result = url.replace(normalizedBaseUrl, "");
if (_$1.isEmpty(result)) {
result = "/";
}
return result;
}
function removeBaseUrlFromRequestUrl(record, baseUrl) {
if (!record?.request?.url || !baseUrl || !_$1.isString(baseUrl)) {
return;
}
record.request.url = removeBaseUrlFromString(record.request.url, baseUrl);
}
function normalizeUrl(url) {
return url.replace(/\/+$/, "");
}
function tenantUrl(baseUrl, tenant) {
if (!baseUrl || !tenant)
return undefined;
try {
const url = new URL(baseUrl);
const hostComponents = url.host.split(".");
if (hostComponents.length <= 2) {
url.host = `${tenant}.${hostComponents.join(".")}`;
}
else {
const instance = url.host.split(".")?.slice(1)?.join(".");
url.host = `${tenant}.${instance}`;
}
return normalizeUrl(url.toString());
}
catch {
// no-op
}
return undefined;
}
function updateURLs(value, from, to) {
if (!value || !from || !to)
return value;
let result = value;
const fromTenantUrl = tenantUrl(from.baseUrl, from.tenant);
const toTenantUrl = tenantUrl(to.baseUrl, to.tenant);
if (fromTenantUrl && toTenantUrl) {
result = result.replace(new RegExp(fromTenantUrl, "g"), toTenantUrl);
}
if (from.baseUrl && to.baseUrl) {
const fromBaseUrl = normalizeUrl(from.baseUrl);
const toBaseUrl = normalizeUrl(to.baseUrl);
if (fromBaseUrl && toBaseUrl) {
result = result.replace(new RegExp(fromBaseUrl, "g"), toBaseUrl);
}
result = result.replace(new RegExp(from.baseUrl.replace(/https?:\/\//i, ""), "g"), to.baseUrl.replace(/https?:\/\//i, ""));
if (fromTenantUrl) {
result = result.replace(new RegExp(fromTenantUrl, "g"), toTenantUrl || toBaseUrl);
}
}
return result;
}
/**
* 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 || !_$1.isString(url) || _$1.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 || !_$1.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;
}
/**
* 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 (_$1.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}}'.`);
}
}
/// <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 (_$1.isObjectLike(obj) &&
(("user" in obj && "password" in obj) || "token" in obj));
}
// new function to convert C8yAuthOptions to IAuthentication
function toC8yAuthentication(obj) {
if (!obj || !_$1.isObjectLike(obj)) {
return undefined;
}
if (_$1.get(obj, "getFetchOptions")) {
return obj;
}
if (!isAuthOptions(obj)) {
return undefined;
}
if (obj.token) {
return new client.BearerAuth(obj.token);
}
else if (obj.user && obj.password) {
return new client.BasicAuth({
user: obj.user,
password: obj.password,
tenant: obj.tenant,
});
}
return undefined;
}
// map from case insensitive auth type to C8yAuthOptionType
function getAuthType(auth) {
const type = _$1.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 hasAuthentication(client) {
if (!client)
return false;
const fetchClient = _$1.get(client, "_client.core") ?? _$1.get(client, "core") ?? client;
const getFetchOptionsFn = _$1.get(fetchClient, "getFetchOptions");
if (_$1.isFunction(getFetchOptionsFn)) {
const options = getFetchOptionsFn.apply(fetchClient);
if (!options)
return false;
if (get_i(options, "headers.X-XSRF-TOKEN"))
return true;
if (get_i(options, "headers.authorization"))
return true;
}
if (_$1.get(fetchClient, "_auth"))
return true;
return false;
}
function toPactAuthObject(obj) {
return _$1.pick(obj, C8yPactAuthObjectKeys);
}
function isPactAuthObject(obj) {
return (_$1.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;
}
function getAuthOptionsFromEnv(env) {
if (env == null || !_$1.isObjectLike(env)) {
return undefined;
}
// check first environment variables
const jwtToken = env["C8Y_TOKEN"];
let tokenAuth = undefined;
try {
const authFromToken = getAuthOptionsFromJWT(jwtToken);
if (authFromToken) {
tokenAuth = authWithTenant(env, authFromToken);
}
}
catch {
// ignore errors from extractTokensFromJWT
// this is expected if the token is not a valid JWT
}
const user = env[`C8Y_USERNAME`] ?? env[`C8Y_USER`];
const password = env[`C8Y_PASSWORD`];
let basicAuth = undefined;
if (!_$1.isEmpty(user) && !_$1.isEmpty(password)) {
basicAuth = authWithTenant(env, {
user,
password,
});
}
if (!tokenAuth && !basicAuth) {
return undefined;
}
return { ...(basicAuth ?? {}), ...(tokenAuth ?? {}) };
}
function authWithTenant(env, options) {
if (env == null || !_$1.isObjectLike(env)) {
return options;
}
const tenant = env[`C8Y_TENANT`];
if (tenant && !options?.tenant) {
_$1.extend(options, { tenant });
}
return options;
}
function getAuthOptionsFromBasicAuthHeader(authHeader) {
if (!authHeader ||
!_$1.isString(authHeader) ||
!authHeader.startsWith("Basic ")) {
return undefined;
}
const base64Credentials = authHeader.slice("Basic ".length);
const credentials = decodeBase64(base64Credentials);
const components = credentials.split(":");
if (!components || components.length < 2) {
return undefined;
}
return { user: components[0], password: components.slice(1).join(":") };
}
/**
* 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}`);
}
}
/**
* Extracts the tenant from the basic auth object.
* @param auth The basic auth object containing the user property.
* @returns The tenant or undefined if not found.
*/
function tenantFromBasicAuth(auth) {
if (_$1.isString(auth)) {
auth = { user: auth };
}
if (!auth || !_$1.isObjectLike(auth) || !auth.user)
return undefined;
const components = auth.user.split("/");
if (!components ||
components.length < 2 ||
_$1.isEmpty(components[1]) ||
_$1.isEmpty(components[0]))
return undefined;
return components[0];
}
function encodeBase64(str) {
if (!str)
return "";
let encoded;
if (typeof Buffer !== "undefined") {
encoded = Buffer.from(str).toString("base64");
}
else {
encoded = btoa(str);
}
return encoded;
}
function decodeBase64(base64) {
if (!base64)
return "";
let decoded;
if (typeof Buffer !== "undefined") {
decoded = Buffer.from(base64, "base64").toString("utf-8");
}
else {
decoded = atob(base64);
}
return decoded;
}
/// <reference types="cypress" />
// workaround for lodash import in Cypress nodejs typescript runtime and browser
const _ = _$1 || ___namespace;
/**
* 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 (_.isObjectLike(obj) &&
"request" in obj &&
_.isObjectLike(_.get(obj, "request")) &&
"response" in obj &&
_.isObjectLike(_.get(obj, "response")) &&
_.isFunction(_.get(obj, "toCypressResponse")));
}
async function wrapFetchResponse(response, options = {}) {
// only wrap valid responses or new Response() will fail later
if (response.status == null ||
response.status < 200 ||
response.status > 599) {
return response;
}
const responseObj = await (async () => {
return toCypressResponse(response, options.duration, options.fetchOptions, options.url);
})();
if (!responseObj)
return response;
let rawBody = undefined;
if (response.data) {
responseObj.body = response.data;
rawBody = _$1.isObject(responseObj.body)
? JSON.stringify(responseObj.body)
: responseObj.body;
}
else if (response.body) {
try {
rawBody = await response.text();
const json = JSON.parse(rawBody);
responseObj.body = _$1.isObjectLike(json) ? json : rawBody;
}
catch {
responseObj.body = rawBody;
}
}
// empty body ("") is not allowed, make sure to use undefined instead
if (_$1.isEmpty(rawBody)) {
rawBody = undefined;
}
const fetchOptions = options?.fetchOptions ?? {};
const logOptions = options?.logOptions;
try {
responseObj.requestBody =
fetchOptions && _$1.isString(fetchOptions?.body)
? JSON.parse(fetchOptions.body)
: fetchOptions?.body;
}
catch {
responseObj.requestBody = fetchOptions?.body;
}
// res.ok = response.ok,
responseObj.method = fetchOptions?.method || response.method || "GET";
updateConsoleProps(responseObj, fetchOptions, logOptions, options.url, options.duration);
// create a new window.Response for Client. this is required as the body
// stream can not be read more than once. as we just read it, recreate the response
// and resolve json() and text() promises using the values we read from the stream.
const result = new Response(rawBody, {
status: response.status,
statusText: response.statusText,
headers: response.headers,
});
// pass the responseObj as part of the window.Response object. this way we can access
// in the Clients response and do not need to reprocess
result.responseObj = responseObj;
result.requestBody = responseObj.requestBody;
result.method = responseObj.method;
result.data = responseObj.body;
// result.json = () => Promise.resolve(responseObj.body);
// result.text = () => Promise.resolve(rawBody || "");
return result;
}
function updateConsoleProps(responseObj, fetchOptions, logOptions, url, duration) {
const authInfo = {};
const authorizationHeader = get_i(responseObj, "requestHeaders.authorization");
if (authorizationHeader) {
const auth = getAuthOptionsFromBasicAuthHeader(authorizationHeader);
if (auth?.user && auth?.password) {
authInfo["Basicauth"] = `${authorizationHeader} (${auth.user})`;
}
else {
if (authorizationHeader.startsWith("Bearer ")) {
try {
const jwt = authorizationHeader.replace("Bearer ", "");
const authOptions = getAuthOptionsFromJWT(jwt);
authInfo["BearerAuth"] = authOptions;
}
catch {
// ignore errors parsing JWT
}
}
}
}
else {
let token = get_i(responseObj, "requestHeaders.cookie.authorization");
if (!token) {
token = get_i(responseObj, "requestHeaders.X-XSRF-TOKEN");
}
// props["Options"] = options;
if (token) {
const loggedInUser = logOptions?.loggedInUser || "";
authInfo["CookieAuth"] = `${token} (${loggedInUser})`;
}
}
// Call onRequestEnd callback if available
if (logOptions?.onRequestEnd && logOptions?.contextId) {
logOptions.onRequestEnd({
contextId: logOptions.contextId,
url: toUrlString(url || responseObj.url || ""),
method: fetchOptions?.method || responseObj.method || "GET",
status: responseObj.status || 0,
headers: responseObj.headers || {},
body: responseObj.body,
duration: duration || responseObj.duration || 0,
success: responseObj.isOkStatusCode || false,
fetchOptions,
options: logOptions.options,
yielded: responseObj,
additionalInfo: authInfo,
});
}
}
/**
* 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(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 _$1.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),
};
}
/**
* Checks if the given object is a window.Response.
* @param obj The object to check.
*/
function isWindowFetchResponse(obj) {
return (obj != null &&
_$1.isObjectLike(obj) &&
"status" in obj &&
"statusText" in obj &&
"headers" in obj &&
"body" in obj &&
"url" in obj &&
_$1.isFunction(_$1.get(obj, "json")) &&
_$1.isFunction(_$1.get(obj, "arrayBuffer")));
}
/**
* Checks if the given object is an IResult.
* @param obj The object to check.
*/
function isIResult(obj) {
return (obj != null &&
_$1.isObjectLike(obj) &&
"data" in obj &&
"res" in obj &&
isWindowFetchResponse(obj.res));
}
const maxPageSize = 2000;
/**
* Validates that a delete operation was successful.
*
* Accepts status codes:
* - 204 (No Content) - Successfully deleted
* - 404 (Not Found) - Resource already deleted or never existed
*
* @param status - HTTP status code to validate
* @throws Error if status is not 204 or 404
*
* @internal
*/
function expectSuccessfulDelete(status) {
if (status !== 204 && status !== 404) {
throw new Error(`Expected status 204 or 404, but got ${status}`);
}
}
/**
* Creates a user with the specified global roles and optionally assigns applications.
*
* This function:
* 1. Creates the user in Cumulocity
* 2. Assigns the user to the specified global role groups
* 3. Optionally assigns applications to the user (by name or IApplication object)
*
* @param client - The Cumulocity client instance
* @param user - The user object to create (must include userName, email, etc.)
* @param globalRoles - Array of global role names to assign to the user
* @param applications - Optional array of application names (strings) or IApplication objects to assign
* @returns Promise resolving to the created user result
*
* @throws Error if user creation fails or if roles/applications cannot be assigned
*
* @example
* const userResult = await createUser(
* client,
* { userName: 'john.doe', email: 'john@example.com', password: 'SecurePass123!' },
* ['business'],
* ['cockpit', 'devicemanagement']
* );
*/
async function createUser(client, user, globalRoles, applications) {
const userResponse = await client.user.create(user);
for (const role of globalRoles) {
const groupResponse = await wrapFetchResponse(await client.core.fetch("/user/" + client.core.tenant + "/groupByName/" + role));
const childId = userResponse?.data?.self;
const groupId = groupResponse?.data?.id;
if (!childId || !groupId) {
throw `Failed to add user ${childId} to group ${childId}.`;
}
await client.userGroup.addUserToGroup(groupId, childId);
}
const userId = userResponse.data.id;
// Handle applications if provided
if (applications && applications.length > 0) {
const allApps = [];
for (const app of applications) {
if (typeof app === "string") {
// Fetch application by name
const applicationResponse = await wrapFetchResponse(await client.core.fetch(`/application/applicationsByName/${app}`, {
headers: {
accept: "application/vnd.com.nsn.cumulocity.applicationcollection+json",
},
}));
const applicationsData = applicationResponse.data?.applications || applicationResponse.data;
if (!applicationsData || !Array.isArray(applicationsData)) {
throw new Error(`Application ${app} not found. No or empty response.`);
}
const apps = applicationsData
.map((a) => {
if (typeof a === "string") {
return { type: "HOSTED", id: a };
}
else if (typeof a === "object" && a.id) {
return { id: a.id, type: a.type || "HOSTED" };
}
return undefined;
})
.filter((a) => a !== undefined);
allApps.push(...apps);
}
else if (typeof app === "object" && app.id) {
allApps.push({
id: app.id,
type: app.type || "HOSTED",
});
}
else {
throw new Error("Invalid application format. Expected string (name) or IApplication object with id.");
}
}
// Get user details and merge applications
if (userId && allApps.length > 0) {
const userDetailResponse = await client.user.detail(userId);
const existingApps = userDetailResponse.data?.applications || [];
// Merge with existing applications, avoiding duplicates by id
const mergedApps = [...existingApps];
for (const app of allApps) {
if (!mergedApps.find((existing) => existing.id === app.id)) {
mergedApps.push(app);
}
}
await client.user.update({ id: userId, applications: mergedApps });
}
}
return userResponse;
}
function isIdentifiedObject(user) {
return (typeof user === "object" &&
(user.id != null ||
user.userName != null ||
user.displayName != null ||
user.self != null ||
user.email != null));
}
function needsAllUsersFetch(users) {
const userArray = to_array(users) ?? [];
return (userArray.filter((u) => typeof u === "string" ||
typeof u === "function" ||
typeof u === "string" ||
(typeof u === "object" && u.userName == null && u.id == null)).length > 0);
}
/**
* Deletes one or more users from Cumulocity.
*
* Supports multiple input formats:
* - Single username string
* - Single IUser object (matched by id, userName, displayName, self, or email)
* - Array of usernames or IUser objects
* - Filter function to select users to delete
*
* When an IUser object is provided, the function matches it against existing users using
* any available identifying properties (id, userName, displayName, self, email). This allows
* for flexible matching even with partial user objects.
*
* @param client - The Cumulocity client instance
* @param user - Username(s), IUser object(s), or filter function to identify users to delete
* @param options - Optional configuration
* @param options.ignoreNotFound - If true (default), ignores 404 errors when user is not found
* @returns Promise that resolves when all users are deleted
*
* @throws Error if user is missing required properties or if deletion fails (unless ignoreNotFound is true)
*
* @example
* // Delete single user by username
* await deleteUser(client, 'john.doe');
*
* @example
* // Delete multiple users
* await deleteUser(client, ['user1', 'user2', 'user3']);
*
* @example
* // Delete users matching a filter
* await deleteUser(client, (user) => user.email?.includes('@example.com'));
*
* @example
* // Delete user by partial IUser object
* await deleteUser(client, { displayName: 'John Doe', email: 'john@example.com' });
*/
async function deleteUser(client, user, options) {
if (!user) {
throw new Error("Missing user argument. deleteUser() requires IUser object or username string.");
}
const userArray = to_array(user) ?? [];
const ignoreNotFound = options?.ignoreNotFound ?? true;
let allUsersResponse = undefined;
if (needsAllUsersFetch(user)) {
try {
allUsersResponse = await client.user.list({
pageSize: maxPageSize,
});
}
catch (error) {
throw new Error(`Failed to fetch list of users for list of usernames or filter function: ${error}`);
}
}
let allUsers = undefined;
if (typeof user === "function") {
const fn = user;
allUsers = allUsersResponse?.data.filter((userItem) => fn(userItem));
}
else {
allUsers = userArray.reduce((acc, u) => {
if (typeof u === "string") {
const lowerU = u.toLowerCase();
const foundUser = allUsersResponse?.data.find((userItem) => userItem.userName?.toLowerCase() === lowerU ||
userItem.id?.toLowerCase() === lowerU) ?? false;
if (!foundUser) {
if (ignoreNotFound) {
return acc;
}
throw new Error(`User with username '${u}' not found.`);
}
acc.push(foundUser);
}
else if (typeof u === "object") {
if (!isIdentifiedObject(u)) {
throw new Error("IUser object must have at least one identifying property (id, userName, displayName, self, or email).");
}
if (u.id != null || u.userName != null) {
acc.push(u);
return acc;
}
// If u is IUser object, match using fields available in u
// Properties used for matching: id, userName, displayName, self, email
const foundUser = allUsersResponse?.data.find((userItem) => {
if (u.displayName && userItem.displayName !== u.displayName)
return false;
if (u.self && userItem.self !== u.self)
return false;
if (u.email &&
userItem.email?.toLowerCase() !== u.email.toLowerCase())
return false;
return true;
}) ?? false;
if (!foundUser) {
if (ignoreNotFound) {
return acc;
}
const identifier = u.userName || u.email || u.displayName || u.id || "unknown";
throw new Error(`User with identifier '${identifier}' not found.`);
}
acc.push(foundUser);
}
return acc;
}, []);
}
for (const user of allUsers ?? []) {
try {
const response = await client.user.delete(user.id ?? user.userName);
expectSuccessfulDelete(response.res?.status || 204);
}
catch (error) {
if (error?.res?.status && error?.res?.status !== 404)
throw error;
}
}
}
/**
* Assigns one or more global roles to a user.
*
* This function adds the user to the specified global role groups, granting them
* the permissions associated with those roles.
*
* @param client - The Cumulocity client instance
* @param username - Username string or IUser object (must have userName property)
* @param roles - Array of global role names to assign to the user
* @returns Promise that resolves when all roles are assigned
*
* @throws Error if username is missing, roles array is empty, or if role assignment fails
*
* @example
* await assignUserRoles(client, 'john.doe', ['business', 'admins']);
*
* @example
* const user = await client.user.detail('john.doe');
* await assignUserRoles(client, user.data, ['devicemanagement']);
*/
async function assignUserRoles(client, username, roles) {
const userIdentifier = typeof username === "object" && username.userName
? username.userName
: username;
if (!userIdentifier || (typeof username === "object" && !username.userName)) {
throw new Error("Missing argument. Requiring IUser object with userName or username argument.");
}
if (!roles || roles.length === 0) {
throw new Error("Missing argument. Requiring a string array with roles.");
}
const userResponse = await client.user.detail(userIdentifier);
const childId = userResponse.data?.self;
if (!childId) {
throw new Error(`Failed to assign roles to user ${userIdentifier}. User data null or does not contain self linking.`);
}
for (const role of roles) {
const groupResponse = await client.core.fetch(`/user/${client.core.tenant}/groupByName/${role}`);
const groupId = groupResponse.data?.id;
if (!childId || !groupId) {
throw new Error(`Failed to add user ${childId} to group ${groupId}.`);
}
await client.userGroup.addUserToGroup(groupId, childId);
}
}
/**
* Removes all global roles currently assigned to a user.
*
* This function removes the user from all global role groups, effectively
* revoking all role-based permissions.
*
* @param client - The Cumulocity client instance
* @param username - Username string or IUser object (must have userName property)
* @returns Promise that resolves when all roles are removed
*
* @throws Error if username is missing or if role removal fails
*
* @example
* await clearUserRoles(client, 'john.doe');
*
* @example
* const user = await client.user.detail('john.doe');
* await clearUserRoles(client, user.data);
*/
async function clearUserRoles(client, username) {
const userIdentifier = typeof username === "object" && username.userName
? username.userName
: username;
if (!userIdentifier || (typeof username === "object" && !username.userName)) {
throw new Error("Missing argument. Requiring IUser object with userName or username argument.");
}
const response = await client.user.detail(userIdentifier);
const assignedRoles = response.data.groups?.references;
if (!assignedRoles || assignedRoles.length === 0) {
return;
}
for (const assignedRole of assignedRoles) {
await client.userGroup.removeUserFromGroup(assignedRole.group.id, userIdentifier);
}
}
/**
* Generates a secure random password with mixed case letters, numbers, and special characters.
*
* The password includes:
* - Uppercase and lowercase letters (50% chance for each letter)
* - Numbers (from timestamp)
* - Special characters (!@#$%^&*())
*
* @param length - The desired length of the password (default: 28, minimum: 8)
* @returns A randomly generated password string
*
* @example
* const password = generatePassword();
* // Returns something like: "2Kl9j8Gh!4m2@x7n#5p3q8r9"
*
* @example
* const shortPassword = generatePassword(12);
* // Returns a 12-character password
*/
function generatePassword(length = 28) {
const minLength = 8;
const targetLength = Math.max(length, minLength);
const timestamp = Date.now().toString(36);
const random1 = Math.random().toString(36).substring(2);
const random2 = Math.random().toString(36).substring(2);
// Build password ensuring minimum length before randomization
const base = `${timestamp}-${random1}-${random2}`.substring(0, targetLength);
const specialChars = ["!", "@", "#", "$", "%", "^", "&", "*", "(", ")"];
return randomizePassword(base, specialChars);
}
/**
* Converts letters to uppercase by 50% chance and replaces hyphens with special characters.
* Ensures at least one uppercase character is included.
*
* @param text - The base text to randomize
* @param replaceOptions - Array of special characters to use for replacing hyphens
* @returns The randomized password string
*
* @internal
*/
function randomizePassword(text, replaceOptions) {
let randomizedString = "";
// make sure that at least one char is uppercase by using isFirst flag
let isFirst = true;
for (let i = 0; i < text.length; i++) {
const character = text.charAt(i);
if (/^[a-zA-Z]$/.test(character)) {
const transformedCharacter = isFirst || Math.random() < 0.5
? character.toUpperCase()
: character.toLowerCase();
randomizedString += transformedCharacter;
isFirst = false;
continue;
}
if (character === "-") {
const index = Math.floor(Math.random() * replaceOptions.length);
randomizedString += replaceOptions[index];
continue;
}
randomizedString += character;
}
return randomizedString;
}
/**
* Creates a global role (user group) with the specified permissions.
*
* Global roles are user groups that define a set of permissions. This function:
* 1. Creates a new user group with the specified name
* 2. Assigns the specified role permissions to the group
*
* @param client - The Cumulocity client instance
* @param roleOptions - Role name as string, or object with name and optional description
* @param permissions - Array of permission (role) IDs or names to assign to this global role (e.g., ['ROLE_USER_MANAGEMENT', 'ROLE_INVENTORY_READ'])
* @returns Promise resolving to the created user group result
*
* @throws Error if role creation fails or if any of the specified roles cannot be found
*
* @example
* const roleResult = await createGlobalRole(
* client,
* { name: 'Custom Admin', description: 'Custom admin role with specific permissions' },
* ['ROLE_USER_MANAGEMENT', 'ROLE_INVENTORY_ADMIN']
* );
*/
async function createGlobalRole(client, roleOptions, permissions) {
const roleConfig = typeof roleOptions === "string" ? { name: roleOptions } : roleOptions;
if (!roleConfig.name || roleConfig.name.trim() === "") {
throw new Error("Missing argument. Requiring a name for the global role.");
}
// Create the user group
const createResponse = await client.userGroup.create(roleConfig);
const userGroup = createResponse.data;
const userGroupId = userGroup.id;
if (!userGroupId) {
throw new Error("Failed to create global role. UserGroup id is missing.");
}
// Get all available roles
const listResponse = await client.userRole.list({
pageSize: maxPageSize,
withTotalPages: false,
});
const listRoles = listResponse.data || [];
if (!listRoles || listRoles.length === 0) {
throw new Error("Failed to load roles. No roles found.");
}
// Find matching roles
const matches = listRoles.filter((r) => permissions?.find((item) => item === r.id || item === r.name) != null);
if (matches.length < permissions.length) {
throw new Error(`Failed to assign one of provided userRoles to ${roleConfig.name}. User role not found.`);
}
// Assign roles to the group
for (const match of matches) {
if (!match.self)
continue;
await client.userGroup.addRoleToGroup(userGroupId, match.self);
}
return createResponse;
}
/**
* Deletes one or more global roles (user groups) by name.
*
* @param client - The Cumulocity client instance
* @param roleNames - Single role name or array of role names to delete
* @param options - Optional configuration
* @param options.ignoreNotFound - If true (default), ignores 404 errors when role is not found
* @returns Promise that resolves when all roles are deleted
*
* @throws Error if role names are missing or if deletion fails (unless ignoreNotFound is true)
*
* @example
* // Delete single role
* await deleteGlobalRoles(client, 'CustomRole');
*
* @example
* // Delete multiple roles
* await deleteGlobalRoles(client, ['Role1', 'Role2', 'Role3']);
*/
async function deleteGlobalRoles(client, roleNames, options) {
const roleNamesArray = to_array(roleNames) ?? [];
if (!roleNamesArray || roleNamesArray.length === 0) {
throw new Error("Missing argument. Requiring an array of role names.");
}
const ignoreNotFound = options?.ignoreNotFound ?? true;
const listResponse = await client.userGroup.list({ pageSize: maxPageSize });
const groups = listResponse.data || [];
if (!ignoreNotFound && (!groups || groups.length === 0)) {
throw new Error("Failed to load userGroups. No groups found.");
}
for (const group of groups) {
if (group.name &&
roleNamesArray.some((name) => name.toLowerCase() === group.name?.toLowerCase()) &&
group.id) {
try {
const response = await client.userGroup.delete(group.id);
expectSuccessfulDelete(response.res?.status || 204);
}
catch (error) {
if (error?.res?.status !== 404) {
throw error;
}
}
}
}
}
exports.C8yPactAuthObjectKeys = C8yPactAuthObjectKeys;
exports.assignUserRoles = assignUserRoles;
exports.authWithTenant = authWithTenant;
exports.clearUserRoles = clearUserRoles;
exports.createGlobalRole = createGlobalRole;
exports.createUser = createUser;
exports.decodeBase64 = decodeBase64;
exports.deleteGlobal