cumulocity-cypress
Version:
Cypress commands for Cumulocity IoT
287 lines (286 loc) • 10 kB
JavaScript
import _ from "lodash";
import { getAuthOptionsFromBasicAuthHeader } from "./auth";
import { isCypressResponse, isPactRecord, } from "./c8ypact/c8ypact";
export async function wrapFetchRequest(url, fetchOptions, logOptions) {
// client.tenant.current() does add content-type header for some reason. probably mistaken accept header.
// as this is not required, remove it to avoid special handling in pact matching against recordings
// not created by c8y/client.
if (_.endsWith(toUrlString(url), "/tenant/currentTenant")) {
// @ts-expect-error
fetchOptions.headers = _.omit(fetchOptions.headers, ["content-type"]);
}
else {
// add json content type if body is present and content-type is not set
const method = fetchOptions?.method || "GET";
if (fetchOptions?.body && method !== "GET" && method != "HEAD") {
fetchOptions.headers = {
"content-type": "application/json",
...fetchOptions.headers,
};
}
}
const startTime = Date.now();
const fetchFn = _.get(globalThis, "fetchStub") || globalThis.fetch;
const fetchPromise = fetchFn(url, fetchOptions);
const duration = Date.now() - startTime;
const options = {
url,
fetchOptions,
logOptions,
duration,
};
return fetchPromise
.then(async (response) => {
const res = await wrapFetchResponse(response, options);
if (_.isFunction(logOptions?.logger?.end))
logOptions?.logger?.end();
return Promise.resolve(res);
})
.catch(async (response) => {
const res = await wrapFetchResponse(response, options);
if (_.isFunction(logOptions?.logger?.end))
logOptions?.logger?.end();
return Promise.reject(res);
});
}
export 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 = _.isObject(responseObj.body)
? JSON.stringify(responseObj.body)
: responseObj.body;
}
else if (response.body) {
try {
rawBody = await response.text();
responseObj.body = JSON.parse(rawBody);
}
catch {
responseObj.body = rawBody;
}
}
// empty body ("") is not allowed, make sure to use undefined instead
if (_.isEmpty(rawBody)) {
rawBody = undefined;
}
const fetchOptions = options?.fetchOptions ?? {};
const logOptions = options?.logOptions;
try {
responseObj.requestBody =
fetchOptions && _.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";
if (logOptions?.consoleProps) {
_.extend(logOptions.consoleProps, updateConsoleProps(responseObj, fetchOptions, logOptions));
}
// 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) {
const props = {};
const cookieAuth = (responseObj.requestHeaders &&
responseObj.requestHeaders["X-XSRF-TOKEN"]) ||
undefined;
const basicAuth = (responseObj.requestHeaders &&
responseObj.requestHeaders["Authorization"]) ||
undefined;
// props["Options"] = options;
if (cookieAuth) {
const loggedInUser = logOptions?.loggedInUser || "";
props["CookieAuth"] = `XSRF-TOKEN ${cookieAuth} (${loggedInUser})`;
}
if (basicAuth) {
const auth = getAuthOptionsFromBasicAuthHeader(basicAuth);
if (auth?.user) {
props["BasicAuth"] = `${basicAuth} (${auth.user})`;
}
}
props["Options"] = fetchOptions;
props["Request"] = {
responseBody: responseObj.body,
responseStatus: responseObj.status,
requestHeaders: responseObj.requestHeaders,
requestBody: fetchOptions?.body || "",
responseHeaders: responseObj.headers || [],
requestURL: responseObj.url || url,
};
props["Yielded"] = responseObj;
return props;
}
/**
* Converts the given URL to a string.
* @param url The URL or RequestInfo to convert.
* @returns The URL as a string.
*/
export function toUrlString(url) {
if (_.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.
*/
export 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 _.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 }),
};
}
/**
* Converts a Cypress.Response or C8yPactRecord to a window.Response. If
* the given object is not a Cypress.Response or C8yPactRecord, undefined
* is returned.
* @param obj The object to check.
*/
export function toWindowFetchResponse(obj) {
if (isPactRecord(obj)) {
const body = _.isObjectLike(obj.response.body)
? JSON.stringify(obj.response.body)
: obj.response.body;
return new window.Response(body, {
status: obj.response.status,
statusText: obj.response.statusText,
url: obj.request.url,
...(obj.response.headers && {
headers: toResponseHeaders(obj.response.headers),
}),
...(obj.request.url && { url: obj.request.url }),
});
}
if (isCypressResponse(obj)) {
return new window.Response(obj.body, {
status: obj.status,
statusText: obj.statusText,
headers: toResponseHeaders(obj.headers),
...(obj.url && { url: obj.url }),
});
}
return undefined;
}
/**
* Converts the given headers to a window.Headers object.
* @param headers The headers object to convert.
*/
export function toResponseHeaders(headers) {
// type HeadersInit = [string, string][] | Record<string, string> | Headers;
const arr = [];
for (const [key, value] of Object.entries(headers)) {
if (Array.isArray(value)) {
value.forEach((v) => arr.push([key, v]));
}
else {
arr.push([key, value]);
}
}
return new Headers(arr);
}
/**
* Checks if the given object is a window.Response.
* @param obj The object to check.
*/
export function isWindowFetchResponse(obj) {
return (obj != null &&
_.isObjectLike(obj) &&
"status" in obj &&
"statusText" in obj &&
"headers" in obj &&
"body" in obj &&
"url" in obj &&
_.isFunction(_.get(obj, "json")) &&
_.isFunction(_.get(obj, "arrayBuffer")));
}
/**
* Checks if the given object is an IResult.
* @param obj The object to check.
*/
export function isIResult(obj) {
return (obj != null &&
_.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.
*/
export function isCypressError(error) {
return _.isError(error) && _.get(error, "name") === "CypressError";
}