UNPKG

cumulocity-cypress

Version:
379 lines (378 loc) 16.6 kB
import { getCreatedObjectId, isPact, } from "../../shared/c8ypact"; import { getBaseUrlFromEnv, throwError } from "../utils"; import { buildTestHierarchy, to_array } from "../../shared/util"; import { getAuthType } from "../../shared/auth"; const { _ } = Cypress; /** * Default implementation of C8yPactRunner. Runtime for C8yPact objects that will * create the tests dynamically and rerun recorded requests. Supports Basic, Cookie, and Bearer based * authentication, id mapping, consumer and producer filtering and URL replacement. * * Use `C8Y_PACT_RUNNER_AUTH` to set the authentication type for the runner and overwrite * the authentication type detected in the pact records. Supported values are `CookieAuth`, `BasicAuth`, and `BearerAuth`. */ export class C8yDefaultPactRunner { constructor() { this.idMapper = {}; } run(pacts, options = {}) { this.idMapper = {}; if (!_.isArray(pacts)) return; const tests = []; for (const pact of pacts) { const { info } = pact; if (!isPact(pact)) continue; if (_.isString(options.consumer) && (_.isString(info?.consumer) ? info?.consumer : info?.consumer?.name) !== options.consumer) { continue; } if (_.isString(options.producer) && (_.isString(info?.producer) ? info?.consumer : info?.producer?.name) !== options.consumer) { continue; } if (!info?.title) { pact.info.title = pact.id?.split("__"); } tests.push(pact); } const testHierarchy = buildTestHierarchy(tests, (item) => item.info.title ?? [item.id]); this.createTestsFromHierarchy(testHierarchy, options); } createTestsFromHierarchy(hierarchy, options) { const keys = Object.keys(hierarchy); keys.forEach((key) => { const subTree = hierarchy[key]; if (isPact(subTree)) { const annotations = { tags: subTree.info?.tags, }; beforeEach(() => { if (!Cypress.env("C8Y_TENANT")) { cy.getAuth().getTenantId({ ignorePact: true }); } }); it(key, annotations, () => { this.runTest(subTree, options); }); } else { const that = this; context(key, function () { that.createTestsFromHierarchy(subTree, options); }); } }); } runTest(pact, options = {}) { Cypress.c8ypact.current = pact; this.idMapper = {}; let currentAuth = undefined; const methods = options.methods?.map((m) => m.toLowerCase()) || []; let recordIndex = -1; const failedRequests = []; for (const preRecord of pact?.records || []) { recordIndex++; if (preRecord == null) continue; if (preRecord.request.method != null && !_.isEmpty(methods) && !methods.includes(preRecord.request.method.toLowerCase())) { continue; } if (preRecord.request.url != null && !_.isEmpty(options.paths)) { const url = preRecord.request.url; if (!options.paths?.some((p) => url.startsWith(p))) { continue; } } let record = preRecord; if (_.isFunction(Cypress.c8ypact.on?.runRecord)) { record = Cypress.c8ypact.on.runRecord(preRecord, pact); } if (!record) { cy.log("Skipping request disabled by Cypress.c8ypact.on.runRecord."); continue; } cy.then(() => { const url = this.createURL(record, pact.info); if (!url) { cy.log("Skipping request without URL."); return; } const clientFetchOptions = this.createFetchOptions(record, pact.info); if (clientFetchOptions.method.toLowerCase() === "post") { if (!clientFetchOptions.body) { cy.log("Skipping POST request without body: " + url); return; } } let users = (to_array(record.auth?.userAlias ?? record.auth?.user) ?? []).map((item) => { if ((item || "").split("/").length > 1) { return item?.split("/")?.slice(1)?.join("/"); } else { return item; } }); if (url === "/devicecontrol/deviceCredentials") { users = to_array("devicebootstrap") ?? []; } if (users?.length === 0) { users = [undefined]; } const configKeys = [ "skipClientAuthentication", "preferBasicAuth", "timeout", "schema", ]; const strictMatching = Cypress.config().c8ypact?.strictMatching ?? record.options?.strictMatching ?? pact.info?.strictMatching ?? Cypress.c8ypact.getConfigValue("strictMatching") ?? true; const requestId = record.id ?? record.options?.requestId; const failOnStatusCode = record.options?.failOnStatusCode ?? (record.response?.status ?? 200) < 400; const matchSchemaAndObject = record.options?.matchSchemaAndObject ?? pact.info?.matchSchemaAndObject ?? Cypress.c8ypact.getConfigValue("matchSchemaAndObject") ?? false; const cOpts = { strictMatching, record, failOnStatusCode, matchSchemaAndObject, ...(requestId ? { requestId } : {}), // config keys from record override pact info values ..._.pick(pact.info, configKeys), ..._.pick(record.options, configKeys), }; const responseFn = (response, id) => { if (url === "/devicecontrol/deviceCredentials" && response.status === 201) { const { username, password } = response.body; if (username && password) { Cypress.env(`${username}_username`, username); Cypress.env(`${username}_password`, password); } } const createdObjectId = getCreatedObjectId(response); if (createdObjectId != null && record.createdObject != null) { this.idMapper[record.createdObject] = createdObjectId; // Ensure config, preprocessor, and regexReplace objects exist const config = Cypress.config(); if (config.c8ypact != null) { const preprocessorOptions = config.c8ypact.preprocessor ?? {}; preprocessorOptions.regexReplace ?? (preprocessorOptions.regexReplace = {}); const key = "response"; const newValue = `/${createdObjectId}/${record.createdObject}/g`; const existing = preprocessorOptions.regexReplace[key]; if (Array.isArray(existing)) { preprocessorOptions.regexReplace[key] = [...existing, newValue]; } else if (_.isString(existing)) { preprocessorOptions.regexReplace[key] = [existing, newValue]; } else if (existing == null) { preprocessorOptions.regexReplace[key] = [newValue]; } _.set(Cypress.config(), "c8ypact.preprocessor", preprocessorOptions); } } if (options.assertions?.maxRequestDuration != null && response.duration != null && response.duration > options.assertions.maxRequestDuration) { failedRequests.push({ id: id ?? `record-${recordIndex}`, duration: response.duration, message: `Request duration of ${response.duration}ms exceeds maximum of ${options.assertions.maxRequestDuration}ms.`, }); } }; const envAuth = getAuthType(Cypress.env("C8Y_PACT_RUNNER_AUTH")); const pactAuth = record.authType(); const optionsAuth = getAuthType(options.authType); // Priority for auth type: options, env, pact record const authType = optionsAuth ?? envAuth ?? pactAuth; const isCookieAuth = authType === "CookieAuth"; const isBasicAuth = authType === "BasicAuth"; const isBearerAuth = authType === "BearerAuth"; const f = (c) => c.core.fetch(url, clientFetchOptions); users.forEach((user) => { (user ? cy.getAuth(user) : cy.getAuth()).then((auth) => { if (isBasicAuth && (auth?.user == null || auth.password == null)) { throw new Error(`Basic auth configured for request, but username and password not found for ${auth?.userAlias ?? user}.`); } else if (isBearerAuth && auth?.token == null) { throw new Error(`Bearer auth configured for request, but token not found for ${auth?.userAlias ?? user}.`); } const updatedOpts = _.cloneDeep(cOpts); if (requestId != null && users.length > 1) { updatedOpts.requestId = `${requestId}/${user}`; } if (user !== "devicebootstrap" && isCookieAuth) { if (currentAuth == null || auth?.user !== currentAuth?.user) { cy.wrap(auth, { log: false }).login(); currentAuth = auth; } cy.c8yclient(f, updatedOpts).then((response) => responseFn(response, requestId)); } else { if (isBasicAuth || isBearerAuth || user != null) { if (auth == null) { // should not get here as cy.getAuth(user) should fail if // no auth is found for the user. just making sure we get the // correct error message in case we still get here throw new Error(`Auth missing for user ${user}. This should not happen.`); } cy.wrap(auth, { log: false }) .c8yclient(f, updatedOpts) .then((response) => responseFn(response, requestId)); } else { cy.c8yclient(f, updatedOpts).then((response) => responseFn(response, requestId)); } } }); }); }); } cy.then(() => { if (failedRequests.length > 0) { const messages = failedRequests .map((fr) => { return `- [${fr.id}]: ${fr.message}`; }) .join("\n"); throwError(`One or more requests failed:\n${messages}`); } }); } createHeader(pact) { const headers = _.omitBy(pact.request.headers || {}, (v, k) => k.toLowerCase() === "x-xsrf-token" || k.toLowerCase() === "authorization"); return headers; } createFetchOptions(pact, info) { const options = { method: pact.request.method || "GET", headers: this.createHeader(pact), }; const body = pact.request.body; if (body) { if (_.isString(body)) { options.body = this.updateIds(body); options.body = this.updateURLs(options.body, info); } else if (_.isObject(body)) { let b = JSON.stringify(body); b = this.updateIds(b); b = this.updateURLs(b, info); options.body = b; } } return options; } createURL(pact, info) { let url = pact.request.url; if (info?.baseUrl && url?.includes(info.baseUrl)) { url = url.replace(info.baseUrl, ""); } const baseUrl = getBaseUrlFromEnv(); if (baseUrl && url?.includes(baseUrl)) { url = url.replace(baseUrl, ""); } if (url) { url = this.updateIds(url); } return url; } updateURLs(value, info) { if (!value || !info) return value; let result = value; const tenantUrl = (baseUrl, tenant) => { if (!baseUrl || !tenant) return undefined; try { const url = new URL(baseUrl); const instance = url.host.split(".")?.slice(1)?.join("."); url.host = `${tenant}.${instance}`; return url; } catch { // no-op } return undefined; }; const baseUrl = getBaseUrlFromEnv(); if (baseUrl && info.baseUrl) { const infoUrl = tenantUrl(info.baseUrl, info?.tenant); const url = tenantUrl(baseUrl, Cypress.env("C8Y_TENANT")); if (infoUrl && url) { const regexp = new RegExp(`${infoUrl.href}`, "g"); result = result.replace(regexp, url.href); } if (getBaseUrlFromEnv() && info.baseUrl) { const regexp = new RegExp(`${info.baseUrl}`, "g"); result = result.replace(regexp, baseUrl); } } if (info.tenant && Cypress.env("C8Y_TENANT")) { const regexp = new RegExp(`${info.tenant}`, "g"); result = result.replace(regexp, Cypress.env("C8Y_TENANT")); } return result; } updateIds(value) { if (!value || !this.idMapper) return value; let result = value; for (const currentId of Object.keys(this.idMapper)) { const regexp = new RegExp(`${currentId}`, "g"); result = result.replace(regexp, this.idMapper[currentId]); } return result; } } export function getOptionsFromEnvironment() { let methods = Cypress.env("C8Y_PACT_RUNNER_METHODS"); if (methods != null) { if (_.isString(methods)) { methods = methods.split(","); } if (_.isArray(methods)) { methods = methods.map((m) => m.trim().toLowerCase()); } else { methods = undefined; } } let paths = Cypress.env("C8Y_PACT_RUNNER_PATHS"); if (paths != null) { if (_.isString(paths)) { paths = paths.split(","); } if (_.isArray(paths)) { paths = paths.map((p) => p.trim()); } else { paths = undefined; } } let authType = Cypress.env("C8Y_PACT_RUNNER_AUTH"); if (authType && !["basicauth", "cookieauth", "bearerauth"].includes(authType.toLowerCase())) { authType = undefined; } return { authType, methods, paths, }; }