rhdh-e2e-test-utils 1.0.0 → 1.1.0
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/README.md +652 -56
- package/dist/deployment/keycloak/config/keycloak-values.yaml +94 -0
- package/dist/deployment/keycloak/constants.d.ts +29 -0
- package/dist/deployment/keycloak/constants.d.ts.map +1 -0
- package/dist/deployment/keycloak/constants.js +75 -0
- package/dist/deployment/keycloak/deployment.d.ts +89 -0
- package/dist/deployment/keycloak/deployment.d.ts.map +1 -0
- package/dist/deployment/keycloak/deployment.js +437 -0
- package/dist/deployment/keycloak/index.d.ts +2 -0
- package/dist/deployment/keycloak/index.d.ts.map +1 -0
- package/dist/deployment/keycloak/index.js +1 -0
- package/dist/deployment/keycloak/types.d.ts +59 -0
- package/dist/deployment/keycloak/types.d.ts.map +1 -0
- package/dist/deployment/keycloak/types.js +1 -0
- package/dist/deployment/rhdh/config/auth/guest/app-config.yaml +5 -0
- package/dist/deployment/rhdh/config/auth/keycloak/app-config.yaml +19 -0
- package/dist/deployment/rhdh/config/auth/keycloak/dynamic-plugins.yaml +3 -0
- package/dist/deployment/rhdh/config/auth/keycloak/secrets.yaml +12 -0
- package/dist/deployment/rhdh/config/common/app-config-rhdh.yaml +6 -0
- package/dist/deployment/rhdh/config/common/dynamic-plugins.yaml +3 -0
- package/dist/deployment/rhdh/config/common/rhdh-secrets.yaml +7 -0
- package/dist/deployment/rhdh/config/helm/value_file.yaml +7 -0
- package/dist/deployment/rhdh/config/operator/subscription.yaml +21 -0
- package/dist/deployment/rhdh/constants.d.ts +6 -0
- package/dist/deployment/rhdh/constants.d.ts.map +1 -1
- package/dist/deployment/rhdh/constants.js +17 -5
- package/dist/deployment/rhdh/deployment.d.ts +8 -1
- package/dist/deployment/rhdh/deployment.d.ts.map +1 -1
- package/dist/deployment/rhdh/deployment.js +47 -39
- package/dist/deployment/rhdh/index.d.ts +0 -1
- package/dist/deployment/rhdh/index.d.ts.map +1 -1
- package/dist/deployment/rhdh/types.d.ts +4 -1
- package/dist/deployment/rhdh/types.d.ts.map +1 -1
- package/dist/eslint/base.config.d.ts.map +1 -1
- package/dist/eslint/base.config.js +9 -2
- package/dist/playwright/base-config.d.ts +3 -3
- package/dist/playwright/base-config.d.ts.map +1 -1
- package/dist/playwright/base-config.js +5 -4
- 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 +16 -4
- package/dist/playwright/global-setup.d.ts.map +1 -1
- package/dist/playwright/global-setup.js +36 -1
- package/dist/playwright/helpers/accessibility.d.ts +13 -0
- package/dist/playwright/helpers/accessibility.d.ts.map +1 -0
- package/dist/playwright/helpers/accessibility.js +24 -0
- package/dist/playwright/helpers/api-endpoints.d.ts +11 -0
- package/dist/playwright/helpers/api-endpoints.d.ts.map +1 -0
- package/dist/playwright/helpers/api-endpoints.js +15 -0
- package/dist/playwright/helpers/api-helper.d.ts +77 -0
- package/dist/playwright/helpers/api-helper.d.ts.map +1 -0
- package/dist/playwright/helpers/api-helper.js +285 -0
- package/dist/playwright/helpers/common.d.ts +31 -0
- package/dist/playwright/helpers/common.d.ts.map +1 -0
- package/dist/playwright/helpers/common.js +342 -0
- package/dist/playwright/helpers/index.d.ts +5 -0
- package/dist/playwright/helpers/index.d.ts.map +1 -0
- package/dist/playwright/helpers/index.js +4 -0
- package/dist/playwright/helpers/navbar.d.ts +2 -0
- package/dist/playwright/helpers/navbar.d.ts.map +1 -0
- package/dist/playwright/helpers/navbar.js +1 -0
- package/dist/playwright/helpers/ui-helper.d.ts +106 -0
- package/dist/playwright/helpers/ui-helper.d.ts.map +1 -0
- package/dist/playwright/helpers/ui-helper.js +439 -0
- package/dist/playwright/page-objects/global-obj.d.ts +25 -0
- package/dist/playwright/page-objects/global-obj.d.ts.map +1 -0
- package/dist/playwright/page-objects/global-obj.js +24 -0
- package/dist/playwright/page-objects/page-obj.d.ts +41 -0
- package/dist/playwright/page-objects/page-obj.d.ts.map +1 -0
- package/dist/playwright/page-objects/page-obj.js +40 -0
- package/dist/playwright/pages/catalog-import.d.ts +31 -0
- package/dist/playwright/pages/catalog-import.d.ts.map +1 -0
- package/dist/playwright/pages/catalog-import.js +65 -0
- package/dist/playwright/pages/catalog.d.ts +14 -0
- package/dist/playwright/pages/catalog.d.ts.map +1 -0
- package/dist/playwright/pages/catalog.js +37 -0
- package/dist/playwright/pages/extensions.d.ts +38 -0
- package/dist/playwright/pages/extensions.d.ts.map +1 -0
- package/dist/playwright/pages/extensions.js +110 -0
- package/dist/playwright/pages/home-page.d.ts +10 -0
- package/dist/playwright/pages/home-page.d.ts.map +1 -0
- package/dist/playwright/pages/home-page.js +46 -0
- package/dist/playwright/pages/index.d.ts +6 -0
- package/dist/playwright/pages/index.d.ts.map +1 -0
- package/dist/playwright/pages/index.js +5 -0
- package/dist/playwright/pages/notifications.d.ts +24 -0
- package/dist/playwright/pages/notifications.d.ts.map +1 -0
- package/dist/playwright/pages/notifications.js +112 -0
- package/dist/utils/kubernetes-client.d.ts +9 -0
- package/dist/utils/kubernetes-client.d.ts.map +1 -1
- package/dist/utils/kubernetes-client.js +57 -2
- package/dist/utils/merge-yamls.d.ts +25 -4
- package/dist/utils/merge-yamls.d.ts.map +1 -1
- package/dist/utils/merge-yamls.js +52 -12
- package/package.json +19 -6
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { request, expect } from "@playwright/test";
|
|
2
|
+
import { GITHUB_API_ENDPOINTS } from "./api-endpoints.js";
|
|
3
|
+
export class APIHelper {
|
|
4
|
+
static githubAPIVersion = "2022-11-28";
|
|
5
|
+
staticToken = "";
|
|
6
|
+
baseUrl = "";
|
|
7
|
+
useStaticToken = false;
|
|
8
|
+
static async githubRequest(method, url, body) {
|
|
9
|
+
const context = await request.newContext();
|
|
10
|
+
const options = {
|
|
11
|
+
method: method,
|
|
12
|
+
headers: {
|
|
13
|
+
Accept: "application/vnd.github+json",
|
|
14
|
+
Authorization: `Bearer ${process.env.GH_RHDH_QE_USER_TOKEN}`,
|
|
15
|
+
"X-GitHub-Api-Version": this.githubAPIVersion,
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
if (body) {
|
|
19
|
+
options.data = body;
|
|
20
|
+
}
|
|
21
|
+
const response = await context.fetch(url, options);
|
|
22
|
+
return response;
|
|
23
|
+
}
|
|
24
|
+
static async getGithubPaginatedRequest(url, pageNo = 1, response = []) {
|
|
25
|
+
const fullUrl = `${url}&page=${pageNo}`;
|
|
26
|
+
const result = await this.githubRequest("GET", fullUrl);
|
|
27
|
+
const body = await result.json();
|
|
28
|
+
if (!Array.isArray(body)) {
|
|
29
|
+
throw new Error(`Expected array but got ${typeof body}: ${JSON.stringify(body)}`);
|
|
30
|
+
}
|
|
31
|
+
if (body.length === 0) {
|
|
32
|
+
return response;
|
|
33
|
+
}
|
|
34
|
+
response = [...response, ...body];
|
|
35
|
+
return await this.getGithubPaginatedRequest(url, pageNo + 1, response);
|
|
36
|
+
}
|
|
37
|
+
static async createGitHubRepo(owner, repoName) {
|
|
38
|
+
const response = await APIHelper.githubRequest("POST", GITHUB_API_ENDPOINTS.createRepo(owner), {
|
|
39
|
+
name: repoName,
|
|
40
|
+
private: false,
|
|
41
|
+
});
|
|
42
|
+
expect(response.status() === 201 || response.ok()).toBeTruthy();
|
|
43
|
+
}
|
|
44
|
+
static async createGitHubRepoWithFile(owner, repoName, filename, fileContent) {
|
|
45
|
+
// Create the repository
|
|
46
|
+
await APIHelper.createGitHubRepo(owner, repoName);
|
|
47
|
+
// Add the specified file
|
|
48
|
+
await APIHelper.createFileInRepo(owner, repoName, filename, fileContent, `Add ${filename} file`);
|
|
49
|
+
}
|
|
50
|
+
static async createFileInRepo(owner, repoName, filePath, content, commitMessage, branch = "main") {
|
|
51
|
+
const encodedContent = Buffer.from(content).toString("base64");
|
|
52
|
+
const response = await APIHelper.githubRequest("PUT", `${GITHUB_API_ENDPOINTS.contents(owner, repoName)}/${filePath}`, {
|
|
53
|
+
message: commitMessage,
|
|
54
|
+
content: encodedContent,
|
|
55
|
+
branch: branch,
|
|
56
|
+
});
|
|
57
|
+
expect(response.status() === 201 || response.ok()).toBeTruthy();
|
|
58
|
+
}
|
|
59
|
+
static async initCommit(owner, repo, branch = "main") {
|
|
60
|
+
const content = Buffer.from("This is the initial commit for the repository.").toString("base64");
|
|
61
|
+
const response = await APIHelper.githubRequest("PUT", `${GITHUB_API_ENDPOINTS.contents(owner, repo)}/initial-commit.md`, {
|
|
62
|
+
message: "Initial commit",
|
|
63
|
+
content: content,
|
|
64
|
+
branch: branch,
|
|
65
|
+
});
|
|
66
|
+
expect(response.status() === 201 || response.ok()).toBeTruthy();
|
|
67
|
+
}
|
|
68
|
+
static async deleteGitHubRepo(owner, repoName) {
|
|
69
|
+
await APIHelper.githubRequest("DELETE", GITHUB_API_ENDPOINTS.deleteRepo(owner, repoName));
|
|
70
|
+
}
|
|
71
|
+
static async mergeGitHubPR(owner, repoName, pullNumber) {
|
|
72
|
+
await APIHelper.githubRequest("PUT", GITHUB_API_ENDPOINTS.mergePR(owner, repoName, pullNumber));
|
|
73
|
+
}
|
|
74
|
+
static async getGitHubPRs(owner, repoName, state, paginated = false) {
|
|
75
|
+
const url = GITHUB_API_ENDPOINTS.pull(owner, repoName, state);
|
|
76
|
+
if (paginated) {
|
|
77
|
+
return await APIHelper.getGithubPaginatedRequest(url);
|
|
78
|
+
}
|
|
79
|
+
const response = await APIHelper.githubRequest("GET", url);
|
|
80
|
+
return response.json();
|
|
81
|
+
}
|
|
82
|
+
static async getfileContentFromPR(owner, repoName, pr, filename) {
|
|
83
|
+
const response = await APIHelper.githubRequest("GET", GITHUB_API_ENDPOINTS.pullFiles(owner, repoName, pr));
|
|
84
|
+
const fileRawUrl = (await response.json()).find((file) => file.filename === filename).raw_url;
|
|
85
|
+
const rawFileContent = await (await APIHelper.githubRequest("GET", fileRawUrl)).text();
|
|
86
|
+
return rawFileContent;
|
|
87
|
+
}
|
|
88
|
+
async getGuestToken() {
|
|
89
|
+
const context = await request.newContext();
|
|
90
|
+
const response = await context.post("/api/auth/guest/refresh");
|
|
91
|
+
expect(response.status()).toBe(200);
|
|
92
|
+
const data = await response.json();
|
|
93
|
+
return data.backstageIdentity.token;
|
|
94
|
+
}
|
|
95
|
+
async getGuestAuthHeader() {
|
|
96
|
+
const token = await this.getGuestToken();
|
|
97
|
+
const headers = {
|
|
98
|
+
Authorization: `Bearer ${token}`,
|
|
99
|
+
};
|
|
100
|
+
return headers;
|
|
101
|
+
}
|
|
102
|
+
async setStaticToken(token) {
|
|
103
|
+
this.useStaticToken = true;
|
|
104
|
+
this.staticToken = "Bearer " + token;
|
|
105
|
+
}
|
|
106
|
+
async setBaseUrl(url) {
|
|
107
|
+
this.baseUrl = url;
|
|
108
|
+
}
|
|
109
|
+
static async apiRequestWithStaticToken(method, url, staticToken, body) {
|
|
110
|
+
const context = await request.newContext();
|
|
111
|
+
const options = {
|
|
112
|
+
method: method,
|
|
113
|
+
headers: {
|
|
114
|
+
Accept: "application/json",
|
|
115
|
+
Authorization: `${staticToken}`,
|
|
116
|
+
},
|
|
117
|
+
...(body && { data: body }),
|
|
118
|
+
};
|
|
119
|
+
const response = await context.fetch(url, options);
|
|
120
|
+
return response;
|
|
121
|
+
}
|
|
122
|
+
async getAllCatalogUsersFromAPI() {
|
|
123
|
+
const url = `${this.baseUrl}/api/catalog/entities/by-query?orderField=metadata.name%2Casc&filter=kind%3Duser`;
|
|
124
|
+
const token = this.useStaticToken ? this.staticToken : "";
|
|
125
|
+
const response = await APIHelper.apiRequestWithStaticToken("GET", url, token);
|
|
126
|
+
return response.json();
|
|
127
|
+
}
|
|
128
|
+
async getAllCatalogLocationsFromAPI() {
|
|
129
|
+
const url = `${this.baseUrl}/api/catalog/entities/by-query?orderField=metadata.name%2Casc&filter=kind%3Dlocation`;
|
|
130
|
+
const token = this.useStaticToken ? this.staticToken : "";
|
|
131
|
+
const response = await APIHelper.apiRequestWithStaticToken("GET", url, token);
|
|
132
|
+
return response.json();
|
|
133
|
+
}
|
|
134
|
+
async getAllCatalogGroupsFromAPI() {
|
|
135
|
+
const url = `${this.baseUrl}/api/catalog/entities/by-query?orderField=metadata.name%2Casc&filter=kind%3Dgroup`;
|
|
136
|
+
const token = this.useStaticToken ? this.staticToken : "";
|
|
137
|
+
const response = await APIHelper.apiRequestWithStaticToken("GET", url, token);
|
|
138
|
+
return response.json();
|
|
139
|
+
}
|
|
140
|
+
async getGroupEntityFromAPI(group) {
|
|
141
|
+
const url = `${this.baseUrl}/api/catalog/entities/by-name/group/default/${group}`;
|
|
142
|
+
const token = this.useStaticToken ? this.staticToken : "";
|
|
143
|
+
const response = await APIHelper.apiRequestWithStaticToken("GET", url, token);
|
|
144
|
+
return response.json();
|
|
145
|
+
}
|
|
146
|
+
async getCatalogUserFromAPI(user) {
|
|
147
|
+
const url = `${this.baseUrl}/api/catalog/entities/by-name/user/default/${user}`;
|
|
148
|
+
const token = this.useStaticToken ? this.staticToken : "";
|
|
149
|
+
const response = await APIHelper.apiRequestWithStaticToken("GET", url, token);
|
|
150
|
+
return response.json();
|
|
151
|
+
}
|
|
152
|
+
async deleteUserEntityFromAPI(user) {
|
|
153
|
+
const r = await this.getCatalogUserFromAPI(user);
|
|
154
|
+
if (!r.metadata || !r.metadata.uid) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const url = `${this.baseUrl}/api/catalog/entities/by-uid/${r.metadata.uid}`;
|
|
158
|
+
const token = this.useStaticToken ? this.staticToken : "";
|
|
159
|
+
const response = await APIHelper.apiRequestWithStaticToken("DELETE", url, token);
|
|
160
|
+
return response.statusText;
|
|
161
|
+
}
|
|
162
|
+
async getCatalogGroupFromAPI(group) {
|
|
163
|
+
const url = `${this.baseUrl}/api/catalog/entities/by-name/group/default/${group}`;
|
|
164
|
+
const token = this.useStaticToken ? this.staticToken : "";
|
|
165
|
+
const response = await APIHelper.apiRequestWithStaticToken("GET", url, token);
|
|
166
|
+
return response.json();
|
|
167
|
+
}
|
|
168
|
+
async deleteGroupEntityFromAPI(group) {
|
|
169
|
+
const r = await this.getCatalogGroupFromAPI(group);
|
|
170
|
+
const url = `${this.baseUrl}/api/catalog/entities/by-uid/${r.metadata.uid}`;
|
|
171
|
+
const token = this.useStaticToken ? this.staticToken : "";
|
|
172
|
+
const response = await APIHelper.apiRequestWithStaticToken("DELETE", url, token);
|
|
173
|
+
return response.statusText;
|
|
174
|
+
}
|
|
175
|
+
async scheduleEntityRefreshFromAPI(entity, kind, token) {
|
|
176
|
+
const url = `${this.baseUrl}/api/catalog/refresh`;
|
|
177
|
+
const reqBody = { entityRef: `${kind}:default/${entity}` };
|
|
178
|
+
const responseRefresh = await APIHelper.apiRequestWithStaticToken("POST", url, token, reqBody);
|
|
179
|
+
return responseRefresh.status();
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Fetches the UID of an entity by its name from the Backstage catalog.
|
|
183
|
+
*
|
|
184
|
+
* @param name - The name of the entity (e.g., 'hello-world-2').
|
|
185
|
+
* @returns The UID string if found, otherwise undefined.
|
|
186
|
+
*/
|
|
187
|
+
static async getEntityUidByName(name) {
|
|
188
|
+
const baseUrl = process.env.BASE_URL;
|
|
189
|
+
const url = `${baseUrl}/api/catalog/entities/by-name/template/default/${name}`;
|
|
190
|
+
const context = await request.newContext();
|
|
191
|
+
const response = await context.get(url);
|
|
192
|
+
if (response.status() !== 200) {
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
const data = await response.json();
|
|
196
|
+
return data?.metadata?.uid;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Deletes a location from the Backstage catalog by its UID.
|
|
200
|
+
*
|
|
201
|
+
* @param uid - The UID of the location to delete.
|
|
202
|
+
* @returns The status code of the delete operation.
|
|
203
|
+
*/
|
|
204
|
+
static async deleteLocationByUid(uid) {
|
|
205
|
+
const baseUrl = process.env.BASE_URL;
|
|
206
|
+
const url = `${baseUrl}/api/catalog/locations/${uid}`;
|
|
207
|
+
const context = await request.newContext();
|
|
208
|
+
const response = await context.delete(url);
|
|
209
|
+
return response.status();
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Fetches the UID of a Template entity by its name and namespace from the Backstage catalog.
|
|
213
|
+
*
|
|
214
|
+
* @param name - The name of the template entity (e.g., 'hello-world-2').
|
|
215
|
+
* @param namespace - The namespace of the template entity (default: 'default').
|
|
216
|
+
* @returns The UID string if found, otherwise undefined.
|
|
217
|
+
*/
|
|
218
|
+
static async getTemplateEntityUidByName(name, namespace = "default") {
|
|
219
|
+
const baseUrl = process.env.BASE_URL;
|
|
220
|
+
const url = `${baseUrl}/api/catalog/locations/by-entity/template/${namespace}/${name}`;
|
|
221
|
+
const context = await request.newContext();
|
|
222
|
+
const response = await context.get(url);
|
|
223
|
+
if (response.status() === 200) {
|
|
224
|
+
const data = await response.json();
|
|
225
|
+
return data?.metadata?.uid;
|
|
226
|
+
}
|
|
227
|
+
if (response.status() === 404) {
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Deletes an entity location from the Backstage catalog by its ID.
|
|
234
|
+
*
|
|
235
|
+
* @param id - The ID of the entity to delete.
|
|
236
|
+
* @returns The status code of the delete operation.
|
|
237
|
+
*/
|
|
238
|
+
static async deleteEntityLocationById(id) {
|
|
239
|
+
const baseUrl = process.env.BASE_URL;
|
|
240
|
+
const url = `${baseUrl}/api/catalog/locations/${id}`;
|
|
241
|
+
const context = await request.newContext();
|
|
242
|
+
const response = await context.delete(url);
|
|
243
|
+
return response.status();
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Registers a new location in the Backstage catalog.
|
|
247
|
+
*
|
|
248
|
+
* @param target - The target URL of the location to register.
|
|
249
|
+
* @returns The status code of the registration operation.
|
|
250
|
+
*/
|
|
251
|
+
static async registerLocation(target) {
|
|
252
|
+
const baseUrl = process.env.BASE_URL;
|
|
253
|
+
const url = `${baseUrl}/api/catalog/locations`;
|
|
254
|
+
const context = await request.newContext();
|
|
255
|
+
const response = await context.post(url, {
|
|
256
|
+
data: {
|
|
257
|
+
type: "url",
|
|
258
|
+
target,
|
|
259
|
+
},
|
|
260
|
+
headers: {
|
|
261
|
+
"Content-Type": "application/json",
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
return response.status();
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Fetches the ID of a location from the Backstage catalog by its target URL.
|
|
268
|
+
*
|
|
269
|
+
* @param target - The target URL of the location to search for.
|
|
270
|
+
* @returns The ID string if found, otherwise undefined.
|
|
271
|
+
*/
|
|
272
|
+
static async getLocationIdByTarget(target) {
|
|
273
|
+
const baseUrl = process.env.BASE_URL;
|
|
274
|
+
const url = `${baseUrl}/api/catalog/locations`;
|
|
275
|
+
const context = await request.newContext();
|
|
276
|
+
const response = await context.get(url);
|
|
277
|
+
if (response.status() !== 200) {
|
|
278
|
+
return undefined;
|
|
279
|
+
}
|
|
280
|
+
const data = await response.json();
|
|
281
|
+
// data is expected to be an array of objects with a 'data' property
|
|
282
|
+
const location = (Array.isArray(data) ? data : []).find((entry) => entry?.data?.target === target);
|
|
283
|
+
return location?.data?.id;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { UIhelper } from "./ui-helper.js";
|
|
2
|
+
import type { Browser, Page, TestInfo } from "@playwright/test";
|
|
3
|
+
export declare class LoginHelper {
|
|
4
|
+
page: Page;
|
|
5
|
+
uiHelper: UIhelper;
|
|
6
|
+
constructor(page: Page);
|
|
7
|
+
loginAsGuest(): Promise<void>;
|
|
8
|
+
signOut(): Promise<void>;
|
|
9
|
+
private logintoGithub;
|
|
10
|
+
logintoKeycloak(userid: string, password: string): Promise<void>;
|
|
11
|
+
loginAsKeycloakUser(userid?: string, password?: string): Promise<void>;
|
|
12
|
+
loginAsGithubUser(userid?: string): Promise<void>;
|
|
13
|
+
checkAndReauthorizeGithubApp(): Promise<void>;
|
|
14
|
+
googleSignIn(email: string): Promise<void>;
|
|
15
|
+
checkAndClickOnGHloginPopup(force?: boolean): Promise<void>;
|
|
16
|
+
getButtonSelector(label: string): string;
|
|
17
|
+
getLoginBtnSelector(): string;
|
|
18
|
+
clickOnGHloginPopup(): Promise<void>;
|
|
19
|
+
getGitHub2FAOTP(userid: string): string;
|
|
20
|
+
getGoogle2FAOTP(): string;
|
|
21
|
+
keycloakLogin(username: string, password: string): Promise<"Already logged in" | "Login successful" | "User does not exist">;
|
|
22
|
+
private handleGitHubPopupLogin;
|
|
23
|
+
githubLogin(username: string, password: string, twofactor: string): Promise<string>;
|
|
24
|
+
githubLoginFromSettingsPage(username: string, password: string, twofactor: string): Promise<string>;
|
|
25
|
+
microsoftAzureLogin(username: string, password: string): Promise<"Already logged in" | "Login successful" | "User does not exist">;
|
|
26
|
+
}
|
|
27
|
+
export declare function setupBrowser(browser: Browser, testInfo: TestInfo): Promise<{
|
|
28
|
+
page: Page;
|
|
29
|
+
context: import("playwright-core").BrowserContext;
|
|
30
|
+
}>;
|
|
31
|
+
//# sourceMappingURL=common.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../src/playwright/helpers/common.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAG1C,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAOhE,qBAAa,WAAW;IACtB,IAAI,EAAE,IAAI,CAAC;IACX,QAAQ,EAAE,QAAQ,CAAC;gBAEP,IAAI,EAAE,IAAI;IAKhB,YAAY;IAcZ,OAAO;YAMC,aAAa;IAmCrB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;IAYhD,mBAAmB,CACvB,MAAM,GAAE,MAAkC,EAC1C,QAAQ,GAAE,MAAkC;IASxC,iBAAiB,CAAC,MAAM,GAAE,MAAyC;IA4BnE,4BAA4B;IAqB5B,YAAY,CAAC,KAAK,EAAE,MAAM;IA2B1B,2BAA2B,CAAC,KAAK,UAAQ;IAU/C,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM;IAIxC,mBAAmB,IAAI,MAAM;IAIvB,mBAAmB;IAgBzB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM;IAcvC,eAAe,IAAI,MAAM;IAKnB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;YAqCxC,sBAAsB;IAoD9B,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;IAYjE,2BAA2B,CAC/B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM;IAYb,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM;CA2C7D;AAED,wBAAsB,YAAY,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ;;;GAWtE"}
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { UIhelper } from "./ui-helper.js";
|
|
2
|
+
import { authenticator } from "otplib";
|
|
3
|
+
import { test, expect } from "@playwright/test";
|
|
4
|
+
import { SETTINGS_PAGE_COMPONENTS } from "../page-objects/page-obj.js";
|
|
5
|
+
import { UI_HELPER_ELEMENTS } from "../page-objects/global-obj.js";
|
|
6
|
+
import * as path from "path";
|
|
7
|
+
import * as fs from "fs";
|
|
8
|
+
import { DEFAULT_USERS } from "../../deployment/keycloak/constants.js";
|
|
9
|
+
export class LoginHelper {
|
|
10
|
+
page;
|
|
11
|
+
uiHelper;
|
|
12
|
+
constructor(page) {
|
|
13
|
+
this.page = page;
|
|
14
|
+
this.uiHelper = new UIhelper(page);
|
|
15
|
+
}
|
|
16
|
+
async loginAsGuest() {
|
|
17
|
+
await this.page.goto("/");
|
|
18
|
+
await this.uiHelper.waitForLoad(240000);
|
|
19
|
+
// TODO - Remove it after https://issues.redhat.com/browse/RHIDP-2043. A Dynamic plugin for Guest Authentication Provider needs to be created
|
|
20
|
+
this.page.on("dialog", async (dialog) => {
|
|
21
|
+
console.log(`Dialog message: ${dialog.message()}`);
|
|
22
|
+
await dialog.accept();
|
|
23
|
+
});
|
|
24
|
+
await this.uiHelper.verifyHeading("Select a sign-in method");
|
|
25
|
+
await this.uiHelper.clickButton("Enter");
|
|
26
|
+
await this.page.waitForSelector("nav a", { timeout: 10_000 });
|
|
27
|
+
}
|
|
28
|
+
async signOut() {
|
|
29
|
+
await this.page.click(SETTINGS_PAGE_COMPONENTS.userSettingsMenu);
|
|
30
|
+
await this.page.click(SETTINGS_PAGE_COMPONENTS.signOut);
|
|
31
|
+
await this.uiHelper.verifyHeading("Select a sign-in method");
|
|
32
|
+
}
|
|
33
|
+
async logintoGithub(userid) {
|
|
34
|
+
await this.page.goto("https://github.com/login");
|
|
35
|
+
await this.page.waitForSelector("#login_field");
|
|
36
|
+
await this.page.fill("#login_field", userid);
|
|
37
|
+
switch (userid) {
|
|
38
|
+
case process.env.GH_USER_ID:
|
|
39
|
+
await this.page.fill("#password", process.env.GH_USER_PASS);
|
|
40
|
+
break;
|
|
41
|
+
case process.env.GH_USER2_ID:
|
|
42
|
+
await this.page.fill("#password", process.env.GH_USER2_PASS);
|
|
43
|
+
break;
|
|
44
|
+
default:
|
|
45
|
+
throw new Error("Invalid User ID");
|
|
46
|
+
}
|
|
47
|
+
await this.page.click('[value="Sign in"]');
|
|
48
|
+
await this.page.fill("#app_totp", this.getGitHub2FAOTP(userid));
|
|
49
|
+
test.setTimeout(130000);
|
|
50
|
+
if ((await this.uiHelper.isTextVisible("The two-factor code you entered has already been used")) ||
|
|
51
|
+
(await this.uiHelper.isTextVisible("too many codes have been submitted", 3000))) {
|
|
52
|
+
await this.page.waitForTimeout(60000);
|
|
53
|
+
await this.page.fill("#app_totp", this.getGitHub2FAOTP(userid));
|
|
54
|
+
}
|
|
55
|
+
await this.page.waitForTimeout(3_000);
|
|
56
|
+
}
|
|
57
|
+
async logintoKeycloak(userid, password) {
|
|
58
|
+
await new Promise((resolve) => {
|
|
59
|
+
this.page.once("popup", async (popup) => {
|
|
60
|
+
await popup.waitForLoadState();
|
|
61
|
+
await popup.locator("#username").fill(userid);
|
|
62
|
+
await popup.locator("#password").fill(password);
|
|
63
|
+
await popup.locator("#kc-login").click();
|
|
64
|
+
resolve();
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
async loginAsKeycloakUser(userid = DEFAULT_USERS[0].username, password = DEFAULT_USERS[0].password) {
|
|
69
|
+
await this.page.goto("/");
|
|
70
|
+
await this.uiHelper.waitForLoad(240000);
|
|
71
|
+
await this.uiHelper.clickButton("Sign In");
|
|
72
|
+
await this.logintoKeycloak(userid, password);
|
|
73
|
+
await this.page.waitForSelector("nav a", { timeout: 10_000 });
|
|
74
|
+
}
|
|
75
|
+
async loginAsGithubUser(userid = process.env.GH_USER_ID) {
|
|
76
|
+
const sessionFileName = `authState_${userid}.json`;
|
|
77
|
+
// Check if a session file for this specific user already exists
|
|
78
|
+
if (fs.existsSync(sessionFileName)) {
|
|
79
|
+
// Load and reuse existing authentication state
|
|
80
|
+
const cookies = JSON.parse(fs.readFileSync(sessionFileName, "utf-8")).cookies;
|
|
81
|
+
await this.page.context().addCookies(cookies);
|
|
82
|
+
console.log(`Reusing existing authentication state for user: ${userid}`);
|
|
83
|
+
await this.page.goto("/");
|
|
84
|
+
await this.uiHelper.waitForLoad(12000);
|
|
85
|
+
await this.uiHelper.clickButton("Sign In");
|
|
86
|
+
await this.checkAndReauthorizeGithubApp();
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
// Perform login if no session file exists, then save the state
|
|
90
|
+
await this.logintoGithub(userid);
|
|
91
|
+
await this.page.goto("/");
|
|
92
|
+
await this.uiHelper.waitForLoad(240000);
|
|
93
|
+
await this.uiHelper.clickButton("Sign In");
|
|
94
|
+
await this.checkAndReauthorizeGithubApp();
|
|
95
|
+
await this.page.waitForSelector("nav a", { timeout: 10_000 });
|
|
96
|
+
await this.page.context().storageState({ path: sessionFileName });
|
|
97
|
+
console.log(`Authentication state saved for user: ${userid}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async checkAndReauthorizeGithubApp() {
|
|
101
|
+
await new Promise((resolve) => {
|
|
102
|
+
this.page.once("popup", async (popup) => {
|
|
103
|
+
await popup.waitForLoadState();
|
|
104
|
+
// Check for popup closure for up to 10 seconds before proceeding
|
|
105
|
+
for (let attempts = 0; attempts < 10 && !popup.isClosed(); attempts++) {
|
|
106
|
+
await this.page.waitForTimeout(1000); // Using page here because if the popup closes automatically, it throws an error during the wait
|
|
107
|
+
}
|
|
108
|
+
const locator = popup.locator("button.js-oauth-authorize-btn");
|
|
109
|
+
if (!popup.isClosed() && (await locator.isVisible())) {
|
|
110
|
+
await popup.locator("body").click();
|
|
111
|
+
await locator.waitFor();
|
|
112
|
+
await locator.click();
|
|
113
|
+
}
|
|
114
|
+
resolve();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
async googleSignIn(email) {
|
|
119
|
+
await new Promise((resolve) => {
|
|
120
|
+
this.page.once("popup", async (popup) => {
|
|
121
|
+
await popup.waitForLoadState();
|
|
122
|
+
const locator = popup
|
|
123
|
+
.getByRole("link", { name: email, exact: false })
|
|
124
|
+
.first();
|
|
125
|
+
await popup.waitForTimeout(3000);
|
|
126
|
+
await locator.waitFor({ state: "visible" });
|
|
127
|
+
await locator.click({ force: true });
|
|
128
|
+
await popup.waitForTimeout(3000);
|
|
129
|
+
await popup
|
|
130
|
+
.locator("[name=Passwd]")
|
|
131
|
+
.fill(process.env.GOOGLE_USER_PASS);
|
|
132
|
+
await popup.locator("[name=Passwd]").press("Enter");
|
|
133
|
+
await popup.waitForTimeout(3500);
|
|
134
|
+
await popup.locator("[name=totpPin]").fill(this.getGoogle2FAOTP());
|
|
135
|
+
await popup.locator("[name=totpPin]").press("Enter");
|
|
136
|
+
await popup
|
|
137
|
+
.getByRole("button", { name: /Continue|Weiter/ })
|
|
138
|
+
.click({ timeout: 60000 });
|
|
139
|
+
resolve();
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
async checkAndClickOnGHloginPopup(force = false) {
|
|
144
|
+
const frameLocator = this.page.getByLabel("Login Required");
|
|
145
|
+
try {
|
|
146
|
+
await frameLocator.waitFor({ state: "visible", timeout: 2000 });
|
|
147
|
+
await this.clickOnGHloginPopup();
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
if (force)
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
getButtonSelector(label) {
|
|
155
|
+
return `${UI_HELPER_ELEMENTS.MuiButtonLabel}:has-text("${label}")`;
|
|
156
|
+
}
|
|
157
|
+
getLoginBtnSelector() {
|
|
158
|
+
return 'MuiListItem-root li.MuiListItem-root button.MuiButton-root:has(span.MuiButton-label:text("Log in"))';
|
|
159
|
+
}
|
|
160
|
+
async clickOnGHloginPopup() {
|
|
161
|
+
const isLoginRequiredVisible = await this.uiHelper.isTextVisible("Sign in");
|
|
162
|
+
if (isLoginRequiredVisible) {
|
|
163
|
+
await this.uiHelper.clickButton("Sign in");
|
|
164
|
+
await this.uiHelper.clickButton("Log in");
|
|
165
|
+
await this.checkAndReauthorizeGithubApp();
|
|
166
|
+
await this.page.waitForSelector(this.getLoginBtnSelector(), {
|
|
167
|
+
state: "detached",
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
console.log('"Log in" button is not visible. Skipping login popup actions.');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
getGitHub2FAOTP(userid) {
|
|
175
|
+
const secrets = {
|
|
176
|
+
[process.env.GH_USER_ID]: process.env.GH_2FA_SECRET,
|
|
177
|
+
[process.env.GH_USER2_ID]: process.env.GH_USER2_2FA_SECRET,
|
|
178
|
+
};
|
|
179
|
+
const secret = secrets[userid];
|
|
180
|
+
if (!secret) {
|
|
181
|
+
throw new Error("Invalid User ID");
|
|
182
|
+
}
|
|
183
|
+
return authenticator.generate(secret);
|
|
184
|
+
}
|
|
185
|
+
getGoogle2FAOTP() {
|
|
186
|
+
const secret = process.env.GOOGLE_2FA_SECRET;
|
|
187
|
+
return authenticator.generate(secret);
|
|
188
|
+
}
|
|
189
|
+
async keycloakLogin(username, password) {
|
|
190
|
+
await this.page.goto("/");
|
|
191
|
+
await this.page.waitForSelector('p:has-text("Sign in using OIDC")');
|
|
192
|
+
const [popup] = await Promise.all([
|
|
193
|
+
this.page.waitForEvent("popup"),
|
|
194
|
+
this.uiHelper.clickButton("Sign In"),
|
|
195
|
+
]);
|
|
196
|
+
await popup.waitForLoadState("domcontentloaded");
|
|
197
|
+
// Check if popup closes automatically (already logged in)
|
|
198
|
+
try {
|
|
199
|
+
await popup.waitForEvent("close", { timeout: 5000 });
|
|
200
|
+
return "Already logged in";
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// Popup didn't close, proceed with login
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
await popup.locator("#username").click();
|
|
207
|
+
await popup.locator("#username").fill(username);
|
|
208
|
+
await popup.locator("#password").fill(password);
|
|
209
|
+
await popup.locator("[name=login]").click({ timeout: 5000 });
|
|
210
|
+
await popup.waitForEvent("close", { timeout: 2000 });
|
|
211
|
+
return "Login successful";
|
|
212
|
+
}
|
|
213
|
+
catch (e) {
|
|
214
|
+
const usernameError = popup.locator("id=input-error");
|
|
215
|
+
if (await usernameError.isVisible()) {
|
|
216
|
+
await popup.close();
|
|
217
|
+
return "User does not exist";
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
throw e;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async handleGitHubPopupLogin(popup, username, password, twofactor) {
|
|
225
|
+
await expect(async () => {
|
|
226
|
+
await popup.waitForLoadState("domcontentloaded");
|
|
227
|
+
expect(popup).toBeTruthy();
|
|
228
|
+
}).toPass({
|
|
229
|
+
intervals: [5_000, 10_000],
|
|
230
|
+
timeout: 20 * 1000,
|
|
231
|
+
});
|
|
232
|
+
// Check if popup closes automatically
|
|
233
|
+
try {
|
|
234
|
+
await popup.waitForEvent("close", { timeout: 5000 });
|
|
235
|
+
return "Already logged in";
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
// Popup didn't close, proceed with login
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
await popup.locator("#login_field").click({ timeout: 5000 });
|
|
242
|
+
await popup.locator("#login_field").fill(username, { timeout: 5000 });
|
|
243
|
+
const cookieLocator = popup.locator("#wcpConsentBannerCtrl");
|
|
244
|
+
if (await cookieLocator.isVisible()) {
|
|
245
|
+
await popup.click('button:has-text("Reject")', { timeout: 5000 });
|
|
246
|
+
}
|
|
247
|
+
await popup.locator("#password").click({ timeout: 5000 });
|
|
248
|
+
await popup.locator("#password").fill(password, { timeout: 5000 });
|
|
249
|
+
await popup
|
|
250
|
+
.locator("[type='submit'][value='Sign in']:not(webauthn-status *)")
|
|
251
|
+
.first()
|
|
252
|
+
.click({ timeout: 5000 });
|
|
253
|
+
const twofactorcode = authenticator.generate(twofactor);
|
|
254
|
+
await popup.locator("#app_totp").click({ timeout: 5000 });
|
|
255
|
+
await popup.locator("#app_totp").fill(twofactorcode, { timeout: 5000 });
|
|
256
|
+
await popup.waitForEvent("close", { timeout: 20000 });
|
|
257
|
+
return "Login successful";
|
|
258
|
+
}
|
|
259
|
+
catch (e) {
|
|
260
|
+
const authorization = popup.locator("button.js-oauth-authorize-btn");
|
|
261
|
+
if (await authorization.isVisible()) {
|
|
262
|
+
await authorization.click();
|
|
263
|
+
return "Login successful";
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
throw e;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
async githubLogin(username, password, twofactor) {
|
|
271
|
+
await this.page.goto("/");
|
|
272
|
+
await this.page.waitForSelector('p:has-text("Sign in using GitHub")');
|
|
273
|
+
const [popup] = await Promise.all([
|
|
274
|
+
this.page.waitForEvent("popup"),
|
|
275
|
+
this.uiHelper.clickButton("Sign In"),
|
|
276
|
+
]);
|
|
277
|
+
return this.handleGitHubPopupLogin(popup, username, password, twofactor);
|
|
278
|
+
}
|
|
279
|
+
async githubLoginFromSettingsPage(username, password, twofactor) {
|
|
280
|
+
await this.page.goto("/settings/auth-providers");
|
|
281
|
+
const [popup] = await Promise.all([
|
|
282
|
+
this.page.waitForEvent("popup"),
|
|
283
|
+
this.page.getByTitle("Sign in to GitHub").click(),
|
|
284
|
+
this.uiHelper.clickButton("Log in"),
|
|
285
|
+
]);
|
|
286
|
+
return this.handleGitHubPopupLogin(popup, username, password, twofactor);
|
|
287
|
+
}
|
|
288
|
+
async microsoftAzureLogin(username, password) {
|
|
289
|
+
await this.page.goto("/");
|
|
290
|
+
await this.page.waitForSelector('p:has-text("Sign in using Microsoft")');
|
|
291
|
+
const [popup] = await Promise.all([
|
|
292
|
+
this.page.waitForEvent("popup"),
|
|
293
|
+
this.uiHelper.clickButton("Sign In"),
|
|
294
|
+
]);
|
|
295
|
+
await popup.waitForLoadState("domcontentloaded");
|
|
296
|
+
if (popup.url().startsWith(process.env.BASE_URL)) {
|
|
297
|
+
// an active microsoft session is already logged in and the popup will automatically close
|
|
298
|
+
return "Already logged in";
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
try {
|
|
302
|
+
await popup.locator("[name=loginfmt]").click();
|
|
303
|
+
await popup
|
|
304
|
+
.locator("[name=loginfmt]")
|
|
305
|
+
.fill(username, { timeout: 5000 });
|
|
306
|
+
await popup
|
|
307
|
+
.locator('[type=submit]:has-text("Next")')
|
|
308
|
+
.click({ timeout: 5000 });
|
|
309
|
+
await popup.locator("[name=passwd]").click();
|
|
310
|
+
await popup.locator("[name=passwd]").fill(password, { timeout: 5000 });
|
|
311
|
+
await popup
|
|
312
|
+
.locator('[type=submit]:has-text("Sign in")')
|
|
313
|
+
.click({ timeout: 5000 });
|
|
314
|
+
await popup
|
|
315
|
+
.locator('[type=button]:has-text("No")')
|
|
316
|
+
.click({ timeout: 15000 });
|
|
317
|
+
return "Login successful";
|
|
318
|
+
}
|
|
319
|
+
catch (e) {
|
|
320
|
+
const usernameError = popup.locator("id=usernameError");
|
|
321
|
+
if (await usernameError.isVisible()) {
|
|
322
|
+
return "User does not exist";
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
throw e;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
export async function setupBrowser(browser, testInfo) {
|
|
332
|
+
const context = await browser.newContext({
|
|
333
|
+
recordVideo: {
|
|
334
|
+
dir: `test-results/${path
|
|
335
|
+
.parse(testInfo.file)
|
|
336
|
+
.name.replace(".spec", "")}/${testInfo.titlePath[1]}`,
|
|
337
|
+
size: { width: 1920, height: 1080 },
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
const page = await context.newPage();
|
|
341
|
+
return { page, context };
|
|
342
|
+
}
|
|
@@ -0,0 +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"}
|