UNPKG

cumulocity-cypress

Version:
368 lines (367 loc) 14.3 kB
const { _ } = Cypress; import { getBaseUrlFromEnv, getCookieAuthFromEnv, normalizedC8yclientArguments, restoreClient, storeClient, tenantFromBasicAuth, throwError, } from "./../utils"; import { BasicAuth, Client, FetchClient, } from "@c8y/client"; import { wrapFetchRequest, toCypressResponse, } from "../../shared/c8yclient"; import { isAuthOptions } from "../../shared/auth"; import "../pact/c8ymatch"; export const defaultClientOptions = () => { return { log: true, timeout: Cypress.env("C8Y_C8YCLIENT_TIMEOUT") || Cypress.config().responseTimeout, failOnStatusCode: true, preferBasicAuth: false, skipClientAuthentication: false, ignorePact: false, failOnPactValidation: true, schema: undefined, strictMatching: false, }; }; let logOnce = true; _.set(globalThis, "fetchStub", window.fetch); globalThis.fetch = async function (url, fetchOptions) { const consoleProps = {}; let logger = undefined; if (logOnce === true) { logger = Cypress.log({ name: "c8yclient", autoEnd: false, message: "", consoleProps: () => consoleProps, renderProps() { function getIndicator() { if (!consoleProps["Yielded"]) return "pending"; if (consoleProps["Yielded"].isOkStatusCode) return "successful"; return "bad"; } function getStatus() { return ((consoleProps["Yielded"] && !_.isEmpty(consoleProps["Yielded"]) && consoleProps["Yielded"].status) || "---"); } return { message: `${fetchOptions?.method || "GET"} ${getStatus()} ${getDisplayUrl(url || consoleProps["Yielded"]?.url || "")}`, indicator: getIndicator(), status: getStatus(), }; }, }); } else { logOnce = true; } return wrapFetchRequest(url, fetchOptions, { consoleProps, logger, loggedInUser: Cypress.env("C8Y_LOGGED_IN_USER") ?? Cypress.env("C8Y_LOGGED_IN_USER_ALIAS"), }); }; const c8yclientFn = (...args) => { const prevSubjectIsAuth = args && !_.isEmpty(args) && isAuthOptions(args[0]); const prevSubject = args && !_.isEmpty(args) && !isAuthOptions(args[0]) ? args[0] : undefined; let $args = normalizedC8yclientArguments(args && prevSubject ? args.slice(1) : args); let authOptions; let basicAuth, cookieAuth; if (!isAuthOptions($args[0]) && _.isObject($args[$args.length - 1])) { $args = [ $args[$args.length - 1].auth, ...($args[0] === undefined ? $args.slice(1) : $args), ]; if ($args[0]?.user) { basicAuth = $args[0]; } else { cookieAuth = $args[0]; } } else if (!_.isEmpty($args) && $args[0]?.user) { authOptions = $args[0]; basicAuth = new BasicAuth({ user: authOptions.user, password: authOptions.password, tenant: authOptions.tenant, }); $args[0] = basicAuth; } else if (_.isFunction($args[0]) || isArrayOfFunctions($args[0])) { $args.unshift(undefined); } // check if there is a XSRF token to use for CookieAuth if (!cookieAuth) { cookieAuth = getCookieAuthFromEnv(); } if ($args.length === 2 && _.isObject($args[0]) && (_.isFunction($args[1]) || isArrayOfFunctions($args[1]))) { $args.push({}); } const [argAuth, clientFn, argOptions] = $args; const options = _.defaults(argOptions, defaultClientOptions()); // force CookieAuth over BasicAuth if present and not disabled by options const auth = cookieAuth && options.preferBasicAuth === false && !prevSubjectIsAuth ? cookieAuth : argAuth; const baseUrl = options.baseUrl || getBaseUrlFromEnv(); const tenant = (basicAuth && tenantFromBasicAuth(basicAuth)) || (authOptions && authOptions.tenant) || Cypress.env("C8Y_TENANT"); // if client is provided via options, use it let c8yclient = { _client: options.client }; // restore client only if client is undefined and no auth is provided as previousSubject // previousSubject must have priority if (!options.client && !(args[0] && isAuthOptions(args[0]))) { c8yclient = restoreClient() || { _client: undefined }; } if (!c8yclient._client && clientFn && !auth) { throwError("Missing authentication. Authentication or Client required."); } // pass userAlias into the auth so it is part of the pact recording if (authOptions && authOptions.userAlias) { auth.userAlias = authOptions.userAlias; } else if (Cypress.env("C8Y_LOGGED_IN_USER_ALIAS")) { auth.userAlias = Cypress.env("C8Y_LOGGED_IN_USER_ALIAS"); } if (!c8yclient._client && !tenant && !options.skipClientAuthentication) { logOnce = options.log; authenticateClient(auth, options, baseUrl).then({ timeout: options.timeout }, (c) => { return runClient(c, clientFn, prevSubject, baseUrl); }); } else { if (!c8yclient._client) { c8yclient._client = new Client(auth, baseUrl); if (tenant) { c8yclient._client.core.tenant = tenant; } } else if ((auth && !options.client) || prevSubjectIsAuth) { // overwrite auth for restored clients c8yclient._client.setAuth(auth); c8yclient._auth = auth; } c8yclient._options = options; if (!c8yclient._auth) { c8yclient._auth = auth; } runClient(c8yclient, clientFn, prevSubject, baseUrl); } }; function runClient(client, fns, prevSubject, baseUrl) { storeClient(client); if (!fns) { // return Cypress.isCy(client) ? client : cy.wrap(client._client, { log: false }); return cy.wrap(client._client, { log: false }); } logOnce = client._options?.log || true; return run(client, fns, prevSubject, client._options || {}, baseUrl); } // create client as Client.authenticate() does, but also support // Cookie authentication as Client.authenticate() only works with BasicAuth function authenticateClient(auth, options, baseUrl) { return cy.then({ timeout: options.timeout }, async () => { let res; try { const clientCore = new FetchClient(auth, baseUrl); res = await clientCore.fetch("/tenant/currentTenant"); } catch (error) { if (_.isError(error)) { error.name = "CypressError"; throw error; } else { const ee = new Error(`Failed to fetch /tenant/currentTenant`); ee.name = "CypressError"; throw ee; } } if (res.status !== 200) { throwError(makeErrorMessage(res.responseObj)); } const { name } = await res.json(); const client = new Client(auth, baseUrl); client.core.tenant = name; return { _client: client, _options: options, _auth: auth }; }); } function run(client, fns, prevSubject, options, baseUrl) { const clientFn = isArrayOfFunctions(fns) ? fns.shift() : fns; if (!clientFn) { return; } const safeClient = client._client; if (!safeClient) { throwError("Client not initialized when running client function."); } return cy.then({ timeout: options.timeout }, async () => { const enabled = Cypress.c8ypact.isEnabled(); const ignore = options?.ignorePact === true || false; const savePact = !ignore && Cypress.c8ypact.isRecordingEnabled(); const matchPact = (response, schema) => { if (schema) { cy.c8ymatch(response, schema, undefined, options); } else { // object matching against existing pact if (ignore || !enabled) return; if (Cypress.c8ypact.mode() !== "apply") return; for (const r of _.isArray(response) ? response : [response]) { const record = options.record ?? Cypress.c8ypact.current?.nextRecord(); const info = Cypress.c8ypact.current?.info; if (record != null && info != null && !ignore) { cy.c8ymatch(r, record, info, options); } else { if (record == null && Cypress.c8ypact.getConfigValue("failOnMissingPacts", true) && !ignore) { throwError(`${Cypress.c8ypact.getCurrentTestId()} not found. Disable Cypress.c8ypact.config.failOnMissingPacts to ignore.`); } } } } }; try { const response = await new Cypress.Promise(async (resolve, reject) => { const isErrorResponse = (resp) => { return ((_.isArray(resp) ? resp : [resp]).filter((r) => (r.isOkStatusCode !== true && options.failOnStatusCode) || _.isError(r)).length > 0); }; const preprocessedResponse = async (promise) => { let result; try { result = await promise; } catch (error) { result = error; } const cypressResponse = toCypressResponse(result); if (cypressResponse) { cypressResponse.$body = options.schema; if (savePact) { await Cypress.c8ypact.savePact(cypressResponse, client); } if (isErrorResponse(cypressResponse)) { throw cypressResponse; } } return cypressResponse; }; const resultPromise = clientFn(safeClient, prevSubject); if (_.isError(resultPromise)) { reject(resultPromise); return; } if (_.isArray(resultPromise)) { let toReject = false; const result = []; for (const task of resultPromise) { try { result.push(await preprocessedResponse(task)); } catch (err) { result.push(err); toReject = true; } } if (toReject) { reject(result); } else { resolve(result); } } else { try { resolve(await preprocessedResponse(resultPromise)); } catch (err) { reject(err); } } }); matchPact(response, options.schema); cy.then(() => { if (isArrayOfFunctions(fns) && !_.isEmpty(fns)) { run(client, fns, response, options, baseUrl); } else { cy.wrap(response, { log: Cypress.c8ypact.debugLog }); } }); } catch (err) { if (_.isError(err)) throw err; matchPact(err, options.schema); cy.then(() => { // @ts-expect-error: utils is not public Cypress.utils.throwErrByPath("request.c8yclient_status_invalid", { args: err, stack: false, }); }); } }); } _.extend(Cypress.errorMessages.request, { c8yclient_status_invalid(obj) { const err = obj.args || obj.errorProps || obj; return { message: makeErrorMessage(obj), docsUrl: `${(err.body && err.body.info) || "https://github.com/Cumulocity-IoT/cumulocity-cypress"}`, }; }, }); function makeErrorMessage(obj) { const err = obj.args || obj.errorProps || obj; const body = err.body || {}; const message = [ `c8yclient failed with: ${err.status} (${err.statusText})`, `${err.url}`, `The response we received from Cumulocity was: `, `${_.isObject(body) ? JSON.stringify(body, null, 2) : body.toString()}`, `For more information check:`, `${(err.body && err.body.info) || "https://github.com/Cumulocity-IoT/cumulocity-cypress"}`, `\n`, ].join(`\n`); return message; } // from error_utils.ts function getDisplayUrl(url, baseUrl = getBaseUrlFromEnv()) { if (!baseUrl) return url; return url.replace(baseUrl, ""); } Cypress.Commands.add("c8yclientf", { prevSubject: "optional" }, (...args) => { const failOnStatus = { failOnStatusCode: false }; args = _.dropRightWhile(args, (n) => n == null); let options = _.last(args); if (!_.isObjectLike(options)) { options = failOnStatus; args.push(options); } else { args[args.length - 1] = { ...options, ...failOnStatus }; } return c8yclientFn(...args); }); Cypress.Commands.add("c8yclient", { prevSubject: "optional" }, c8yclientFn); /** * Checks if the given object is an array only containing functions. * @param obj The object to check. */ export function isArrayOfFunctions(functions) { if (!functions || !_.isArray(functions) || _.isEmpty(functions)) return false; return _.isEmpty(functions.filter((f) => !_.isFunction(f))); }