@red-hat-developer-hub/e2e-test-utils 1.1.13 → 1.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/deployment/rhdh/deployment.d.ts.map +1 -1
- package/dist/deployment/rhdh/deployment.js +20 -17
- package/dist/playwright/base-config.d.ts.map +1 -1
- package/dist/playwright/base-config.js +1 -0
- package/dist/playwright/fixtures/test.d.ts +4 -1
- package/dist/playwright/fixtures/test.d.ts.map +1 -1
- package/dist/playwright/fixtures/test.js +8 -13
- package/dist/playwright/helpers/auth-api-helper.d.ts +7 -0
- package/dist/playwright/helpers/auth-api-helper.d.ts.map +1 -0
- package/dist/playwright/helpers/auth-api-helper.js +31 -0
- package/dist/playwright/helpers/index.d.ts +2 -0
- package/dist/playwright/helpers/index.d.ts.map +1 -1
- package/dist/playwright/helpers/index.js +2 -0
- package/dist/playwright/helpers/rbac-api-helper.d.ts +35 -0
- package/dist/playwright/helpers/rbac-api-helper.d.ts.map +1 -0
- package/dist/playwright/helpers/rbac-api-helper.js +63 -0
- package/dist/playwright/run-once.d.ts +11 -0
- package/dist/playwright/run-once.d.ts.map +1 -0
- package/dist/playwright/run-once.js +40 -0
- package/dist/playwright/teardown-namespaces.d.ts +11 -0
- package/dist/playwright/teardown-namespaces.d.ts.map +1 -0
- package/dist/playwright/teardown-namespaces.js +34 -0
- package/dist/playwright/teardown-reporter.d.ts +15 -0
- package/dist/playwright/teardown-reporter.d.ts.map +1 -0
- package/dist/playwright/teardown-reporter.js +40 -0
- package/package.json +9 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"deployment.d.ts","sourceRoot":"","sources":["../../../src/deployment/rhdh/deployment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;
|
|
1
|
+
{"version":3,"file":"deployment.d.ts","sourceRoot":"","sources":["../../../src/deployment/rhdh/deployment.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAkB1E,OAAO,KAAK,EACV,iBAAiB,EACjB,gBAAgB,EAGjB,MAAM,YAAY,CAAC;AAEpB,qBAAa,cAAc;IAClB,SAAS,yBAAgC;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,gBAAgB,EAAE,gBAAgB,CAAC;gBAE9B,SAAS,EAAE,MAAM;IAKvB,MAAM,CAAC,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC;YAuCpD,eAAe;YAmBf,aAAa;IAqB3B;;;OAGG;YACW,0BAA0B;YAuC1B,oBAAoB;YAWpB,eAAe;YAmCf,mBAAmB;IAiD3B,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAWrC;;;;OAIG;IACG,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC;IAOpC,cAAc,CAAC,OAAO,GAAE,MAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAwBpD,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;YAIjB,oBAAoB;IAsClC;;;OAGG;YACW,0BAA0B;IAwCxC,OAAO,CAAC,sBAAsB;IAoCxB,SAAS,CAAC,iBAAiB,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAUrE,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,IAAI;IAIZ,OAAO,CAAC,SAAS;CAMlB"}
|
|
@@ -5,6 +5,7 @@ import { test } from "@playwright/test";
|
|
|
5
5
|
import { mergeYamlFilesIfExists, deepMerge } from "../../utils/merge-yamls.js";
|
|
6
6
|
import { loadAndInjectPluginMetadata, generateDynamicPluginsConfigFromMetadata, } from "../../utils/plugin-metadata.js";
|
|
7
7
|
import { envsubst } from "../../utils/common.js";
|
|
8
|
+
import { runOnce } from "../../playwright/run-once.js";
|
|
8
9
|
import cloneDeepWith from "lodash.clonedeepwith";
|
|
9
10
|
import fs from "fs-extra";
|
|
10
11
|
import { DEFAULT_CONFIG_PATHS, AUTH_CONFIG_PATHS, CHART_URL, } from "./constants.js";
|
|
@@ -15,29 +16,33 @@ export class RHDHDeployment {
|
|
|
15
16
|
constructor(namespace) {
|
|
16
17
|
this.deploymentConfig = this._buildDeploymentConfig({ namespace });
|
|
17
18
|
this.rhdhUrl = this._buildBaseUrl();
|
|
18
|
-
this._log(`RHDH deployment initialized (namespace: ${this.deploymentConfig.namespace})`);
|
|
19
|
-
this._log("RHDH Base URL: " + this.rhdhUrl);
|
|
20
|
-
console.table(this.deploymentConfig);
|
|
21
19
|
}
|
|
22
20
|
async deploy(options) {
|
|
23
|
-
this._log("Starting RHDH deployment...");
|
|
24
21
|
// Default 600s, custom number to override, null to skip and let consumer control the timeout
|
|
25
22
|
const timeout = options?.timeout === undefined ? 600_000 : options.timeout;
|
|
26
23
|
if (timeout !== null) {
|
|
27
24
|
test.setTimeout(timeout);
|
|
28
25
|
}
|
|
29
|
-
await
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
await this.
|
|
34
|
-
await this.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
26
|
+
const executed = await runOnce(`deploy-${this.deploymentConfig.namespace}`, async () => {
|
|
27
|
+
this._log("Starting RHDH deployment...");
|
|
28
|
+
this._log("RHDH Base URL: " + this.rhdhUrl);
|
|
29
|
+
console.table(this.deploymentConfig);
|
|
30
|
+
await this.k8sClient.createNamespaceIfNotExists(this.deploymentConfig.namespace);
|
|
31
|
+
await this._applyAppConfig();
|
|
32
|
+
await this._applySecrets();
|
|
33
|
+
if (this.deploymentConfig.method === "helm") {
|
|
34
|
+
await this._deployWithHelm(this.deploymentConfig.valueFile);
|
|
35
|
+
await this.scaleDownAndRestart(); // Restart as helm does not monitor config changes
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
await this._applyDynamicPlugins();
|
|
39
|
+
await this._deployWithOperator(this.deploymentConfig.subscription);
|
|
40
|
+
}
|
|
41
|
+
await this.waitUntilReady();
|
|
42
|
+
});
|
|
43
|
+
if (!executed) {
|
|
44
|
+
this._log(`Deployment already completed for namespace "${this.deploymentConfig.namespace}", skipping`);
|
|
39
45
|
}
|
|
40
|
-
await this.waitUntilReady();
|
|
41
46
|
}
|
|
42
47
|
async _applyAppConfig() {
|
|
43
48
|
const authConfig = AUTH_CONFIG_PATHS[this.deploymentConfig.auth];
|
|
@@ -282,8 +287,6 @@ export class RHDHDeployment {
|
|
|
282
287
|
if (deploymentOptions) {
|
|
283
288
|
this.deploymentConfig = this._buildDeploymentConfig(deploymentOptions);
|
|
284
289
|
this.rhdhUrl = this._buildBaseUrl();
|
|
285
|
-
this._log(`RHDH deployment initialized (namespace: ${this.deploymentConfig.namespace})`);
|
|
286
|
-
console.table(this.deploymentConfig);
|
|
287
290
|
}
|
|
288
291
|
await this.k8sClient.createNamespaceIfNotExists(this.deploymentConfig.namespace);
|
|
289
292
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"base-config.d.ts","sourceRoot":"","sources":["../../src/playwright/base-config.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,oBAAoB,EACrB,MAAM,kBAAkB,CAAC;AAG1B;;;GAGG;AACH,eAAO,MAAM,UAAU,EAAE,
|
|
1
|
+
{"version":3,"file":"base-config.d.ts","sourceRoot":"","sources":["../../src/playwright/base-config.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,oBAAoB,EACrB,MAAM,kBAAkB,CAAC;AAG1B;;;GAGG;AACH,eAAO,MAAM,UAAU,EAAE,oBA6BxB,CAAC;AAEF;;;;;GAKG;AAEH,wBAAgB,YAAY,CAC1B,SAAS,GAAE,IAAI,CAAC,oBAAoB,EAAE,UAAU,CAAM,GACrD,oBAAoB,CAKtB"}
|
|
@@ -15,6 +15,7 @@ export const baseConfig = {
|
|
|
15
15
|
["list"],
|
|
16
16
|
["html", { outputFolder: "playwright-report", open: "on-failure" }],
|
|
17
17
|
["json", { outputFile: "playwright-report/results.json" }],
|
|
18
|
+
[resolve(import.meta.dirname, "../playwright/teardown-reporter.js")],
|
|
18
19
|
],
|
|
19
20
|
use: {
|
|
20
21
|
ignoreHTTPSErrors: true,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { RHDHDeployment } from "../../deployment/rhdh/index.js";
|
|
2
2
|
import { LoginHelper, UIhelper } from "../helpers/index.js";
|
|
3
|
+
import { runOnce } from "../run-once.js";
|
|
3
4
|
type RHDHDeploymentTestFixtures = {
|
|
4
5
|
rhdh: RHDHDeployment;
|
|
5
6
|
uiHelper: UIhelper;
|
|
@@ -8,6 +9,8 @@ type RHDHDeploymentTestFixtures = {
|
|
|
8
9
|
type RHDHDeploymentWorkerFixtures = {
|
|
9
10
|
rhdhDeploymentWorker: RHDHDeployment;
|
|
10
11
|
};
|
|
12
|
+
export declare const test: import("playwright/test").TestType<import("playwright/test").PlaywrightTestArgs & import("playwright/test").PlaywrightTestOptions & RHDHDeploymentTestFixtures, import("playwright/test").PlaywrightWorkerArgs & import("playwright/test").PlaywrightWorkerOptions & RHDHDeploymentWorkerFixtures> & {
|
|
13
|
+
runOnce: typeof runOnce;
|
|
14
|
+
};
|
|
11
15
|
export * from "@playwright/test";
|
|
12
|
-
export declare const test: import("playwright/test").TestType<import("playwright/test").PlaywrightTestArgs & import("playwright/test").PlaywrightTestOptions & RHDHDeploymentTestFixtures, import("playwright/test").PlaywrightWorkerArgs & import("playwright/test").PlaywrightWorkerOptions & RHDHDeploymentWorkerFixtures>;
|
|
13
16
|
//# sourceMappingURL=test.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test.d.ts","sourceRoot":"","sources":["../../../src/playwright/fixtures/test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAEhE,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"test.d.ts","sourceRoot":"","sources":["../../../src/playwright/fixtures/test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAEhE,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAEzC,KAAK,0BAA0B,GAAG;IAChC,IAAI,EAAE,cAAc,CAAC;IACrB,QAAQ,EAAE,QAAQ,CAAC;IACnB,WAAW,EAAE,WAAW,CAAC;CAC1B,CAAC;AAEF,KAAK,4BAA4B,GAAG;IAClC,oBAAoB,EAAE,cAAc,CAAC;CACtC,CAAC;AA2CF,eAAO,MAAM,IAAI;;CAEf,CAAC;AAEH,cAAc,kBAAkB,CAAC"}
|
|
@@ -1,23 +1,14 @@
|
|
|
1
1
|
import { RHDHDeployment } from "../../deployment/rhdh/index.js";
|
|
2
2
|
import { test as base } from "@playwright/test";
|
|
3
3
|
import { LoginHelper, UIhelper } from "../helpers/index.js";
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
import { runOnce } from "../run-once.js";
|
|
5
|
+
const baseTest = base.extend({
|
|
6
6
|
rhdhDeploymentWorker: [
|
|
7
7
|
// eslint-disable-next-line no-empty-pattern
|
|
8
8
|
async ({}, use, workerInfo) => {
|
|
9
|
-
console.log(`Deploying rhdh for plugin ${workerInfo.project.name} in namespace ${workerInfo.project.name}`);
|
|
10
9
|
const rhdhDeployment = new RHDHDeployment(workerInfo.project.name);
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
await use(rhdhDeployment);
|
|
14
|
-
}
|
|
15
|
-
finally {
|
|
16
|
-
if (process.env.CI) {
|
|
17
|
-
console.log(`Deleting namespace ${workerInfo.project.name}`);
|
|
18
|
-
await rhdhDeployment.teardown();
|
|
19
|
-
}
|
|
20
|
-
}
|
|
10
|
+
await rhdhDeployment.configure();
|
|
11
|
+
await use(rhdhDeployment);
|
|
21
12
|
},
|
|
22
13
|
{ scope: "worker", auto: true },
|
|
23
14
|
],
|
|
@@ -46,3 +37,7 @@ export const test = base.extend({
|
|
|
46
37
|
{ scope: "test" },
|
|
47
38
|
],
|
|
48
39
|
});
|
|
40
|
+
export const test = Object.assign(baseTest, {
|
|
41
|
+
runOnce,
|
|
42
|
+
});
|
|
43
|
+
export * from "@playwright/test";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-api-helper.d.ts","sourceRoot":"","sources":["../../../src/playwright/helpers/auth-api-helper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAGxC,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAO;gBAEhB,IAAI,EAAE,IAAI;IAIhB,QAAQ,CACZ,QAAQ,GAAE,MAAe,EACzB,WAAW,GAAE,MAAqB;CA8BrC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// here, we spy on the request to get the Backstage token to use APIs
|
|
2
|
+
export class AuthApiHelper {
|
|
3
|
+
page;
|
|
4
|
+
constructor(page) {
|
|
5
|
+
this.page = page;
|
|
6
|
+
}
|
|
7
|
+
async getToken(provider = "oidc", environment = "production") {
|
|
8
|
+
try {
|
|
9
|
+
const response = await this.page.request.get(`/api/auth/${provider}/refresh?optional=&scope=&env=${environment}`, {
|
|
10
|
+
headers: {
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
12
|
+
"x-requested-with": "XMLHttpRequest",
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
if (!response.ok()) {
|
|
16
|
+
throw new Error(`HTTP error! Status: ${response.status()}`);
|
|
17
|
+
}
|
|
18
|
+
const body = await response.json();
|
|
19
|
+
if (typeof body?.backstageIdentity?.token === "string") {
|
|
20
|
+
return body.backstageIdentity.token;
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
throw new TypeError("Token not found in response body");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
console.error("Failed to retrieve the token:", error);
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -2,4 +2,6 @@ export { GITHUB_API_ENDPOINTS } from "./api-endpoints.js";
|
|
|
2
2
|
export { APIHelper } from "./api-helper.js";
|
|
3
3
|
export { LoginHelper, setupBrowser } from "./common.js";
|
|
4
4
|
export { UIhelper } from "./ui-helper.js";
|
|
5
|
+
export { RbacApiHelper, Policy, Response } from "./rbac-api-helper.js";
|
|
6
|
+
export { AuthApiHelper } from "./auth-api-helper.js";
|
|
5
7
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/playwright/helpers/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/playwright/helpers/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC"}
|
|
@@ -2,3 +2,5 @@ export { GITHUB_API_ENDPOINTS } from "./api-endpoints.js";
|
|
|
2
2
|
export { APIHelper } from "./api-helper.js";
|
|
3
3
|
export { LoginHelper, setupBrowser } from "./common.js";
|
|
4
4
|
export { UIhelper } from "./ui-helper.js";
|
|
5
|
+
export { RbacApiHelper, Response } from "./rbac-api-helper.js";
|
|
6
|
+
export { AuthApiHelper } from "./auth-api-helper.js";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { PermissionAction, RoleConditionalPolicyDecision } from "@backstage-community/plugin-rbac-common";
|
|
2
|
+
import { APIResponse } from "@playwright/test";
|
|
3
|
+
export interface Policy {
|
|
4
|
+
entityReference: string;
|
|
5
|
+
permission: string;
|
|
6
|
+
policy: string;
|
|
7
|
+
effect: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Thin HTTP client for the RHDH RBAC permission API.
|
|
11
|
+
* Uses a static factory (`build`) because the Playwright `APIRequestContext`
|
|
12
|
+
* must be created asynchronously — a constructor cannot await it.
|
|
13
|
+
*/
|
|
14
|
+
export declare class RbacApiHelper {
|
|
15
|
+
private readonly token;
|
|
16
|
+
private readonly apiUrl;
|
|
17
|
+
private readonly authHeader;
|
|
18
|
+
private myContext;
|
|
19
|
+
private constructor();
|
|
20
|
+
/** Creates a fully-initialised instance with a live Playwright request context. */
|
|
21
|
+
static build(token: string): Promise<RbacApiHelper>;
|
|
22
|
+
getPoliciesByRole(policy: string): Promise<APIResponse>;
|
|
23
|
+
/** Fetches all conditional policies across all roles. */
|
|
24
|
+
getConditions(): Promise<APIResponse>;
|
|
25
|
+
/** Filters a full conditions list down to those belonging to a specific role entity ref. */
|
|
26
|
+
getConditionsByRole(role: string, remainingConditions: RoleConditionalPolicyDecision<PermissionAction>[]): Promise<RoleConditionalPolicyDecision<PermissionAction>[]>;
|
|
27
|
+
deleteRole(role: string): Promise<APIResponse>;
|
|
28
|
+
deletePolicy(policy: string, policies: Policy[]): Promise<APIResponse>;
|
|
29
|
+
/** `id` comes from the `RoleConditionalPolicyDecision.id` field returned by the API. */
|
|
30
|
+
deleteCondition(id: string): Promise<APIResponse>;
|
|
31
|
+
}
|
|
32
|
+
export declare class Response {
|
|
33
|
+
static removeMetadataFromResponse(response: APIResponse): Promise<unknown[]>;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=rbac-api-helper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rbac-api-helper.d.ts","sourceRoot":"","sources":["../../../src/playwright/helpers/rbac-api-helper.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,6BAA6B,EAC9B,MAAM,yCAAyC,CAAC;AACjD,OAAO,EAAqB,WAAW,EAAW,MAAM,kBAAkB,CAAC;AAE3E,MAAM,WAAW,MAAM;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;GAIG;AACH,qBAAa,aAAa;IAUJ,OAAO,CAAC,QAAQ,CAAC,KAAK;IAT1C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkD;IACzE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAKzB;IACF,OAAO,CAAC,SAAS,CAAqB;IAEtC,OAAO;IAOP,mFAAmF;WAC/D,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;IASnD,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAIpE,yDAAyD;IAC5C,aAAa,IAAI,OAAO,CAAC,WAAW,CAAC;IAIlD,4FAA4F;IAC/E,mBAAmB,CAC9B,IAAI,EAAE,MAAM,EACZ,mBAAmB,EAAE,6BAA6B,CAAC,gBAAgB,CAAC,EAAE,GACrE,OAAO,CAAC,6BAA6B,CAAC,gBAAgB,CAAC,EAAE,CAAC;IAMhD,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;IAI9C,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE;IAM5D,wFAAwF;IAC3E,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC;CAG/D;AAED,qBAAa,QAAQ;WACN,0BAA0B,CACrC,QAAQ,EAAE,WAAW,GACpB,OAAO,CAAC,OAAO,EAAE,CAAC;CActB"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { request } from "@playwright/test";
|
|
2
|
+
/**
|
|
3
|
+
* Thin HTTP client for the RHDH RBAC permission API.
|
|
4
|
+
* Uses a static factory (`build`) because the Playwright `APIRequestContext`
|
|
5
|
+
* must be created asynchronously — a constructor cannot await it.
|
|
6
|
+
*/
|
|
7
|
+
export class RbacApiHelper {
|
|
8
|
+
token;
|
|
9
|
+
apiUrl = process.env.RHDH_BASE_URL + "/api/permission/";
|
|
10
|
+
authHeader;
|
|
11
|
+
myContext;
|
|
12
|
+
constructor(token) {
|
|
13
|
+
this.token = token;
|
|
14
|
+
this.authHeader = {
|
|
15
|
+
Accept: "application/json",
|
|
16
|
+
Authorization: `Bearer ${this.token}`,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** Creates a fully-initialised instance with a live Playwright request context. */
|
|
20
|
+
static async build(token) {
|
|
21
|
+
const instance = new RbacApiHelper(token);
|
|
22
|
+
instance.myContext = await request.newContext({
|
|
23
|
+
baseURL: instance.apiUrl,
|
|
24
|
+
extraHTTPHeaders: instance.authHeader,
|
|
25
|
+
});
|
|
26
|
+
return instance;
|
|
27
|
+
}
|
|
28
|
+
async getPoliciesByRole(policy) {
|
|
29
|
+
return await this.myContext.get(`policies/role/default/${policy}`);
|
|
30
|
+
}
|
|
31
|
+
/** Fetches all conditional policies across all roles. */
|
|
32
|
+
async getConditions() {
|
|
33
|
+
return await this.myContext.get(`roles/conditions`);
|
|
34
|
+
}
|
|
35
|
+
/** Filters a full conditions list down to those belonging to a specific role entity ref. */
|
|
36
|
+
async getConditionsByRole(role, remainingConditions) {
|
|
37
|
+
return remainingConditions.filter((condition) => condition.roleEntityRef === role);
|
|
38
|
+
}
|
|
39
|
+
async deleteRole(role) {
|
|
40
|
+
return await this.myContext.delete(`roles/role/default/${role}`);
|
|
41
|
+
}
|
|
42
|
+
async deletePolicy(policy, policies) {
|
|
43
|
+
return await this.myContext.delete(`policies/role/default/${policy}`, {
|
|
44
|
+
data: policies,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/** `id` comes from the `RoleConditionalPolicyDecision.id` field returned by the API. */
|
|
48
|
+
async deleteCondition(id) {
|
|
49
|
+
return await this.myContext.delete(`roles/conditions/${id}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export class Response {
|
|
53
|
+
static async removeMetadataFromResponse(response) {
|
|
54
|
+
const responseJson = await response.json();
|
|
55
|
+
if (!Array.isArray(responseJson)) {
|
|
56
|
+
throw new TypeError(`Expected an array from policy response but received: ${JSON.stringify(responseJson)}`);
|
|
57
|
+
}
|
|
58
|
+
return responseJson.map((item) => {
|
|
59
|
+
delete item.metadata;
|
|
60
|
+
return item;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Executes a function only once per test run, even across multiple workers.
|
|
3
|
+
* Automatically resets between test runs (each run uses a unique flag directory).
|
|
4
|
+
* Safe for fullyParallel: true (uses proper-lockfile for cross-process coordination).
|
|
5
|
+
*
|
|
6
|
+
* @param key - Unique identifier for this setup operation
|
|
7
|
+
* @param fn - Function to execute once
|
|
8
|
+
* @returns true if executed, false if skipped (already ran)
|
|
9
|
+
*/
|
|
10
|
+
export declare function runOnce(key: string, fn: () => Promise<void> | void): Promise<boolean>;
|
|
11
|
+
//# sourceMappingURL=run-once.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-once.d.ts","sourceRoot":"","sources":["../../src/playwright/run-once.ts"],"names":[],"mappings":"AAQA;;;;;;;;GAQG;AACH,wBAAsB,OAAO,CAC3B,GAAG,EAAE,MAAM,EACX,EAAE,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,GAC7B,OAAO,CAAC,OAAO,CAAC,CAyBlB"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import lockfile from "proper-lockfile";
|
|
5
|
+
// Each test run gets its own flag directory (ppid = Playwright runner PID)
|
|
6
|
+
const flagDir = path.join(os.tmpdir(), `playwright-once-${process.ppid}`);
|
|
7
|
+
/**
|
|
8
|
+
* Executes a function only once per test run, even across multiple workers.
|
|
9
|
+
* Automatically resets between test runs (each run uses a unique flag directory).
|
|
10
|
+
* Safe for fullyParallel: true (uses proper-lockfile for cross-process coordination).
|
|
11
|
+
*
|
|
12
|
+
* @param key - Unique identifier for this setup operation
|
|
13
|
+
* @param fn - Function to execute once
|
|
14
|
+
* @returns true if executed, false if skipped (already ran)
|
|
15
|
+
*/
|
|
16
|
+
export async function runOnce(key, fn) {
|
|
17
|
+
const flagFile = path.join(flagDir, `${key}.done`);
|
|
18
|
+
const lockTarget = path.join(flagDir, key);
|
|
19
|
+
fs.mkdirSync(flagDir, { recursive: true });
|
|
20
|
+
// already executed, skip without locking
|
|
21
|
+
if (fs.existsSync(flagFile))
|
|
22
|
+
return false;
|
|
23
|
+
// Ensure lock target file exists
|
|
24
|
+
fs.writeFileSync(lockTarget, "", { flag: "a" });
|
|
25
|
+
const release = await lockfile.lock(lockTarget, {
|
|
26
|
+
retries: { retries: 30, minTimeout: 200 },
|
|
27
|
+
stale: 300_000,
|
|
28
|
+
});
|
|
29
|
+
try {
|
|
30
|
+
// Double-check after acquiring lock
|
|
31
|
+
if (fs.existsSync(flagFile))
|
|
32
|
+
return false;
|
|
33
|
+
await fn();
|
|
34
|
+
fs.writeFileSync(flagFile, "");
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
await release();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registers a namespace for teardown after all tests in a project complete.
|
|
3
|
+
* Used by consumers who deploy to custom namespaces (not matching the project name).
|
|
4
|
+
*/
|
|
5
|
+
export declare function registerTeardownNamespace(projectName: string, namespace: string): void;
|
|
6
|
+
/**
|
|
7
|
+
* Returns all custom namespaces registered for teardown for a project.
|
|
8
|
+
* Used by the teardown reporter.
|
|
9
|
+
*/
|
|
10
|
+
export declare function getTeardownNamespaces(projectName: string): string[];
|
|
11
|
+
//# sourceMappingURL=teardown-namespaces.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"teardown-namespaces.d.ts","sourceRoot":"","sources":["../../src/playwright/teardown-namespaces.ts"],"names":[],"mappings":"AAsBA;;;GAGG;AACH,wBAAgB,yBAAyB,CACvC,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,GAChB,IAAI,CASN;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,CAEnE"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
// Workers use process.ppid (Playwright runner PID)
|
|
5
|
+
// Reporter (main process) uses process.pid
|
|
6
|
+
// Both resolve to the same directory.
|
|
7
|
+
const TEARDOWN_DIR = path.join(os.tmpdir(), `playwright-teardown-${process.ppid || process.pid}`);
|
|
8
|
+
const TEARDOWN_FILE = path.join(TEARDOWN_DIR, "namespaces.json");
|
|
9
|
+
function read() {
|
|
10
|
+
if (!fs.existsSync(TEARDOWN_FILE))
|
|
11
|
+
return {};
|
|
12
|
+
return JSON.parse(fs.readFileSync(TEARDOWN_FILE, "utf-8"));
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Registers a namespace for teardown after all tests in a project complete.
|
|
16
|
+
* Used by consumers who deploy to custom namespaces (not matching the project name).
|
|
17
|
+
*/
|
|
18
|
+
export function registerTeardownNamespace(projectName, namespace) {
|
|
19
|
+
fs.mkdirSync(TEARDOWN_DIR, { recursive: true });
|
|
20
|
+
const registry = read();
|
|
21
|
+
const namespaces = registry[projectName] ?? [];
|
|
22
|
+
if (!namespaces.includes(namespace)) {
|
|
23
|
+
namespaces.push(namespace);
|
|
24
|
+
registry[projectName] = namespaces;
|
|
25
|
+
fs.writeFileSync(TEARDOWN_FILE, JSON.stringify(registry));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Returns all custom namespaces registered for teardown for a project.
|
|
30
|
+
* Used by the teardown reporter.
|
|
31
|
+
*/
|
|
32
|
+
export function getTeardownNamespaces(projectName) {
|
|
33
|
+
return read()[projectName] ?? [];
|
|
34
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Reporter, Suite } from "@playwright/test/reporter";
|
|
2
|
+
/**
|
|
3
|
+
* Playwright reporter that deletes namespaces after all tests complete.
|
|
4
|
+
* Runs in the main process, so it survives worker restarts.
|
|
5
|
+
* Only active when process.env.CI is set.
|
|
6
|
+
*
|
|
7
|
+
* By default, deletes the namespace matching the project name.
|
|
8
|
+
* For custom namespaces, consumers can register them via registerTeardownNamespace().
|
|
9
|
+
*/
|
|
10
|
+
export default class TeardownReporter implements Reporter {
|
|
11
|
+
private _projects;
|
|
12
|
+
onBegin(_config: unknown, suite: Suite): void;
|
|
13
|
+
onEnd(): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=teardown-reporter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"teardown-reporter.d.ts","sourceRoot":"","sources":["../../src/playwright/teardown-reporter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,2BAA2B,CAAC;AAIjE;;;;;;;GAOG;AACH,MAAM,CAAC,OAAO,OAAO,gBAAiB,YAAW,QAAQ;IACvD,OAAO,CAAC,SAAS,CAAqB;IAEtC,OAAO,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,GAAG,IAAI;IAOvC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CA2B7B"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { KubernetesClientHelper } from "../utils/kubernetes-client.js";
|
|
2
|
+
import { getTeardownNamespaces } from "./teardown-namespaces.js";
|
|
3
|
+
/**
|
|
4
|
+
* Playwright reporter that deletes namespaces after all tests complete.
|
|
5
|
+
* Runs in the main process, so it survives worker restarts.
|
|
6
|
+
* Only active when process.env.CI is set.
|
|
7
|
+
*
|
|
8
|
+
* By default, deletes the namespace matching the project name.
|
|
9
|
+
* For custom namespaces, consumers can register them via registerTeardownNamespace().
|
|
10
|
+
*/
|
|
11
|
+
export default class TeardownReporter {
|
|
12
|
+
_projects = new Set();
|
|
13
|
+
onBegin(_config, suite) {
|
|
14
|
+
for (const test of suite.allTests()) {
|
|
15
|
+
const name = test.parent.project()?.name;
|
|
16
|
+
if (name)
|
|
17
|
+
this._projects.add(name);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async onEnd() {
|
|
21
|
+
if (!process.env.CI)
|
|
22
|
+
return;
|
|
23
|
+
let k8sClient;
|
|
24
|
+
try {
|
|
25
|
+
k8sClient = new KubernetesClientHelper();
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
console.error(`[TeardownReporter] Cannot connect to cluster, skipping teardown:`, error);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
for (const projectName of this._projects) {
|
|
32
|
+
const customNamespaces = getTeardownNamespaces(projectName);
|
|
33
|
+
const namespaces = customNamespaces.length > 0 ? customNamespaces : [projectName];
|
|
34
|
+
for (const ns of namespaces) {
|
|
35
|
+
console.log(`[TeardownReporter] Deleting namespace "${ns}" (project: ${projectName})`);
|
|
36
|
+
await k8sClient.deleteNamespace(ns);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@red-hat-developer-hub/e2e-test-utils",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.15",
|
|
4
4
|
"description": "Test utilities for RHDH E2E tests",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -44,6 +44,10 @@
|
|
|
44
44
|
"./keycloak": {
|
|
45
45
|
"types": "./dist/deployment/keycloak/index.d.ts",
|
|
46
46
|
"default": "./dist/deployment/keycloak/index.js"
|
|
47
|
+
},
|
|
48
|
+
"./teardown": {
|
|
49
|
+
"types": "./dist/playwright/teardown-namespaces.d.ts",
|
|
50
|
+
"default": "./dist/playwright/teardown-namespaces.js"
|
|
47
51
|
}
|
|
48
52
|
},
|
|
49
53
|
"publishConfig": {
|
|
@@ -84,10 +88,12 @@
|
|
|
84
88
|
"@types/js-yaml": "^4.0.9",
|
|
85
89
|
"@types/lodash.clonedeepwith": "^4.5.9",
|
|
86
90
|
"@types/lodash.mergewith": "^4.6.9",
|
|
87
|
-
"@types/node": "^24.10.1"
|
|
91
|
+
"@types/node": "^24.10.1",
|
|
92
|
+
"@types/proper-lockfile": "^4.1.4"
|
|
88
93
|
},
|
|
89
94
|
"dependencies": {
|
|
90
95
|
"@axe-core/playwright": "^4.11.0",
|
|
96
|
+
"@backstage-community/plugin-rbac-common": "1.23.0",
|
|
91
97
|
"@eslint/js": "^9.39.1",
|
|
92
98
|
"@keycloak/keycloak-admin-client": "^26.0.0",
|
|
93
99
|
"@kubernetes/client-node": "^1.4.0",
|
|
@@ -100,6 +106,7 @@
|
|
|
100
106
|
"lodash.mergewith": "^4.6.2",
|
|
101
107
|
"otplib": "12.0.1",
|
|
102
108
|
"prettier": "^3.7.4",
|
|
109
|
+
"proper-lockfile": "^4.1.2",
|
|
103
110
|
"typescript": "^5.9.3",
|
|
104
111
|
"typescript-eslint": "^8.48.1",
|
|
105
112
|
"zx": "^8.8.5"
|