UNPKG

cumulocity-cypress

Version:
307 lines (306 loc) 12.2 kB
import { isPact, } from "../../shared/c8ypact"; import { getBaseUrlFromEnv } from "../utils"; import { buildTestHierarchy, to_array } from "../../shared/util"; const { _ } = Cypress; /** * Default implementation of C8yPactRunner. Runtime for C8yPact objects that will * create the tests dynamically and rerun recorded requests. Supports Basic and Cookie 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` and `BasicAuth`. */ 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()) || []; for (const record of pact?.records || []) { if (record.request.method != null && !_.isEmpty(methods) && !methods.includes(record.request.method.toLowerCase())) { continue; } if (record.request.url != null && !_.isEmpty(options.paths)) { const url = record.request.url; if (!options.paths?.some((p) => url.startsWith(p))) { 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", "failOnStatusCode", "timeout", ]; const strictMatching = Cypress.config().c8ypact?.strictMatching ?? record.options?.strictMatching ?? pact.info?.strictMatching ?? Cypress.c8ypact.getConfigValue("strictMatching") ?? true; const failOnStatusCode = (record.response?.status ?? 200) < 400; const cOpts = { strictMatching, record, failOnStatusCode, // config keys from record override pact info values ..._.pick(pact.info, configKeys), ..._.pick(record.options, configKeys), }; const responseFn = (response) => { 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); } } if (response.method === "POST") { const newId = response.body.id; if (newId && record.createdObject) { this.idMapper[record.createdObject] = newId; } } }; const envAuth = Cypress.env("C8Y_PACT_RUNNER_AUTH"); const pactAuth = options.authType ?? record.authType(); const isCookieAuth = (envAuth ?? pactAuth) === "CookieAuth" && envAuth !== "BasicAuth"; const isBasicAuth = (envAuth ?? pactAuth) === "BasicAuth"; const f = (c) => c.core.fetch(url, clientFetchOptions); users.forEach((user) => { (user ? cy.getAuth(user) : cy.getAuth()).then((auth) => { if (user !== "devicebootstrap" && isCookieAuth) { if (currentAuth == null || auth?.user !== currentAuth?.user) { cy.wrap(auth, { log: false }).login(); currentAuth = auth; } cy.c8yclient(f, cOpts).then(responseFn); } else { if (isBasicAuth || 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, cOpts) .then(responseFn); } else { cy.c8yclient(f, cOpts).then(responseFn); } } }); }); }); } } 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"].includes(authType.toLowerCase())) { authType = undefined; } return { authType, methods, paths, }; }