Skip to content

Commit 7e434fa

Browse files
committed
chore(rbac): migrate RBAC plugin tests from RHDH
Signed-off-by: Patrick Knight <pknight@redhat.com>
1 parent c7076e3 commit 7e434fa

19 files changed

Lines changed: 4258 additions & 0 deletions
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
nodeLinker: node-modules
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { createEslintConfig } from "@red-hat-developer-hub/e2e-test-utils/eslint";
2+
3+
export default createEslintConfig(import.meta.dirname);
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"name": "rbac-e2e-tests",
3+
"version": "1.0.0",
4+
"private": true,
5+
"type": "module",
6+
"engines": {
7+
"node": ">=22",
8+
"yarn": ">=3"
9+
},
10+
"packageManager": "yarn@3.8.7",
11+
"description": "E2E tests for RBAC",
12+
"scripts": {
13+
"test": "playwright test",
14+
"report": "playwright show-report",
15+
"test:ui": "playwright test --ui",
16+
"test:headed": "playwright test --headed",
17+
"lint:check": "eslint .",
18+
"lint:fix": "eslint . --fix",
19+
"prettier:check": "prettier --check .",
20+
"prettier:fix": "prettier --write .",
21+
"tsc:check": "tsc --noEmit",
22+
"check": "tsc --noEmit && yarn lint:check && yarn prettier:check"
23+
},
24+
"devDependencies": {
25+
"@backstage-community/plugin-rbac-common": "1.23.0",
26+
"@eslint/js": "^9.39.2",
27+
"@playwright/test": "1.57.0",
28+
"@red-hat-developer-hub/e2e-test-utils": "1.1.10",
29+
"@types/node": "^24.10.1",
30+
"dotenv": "^16.4.7",
31+
"eslint": "^9.39.2",
32+
"eslint-plugin-check-file": "^3.3.1",
33+
"eslint-plugin-playwright": "^2.4.0",
34+
"prettier": "^3.7.4",
35+
"typescript": "^5.9.3",
36+
"typescript-eslint": "^8.50.0"
37+
}
38+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { defineConfig } from "@red-hat-developer-hub/e2e-test-utils/playwright-config";
2+
import dotenv from "dotenv";
3+
4+
dotenv.config({ path: `${import.meta.dirname}/.env` });
5+
6+
export default defineConfig({
7+
projects: [
8+
{
9+
name: "rbac",
10+
},
11+
],
12+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export interface Policy {
2+
entityReference: string;
3+
permission: string;
4+
policy: string;
5+
effect: string;
6+
}
7+
8+
export interface Role {
9+
memberReferences: string[];
10+
name: string;
11+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { APIRequestContext, APIResponse, request } from "@playwright/test";
2+
import { Policy } from "./rbac-api-structure";
3+
4+
export default class RhdhRbacApi {
5+
private readonly apiUrl = process.env.baseURL + "/api/permission/";
6+
private readonly authHeader: {
7+
// eslint-disable-next-line @typescript-eslint/naming-convention
8+
Accept: "application/json";
9+
// eslint-disable-next-line @typescript-eslint/naming-convention
10+
Authorization: string;
11+
};
12+
private myContext!: APIRequestContext;
13+
14+
private constructor(private readonly token: string) {
15+
this.authHeader = {
16+
Accept: "application/json",
17+
Authorization: `Bearer ${this.token}`,
18+
};
19+
}
20+
21+
public static async build(token: string): Promise<RhdhRbacApi> {
22+
const instance = new RhdhRbacApi(token);
23+
instance.myContext = await request.newContext({
24+
baseURL: instance.apiUrl,
25+
extraHTTPHeaders: instance.authHeader,
26+
});
27+
return instance;
28+
}
29+
30+
// Used during the afterAll to ensure we clean up any roles that are left over due to failing tests
31+
public async deleteRole(role: string): Promise<APIResponse> {
32+
return await this.myContext.delete(`roles/role/default/${role}`);
33+
}
34+
35+
// Used during the afterAll to ensure we clean up any policies that are left over due to failing tests
36+
public async getPoliciesByRole(policy: string): Promise<APIResponse> {
37+
return await this.myContext.get(`policies/role/default/${policy}`);
38+
}
39+
40+
// Used during the afterAll to ensure we clean up any policies that are left over due to failing tests
41+
public async deletePolicy(policy: string, policies: Policy[]) {
42+
return await this.myContext.delete(`policies/role/default/${policy}`, {
43+
data: policies,
44+
});
45+
}
46+
}
47+
48+
export class Response {
49+
static async removeMetadataFromResponse(
50+
response: APIResponse,
51+
): Promise<unknown[]> {
52+
try {
53+
const responseJson = await response.json();
54+
55+
// Validate that the response is an array
56+
if (!Array.isArray(responseJson)) {
57+
console.warn(
58+
`Expected an array but received: ${JSON.stringify(responseJson)}`,
59+
);
60+
return []; // Return an empty array as a fallback
61+
}
62+
63+
// Clean metadata from the response
64+
const responseClean = responseJson.map((item: { metadata: unknown }) => {
65+
if (item.metadata) {
66+
delete item.metadata;
67+
}
68+
return item;
69+
});
70+
71+
return responseClean;
72+
} catch (error) {
73+
console.error("Error processing API response:", error);
74+
throw new Error("Failed to process the API response");
75+
}
76+
}
77+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Page } from "@red-hat-developer-hub/e2e-test-utils/test";
2+
3+
// here, we spy on the request to get the Backstage token to use APIs
4+
export class RhdhAuthApiHack {
5+
static async getToken(
6+
page: Page,
7+
provider: "oidc" = "oidc",
8+
environment: string = "production",
9+
) {
10+
try {
11+
const response = await page.request.get(
12+
`/api/auth/${provider}/refresh?optional=&scope=&env=${environment}`,
13+
{
14+
headers: {
15+
// eslint-disable-next-line @typescript-eslint/naming-convention
16+
"x-requested-with": "XMLHttpRequest",
17+
},
18+
},
19+
);
20+
21+
if (!response.ok()) {
22+
throw new Error(`HTTP error! Status: ${response.status()}`);
23+
}
24+
25+
const body = await response.json();
26+
27+
if (typeof body?.backstageIdentity?.token === "string") {
28+
return body.backstageIdentity.token;
29+
} else {
30+
throw new TypeError("Token not found in response body");
31+
}
32+
} catch (error) {
33+
console.error("Failed to retrieve the token:", error);
34+
35+
throw error;
36+
}
37+
}
38+
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { Locator, Page } from "@playwright/test";
2+
3+
/**
4+
* ROLES_PAGE_COMPONENTS - Roles list page: edit and delete buttons
5+
*/
6+
export const ROLES_PAGE_COMPONENTS = {
7+
getEditRoleButton: (page: Page, name: string): Locator =>
8+
page.getByTestId(`edit-role-${name}`),
9+
10+
getDeleteRoleButton: (page: Page, name: string): Locator =>
11+
page.getByTestId(`delete-role-${name}`),
12+
13+
getFilterInput: (page: Page): Locator => page.getByPlaceholder("Filter"),
14+
};
15+
16+
/**
17+
* DELETE_ROLE_COMPONENTS - Delete role confirmation dialog
18+
*/
19+
export const DELETE_ROLE_COMPONENTS = {
20+
getRoleNameInput: (page: Page): Locator =>
21+
page.locator('input[name="delete-role"]'),
22+
};
23+
24+
/**
25+
* ROLE_OVERVIEW_COMPONENTS - Role overview page action buttons
26+
*/
27+
export const ROLE_OVERVIEW_COMPONENTS = {
28+
getUpdatePoliciesButton: (page: Page): Locator =>
29+
page.getByTestId("update-policies"),
30+
31+
getUpdateMembersButton: (page: Page): Locator =>
32+
page.getByTestId("update-members"),
33+
};
34+
35+
/**
36+
* ROLE_FORM_COMPONENTS - Role creation / edit form fields
37+
*/
38+
export const ROLE_FORM_COMPONENTS = {
39+
getRoleNameInput: (page: Page): Locator => page.locator('input[name="name"]'),
40+
41+
getRoleOwnerInput: (page: Page): Locator =>
42+
page.locator('textarea[name="owner"]'),
43+
44+
getUsersAndGroupsField: (page: Page): Locator =>
45+
page.locator('input[name="add-users-and-groups"]'),
46+
47+
getMemberOption: (page: Page, label: string): Locator =>
48+
page.locator(`span[data-testid="${label}"]`),
49+
50+
getDropdownToggle: (page: Page): Locator =>
51+
page.getByTestId("ArrowDropDownIcon"),
52+
53+
getSelectPluginsCombobox: (page: Page): Locator =>
54+
page.getByRole("combobox", { name: "Select plugins" }),
55+
56+
getExpandCatalogRow: (page: Page): Locator =>
57+
page.getByTestId("expand-row-catalog"),
58+
59+
getNextButton2: (page: Page): Locator =>
60+
page.getByTestId("nextButton-2").first(),
61+
62+
getPermissionsSelectPlaceholder: (page: Page): Locator =>
63+
page.getByText("Select..."),
64+
};
65+
66+
/**
67+
* CONDITIONAL_RULE_COMPONENTS - Conditions sidebar and rule builder
68+
*/
69+
export const CONDITIONAL_RULE_COMPONENTS = {
70+
getRulesSidebar: (page: Page): Locator => page.getByTestId("rules-sidebar"),
71+
72+
getSaveConditionsButton: (page: Page): Locator =>
73+
page.getByTestId("save-conditions"),
74+
75+
getAnyOfButton: (page: Page): Locator =>
76+
page.getByRole("button", { name: "AnyOf" }),
77+
78+
getNotButton: (page: Page): Locator =>
79+
page.getByRole("button", { name: "Not" }),
80+
81+
getAddRuleButton: (page: Page): Locator =>
82+
page.getByRole("button", { name: "Add rule" }),
83+
84+
getAddNestedConditionButton: (page: Page): Locator =>
85+
page.getByRole("button", { name: "Add Nested Condition" }),
86+
87+
getHasSpecButton: (page: Page): Locator => page.getByText("HAS_SPEC"),
88+
89+
getHasAnnotationButton: (page: Page): Locator =>
90+
page.getByText("HAS_ANNOTATION"),
91+
92+
getHasLabelButton: (page: Page): Locator => page.getByText("HAS_LABEL"),
93+
94+
getIsEntityKindButton: (page: Page): Locator =>
95+
page.getByText("IS_ENTITY_KIND"),
96+
97+
getIsOwnerButton: (page: Page): Locator => page.getByText("IS_OWNER"),
98+
99+
getKeyInput: (page: Page): Locator => page.getByLabel("key *"),
100+
101+
getAnnotationInput: (page: Page): Locator => page.getByLabel("annotation *"),
102+
103+
getLabelInput: (page: Page): Locator => page.getByLabel("label *"),
104+
105+
getRuleBadge: (page: Page, count: string): Locator =>
106+
page.locator('span[class*="MuiBadge-badge"]').filter({ hasText: count }),
107+
};
108+
109+
/**
110+
* SEARCH_COMPONENTS - Search inputs on the roles list page
111+
*/
112+
export const SEARCH_COMPONENTS = {
113+
getAriaLabelSearchInput: (page: Page): Locator =>
114+
page.locator('input[aria-label="Search"]'),
115+
116+
getPlaceholderSearchInput: (page: Page): Locator =>
117+
page.locator('input[placeholder="Search"]'),
118+
};

0 commit comments

Comments
 (0)