cumulocity-cypress
Version:
Cypress commands for Cumulocity IoT
379 lines (378 loc) • 16.6 kB
JavaScript
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,
};
}