@camunda/e2e-test-suite 0.0.1067 → 0.0.1069
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.
|
@@ -43,6 +43,7 @@ declare class OCIdentityRolesPage {
|
|
|
43
43
|
}): Promise<void>;
|
|
44
44
|
private isRolesListOpen;
|
|
45
45
|
private openRolesList;
|
|
46
|
+
private recoverToRolesList;
|
|
46
47
|
clickAdminRole(): Promise<void>;
|
|
47
48
|
assignUserToRole(userId: string): Promise<void>;
|
|
48
49
|
clickAssignUserButton(): Promise<void>;
|
|
@@ -6,7 +6,7 @@ const UtilitiesPage_1 = require("../SM-8.10/UtilitiesPage");
|
|
|
6
6
|
const expectLocatorWithPagination_1 = require("../../utils/assertionHelpers/expectLocatorWithPagination");
|
|
7
7
|
const findLocatorWithPagination_1 = require("../../utils/assertionHelpers/findLocatorWithPagination");
|
|
8
8
|
const submitModalWithRetry_1 = require("../../utils/assertionHelpers/submitModalWithRetry");
|
|
9
|
-
const
|
|
9
|
+
const SEARCH_MAPPING_RULE_LABEL = /^(Search by mapping rule ID|searchByMappingRuleId)$/;
|
|
10
10
|
class OCIdentityRolesPage {
|
|
11
11
|
page;
|
|
12
12
|
selectResourceTypeTab;
|
|
@@ -53,11 +53,14 @@ class OCIdentityRolesPage {
|
|
|
53
53
|
// (role="combobox"), not the searchbox the pre design-system modal
|
|
54
54
|
// rendered; the Mapping rules list page keeps a page-level searchbox with
|
|
55
55
|
// the same accessible name, so scope this to the dialog and accept either
|
|
56
|
-
// role.
|
|
56
|
+
// role. The modal's field resolves its placeholder through the
|
|
57
|
+
// `entitySelection` i18n namespace, which carries no `searchByMappingRuleId`
|
|
58
|
+
// entry, so it is labelled with the raw key instead of the translated
|
|
59
|
+
// string the list page renders - match either form.
|
|
57
60
|
this.mappingSearchBox = this.assignMappingModal
|
|
58
|
-
.getByRole('combobox', { name:
|
|
61
|
+
.getByRole('combobox', { name: SEARCH_MAPPING_RULE_LABEL })
|
|
59
62
|
.or(this.assignMappingModal.getByRole('searchbox', {
|
|
60
|
-
name:
|
|
63
|
+
name: SEARCH_MAPPING_RULE_LABEL,
|
|
61
64
|
}))
|
|
62
65
|
.first();
|
|
63
66
|
this.createRoleButton = page.getByRole('button', {
|
|
@@ -145,8 +148,7 @@ class OCIdentityRolesPage {
|
|
|
145
148
|
.catch(() => false);
|
|
146
149
|
if (gotNotFound) {
|
|
147
150
|
console.warn(`Attempt ${attempt + 1}: Role detail page returned 404 for "${roleId}", retrying...`);
|
|
148
|
-
await this.
|
|
149
|
-
await (0, sleep_1.sleep)(3000);
|
|
151
|
+
await this.recoverToRolesList();
|
|
150
152
|
continue;
|
|
151
153
|
}
|
|
152
154
|
await this.clickMappingRulesTab();
|
|
@@ -157,9 +159,13 @@ class OCIdentityRolesPage {
|
|
|
157
159
|
await (0, test_1.expect)(this.assignMappingModal).toBeVisible({ timeout: 60000 });
|
|
158
160
|
// The design-system search input is a cmdk combobox that only issues
|
|
159
161
|
// its server-side query on real keystrokes, so type the id in rather
|
|
160
|
-
// than setting the value in one shot.
|
|
161
|
-
|
|
162
|
-
|
|
162
|
+
// than setting the value in one shot. Budgeted like every other step
|
|
163
|
+
// here rather than inheriting the 10s actionTimeout: the field mounts
|
|
164
|
+
// with the dialog it was just asserted visible in.
|
|
165
|
+
await this.mappingSearchBox.click({ timeout: 60000 });
|
|
166
|
+
await this.mappingSearchBox.pressSequentially(mappingId, {
|
|
167
|
+
timeout: 60000,
|
|
168
|
+
});
|
|
163
169
|
await this.assignMappingRuleOption(mappingId).click({ timeout: 30000 });
|
|
164
170
|
await this.assignMappingModalAssignRuleButton.click();
|
|
165
171
|
return;
|
|
@@ -167,8 +173,7 @@ class OCIdentityRolesPage {
|
|
|
167
173
|
catch (error) {
|
|
168
174
|
if (attempt < maxRetries - 1) {
|
|
169
175
|
console.warn(`Attempt ${attempt + 1} failed to assign mapping rule to role "${roleId}". Retrying...`);
|
|
170
|
-
await this.
|
|
171
|
-
await (0, sleep_1.sleep)(2000);
|
|
176
|
+
await this.recoverToRolesList();
|
|
172
177
|
}
|
|
173
178
|
else {
|
|
174
179
|
throw error;
|
|
@@ -249,6 +254,28 @@ class OCIdentityRolesPage {
|
|
|
249
254
|
await this.rolesNavTab.click({ timeout });
|
|
250
255
|
await (0, test_1.expect)(this.createRoleButton.first()).toBeVisible({ timeout });
|
|
251
256
|
}
|
|
257
|
+
// `page.goBack()` is one history entry, and the role detail page's tab routes
|
|
258
|
+
// are entries of their own, so backing out of a failed assign-mapping step
|
|
259
|
+
// landed on the same role's Users tab - where the next attempt's roles-table
|
|
260
|
+
// lookup could only time out. Re-enter the list through the nav instead, and
|
|
261
|
+
// dismiss the dialog first: an open design-system dialog sets
|
|
262
|
+
// `pointer-events: none` on the body, which makes the nav link unclickable.
|
|
263
|
+
async recoverToRolesList() {
|
|
264
|
+
const modalOpen = await this.assignMappingModal
|
|
265
|
+
.isVisible()
|
|
266
|
+
.catch(() => false);
|
|
267
|
+
if (modalOpen) {
|
|
268
|
+
await this.page.keyboard.press('Escape');
|
|
269
|
+
const closed = await this.assignMappingModal
|
|
270
|
+
.waitFor({ state: 'hidden', timeout: 15000 })
|
|
271
|
+
.then(() => true)
|
|
272
|
+
.catch(() => false);
|
|
273
|
+
if (!closed) {
|
|
274
|
+
await this.page.reload({ waitUntil: 'domcontentloaded' });
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
await this.openRolesList(60000);
|
|
278
|
+
}
|
|
252
279
|
async clickAdminRole() {
|
|
253
280
|
// The roles table is fetched after the Roles tab click, and other specs in
|
|
254
281
|
// the nightly add roles that can push `admin` onto a later page, so a bare
|
|
@@ -32,8 +32,7 @@ _8_10_1.test.describe('Cluster Setup Tests', () => {
|
|
|
32
32
|
await clusterPage.assertClusterHealthyStatusWithRetry(clusterName, 120000, 1500000);
|
|
33
33
|
}
|
|
34
34
|
else {
|
|
35
|
-
await (0, consoleApiHelpers_1.
|
|
36
|
-
const clusterUuid = await (0, consoleApiHelpers_1.createClusterViaApi)(page, clusterName);
|
|
35
|
+
const clusterUuid = await (0, consoleApiHelpers_1.ensureClusterViaApi)(page, clusterName);
|
|
37
36
|
await (0, consoleApiHelpers_1.waitForClusterHealthyViaApi)(page, clusterUuid);
|
|
38
37
|
await homePage.clickClusters();
|
|
39
38
|
}
|
|
@@ -63,8 +62,7 @@ _8_10_1.test.describe('Cluster Setup Tests', () => {
|
|
|
63
62
|
const apiClientName = 'Test_API_Client' + (await (0, _setup_1.generateRandomStringAsync)(3));
|
|
64
63
|
const mcpClientName = 'MCP_Test_API_Client' + (await (0, _setup_1.generateRandomStringAsync)(3));
|
|
65
64
|
await (0, UtilitiesPage_1.loginWithRetry)(page, loginPage, users_1.testUsers.mainUser, (testInfo.workerIndex + 1) * 1000);
|
|
66
|
-
await (0, consoleApiHelpers_1.
|
|
67
|
-
const clusterUuid = await (0, consoleApiHelpers_1.createClusterViaApi)(page, clusterName);
|
|
65
|
+
const clusterUuid = await (0, consoleApiHelpers_1.ensureClusterViaApi)(page, clusterName);
|
|
68
66
|
await (0, consoleApiHelpers_1.waitForClusterHealthyViaApi)(page, clusterUuid, 1500000);
|
|
69
67
|
await homePage.clickClusters();
|
|
70
68
|
await clusterPage.clickClusterLink(clusterName);
|
|
@@ -99,8 +97,7 @@ _8_10_1.test.describe('Cluster Setup Tests', () => {
|
|
|
99
97
|
await clusterPage.assertClusterHealthyStatusWithRetry(clusterName, 120000, 1500000);
|
|
100
98
|
}
|
|
101
99
|
else {
|
|
102
|
-
await (0, consoleApiHelpers_1.
|
|
103
|
-
const clusterUuid = await (0, consoleApiHelpers_1.createClusterViaApi)(page, clusterName, 'AWS');
|
|
100
|
+
const clusterUuid = await (0, consoleApiHelpers_1.ensureClusterViaApi)(page, clusterName, 'AWS');
|
|
104
101
|
await (0, consoleApiHelpers_1.waitForClusterHealthyViaApi)(page, clusterUuid, 1500000);
|
|
105
102
|
await homePage.clickClusters();
|
|
106
103
|
}
|
|
@@ -64,18 +64,42 @@ async function getUserToken(page) {
|
|
|
64
64
|
const TRANSIENT_API_STATUSES = [429, 500, 502, 503, 504];
|
|
65
65
|
const API_MAX_ATTEMPTS = 5;
|
|
66
66
|
// Overrides the global actionTimeout (10s) for Console API calls only: cluster
|
|
67
|
-
//
|
|
68
|
-
const
|
|
67
|
+
// listings answer in tens of milliseconds, so 15s is generous for a read.
|
|
68
|
+
const API_READ_TIMEOUT_MS = 15000;
|
|
69
|
+
// Cluster mutations are an order of magnitude slower than reads on Console:
|
|
70
|
+
// POST /clusters was measured at 8-12s and DELETE at 7-9s against INT. A 15s
|
|
71
|
+
// ceiling sat inside that band, so the client aborted requests the server went
|
|
72
|
+
// on to complete — leaving a cluster nobody was tracking.
|
|
73
|
+
const API_WRITE_TIMEOUT_MS = 45000;
|
|
74
|
+
// POST is the only non-idempotent verb used here: repeating it creates a second
|
|
75
|
+
// cluster under the same name. Let its timeout surface instead, so the caller
|
|
76
|
+
// can recover by looking the cluster up by name (see createClusterViaApi).
|
|
77
|
+
const TIMEOUT_RETRYABLE_METHODS = ['GET', 'PATCH', 'DELETE'];
|
|
69
78
|
async function clusterApiRequest(page, method, url, data) {
|
|
70
79
|
let response;
|
|
71
80
|
for (let attempt = 1; attempt <= API_MAX_ATTEMPTS; attempt++) {
|
|
72
81
|
const token = await getUserToken(page);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
82
|
+
try {
|
|
83
|
+
response = await page.request.fetch(url, {
|
|
84
|
+
method,
|
|
85
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
86
|
+
timeout: method === 'GET' ? API_READ_TIMEOUT_MS : API_WRITE_TIMEOUT_MS,
|
|
87
|
+
...(data === undefined ? {} : { data }),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
// A timeout or transport error throws rather than returning a status, so
|
|
92
|
+
// the status-based retry below never covered it and API_MAX_ATTEMPTS did
|
|
93
|
+
// not apply to the one failure mode that actually happens.
|
|
94
|
+
console.log(`Console API ${method} ${url} threw ${error.message} ` +
|
|
95
|
+
`(attempt ${attempt}/${API_MAX_ATTEMPTS})`);
|
|
96
|
+
if (!TIMEOUT_RETRYABLE_METHODS.includes(method) ||
|
|
97
|
+
attempt === API_MAX_ATTEMPTS) {
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
await (0, sleep_1.sleep)(attempt * 10000);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
79
103
|
if (!TRANSIENT_API_STATUSES.includes(response.status()))
|
|
80
104
|
return response;
|
|
81
105
|
console.log(`Console API ${method} ${url} returned HTTP ${response.status()} ` +
|
|
@@ -113,8 +137,22 @@ async function findClusterByName(page, clusterName) {
|
|
|
113
137
|
: data.clusters ?? [];
|
|
114
138
|
return clusters.find((c) => c.name === clusterName);
|
|
115
139
|
}
|
|
140
|
+
// A cluster is not always listable the instant its create lands, so poll a few
|
|
141
|
+
// times before concluding a timed-out create really did not happen.
|
|
142
|
+
const CLUSTER_RECOVERY_ATTEMPTS = 6;
|
|
143
|
+
const CLUSTER_RECOVERY_INTERVAL_MS = 5000;
|
|
144
|
+
async function findClusterByNameWithRetry(page, clusterName) {
|
|
145
|
+
for (let attempt = 1; attempt <= CLUSTER_RECOVERY_ATTEMPTS; attempt++) {
|
|
146
|
+
const cluster = await findClusterByName(page, clusterName);
|
|
147
|
+
if (cluster)
|
|
148
|
+
return cluster;
|
|
149
|
+
if (attempt < CLUSTER_RECOVERY_ATTEMPTS) {
|
|
150
|
+
await (0, sleep_1.sleep)(CLUSTER_RECOVERY_INTERVAL_MS);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
116
155
|
async function deleteClusterViaApi(page, clusterName) {
|
|
117
|
-
const token = await getUserToken(page);
|
|
118
156
|
const orgId = getOrgId();
|
|
119
157
|
const deadline = Date.now() + 180000;
|
|
120
158
|
// Re-deleting a cluster that is already terminating can answer 409/400.
|
|
@@ -127,8 +165,14 @@ async function deleteClusterViaApi(page, clusterName) {
|
|
|
127
165
|
if (!cluster)
|
|
128
166
|
return;
|
|
129
167
|
if (!deleteIssued.has(cluster.uuid)) {
|
|
130
|
-
|
|
131
|
-
|
|
168
|
+
// Routed through clusterApiRequest for the write timeout and retries: a
|
|
169
|
+
// bare page.request.delete inherits the 10s actionTimeout, which DELETE
|
|
170
|
+
// /clusters (7-9s at INT) can exceed.
|
|
171
|
+
const response = await clusterApiRequest(page, 'DELETE', `${getConsoleBaseUrl()}/api/orgs/${orgId}/clusters/${cluster.uuid}`);
|
|
172
|
+
// 409 is the retry landing on a cluster the first attempt already put
|
|
173
|
+
// into termination -- the outcome this asked for, not a failure.
|
|
174
|
+
const alreadyTerminating = [404, 409].includes(response.status());
|
|
175
|
+
if (!response.ok() && !alreadyTerminating) {
|
|
132
176
|
throw new Error(`Failed to delete cluster "${clusterName}" (${cluster.uuid}): ` +
|
|
133
177
|
`HTTP ${response.status()} - ${await response.text()}`);
|
|
134
178
|
}
|
|
@@ -315,23 +359,40 @@ async function createClusterViaApi(page, clusterName, region = 'GCP', tasklistV2
|
|
|
315
359
|
generationId = resolveGenerationId();
|
|
316
360
|
}
|
|
317
361
|
const k8sContextId = region.toUpperCase() === 'AWS' ? AWS_REGION_ID : GCP_REGION_ID;
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
362
|
+
let createResponse;
|
|
363
|
+
try {
|
|
364
|
+
createResponse = await clusterApiRequest(page, 'POST', `${getConsoleBaseUrl()}/api/orgs/${orgId}/clusters`, {
|
|
365
|
+
name: clusterName,
|
|
366
|
+
planTypeId: PLAN_TYPE_ID,
|
|
367
|
+
channelId,
|
|
368
|
+
generationId,
|
|
369
|
+
k8sContextId,
|
|
370
|
+
autoUpdate: true,
|
|
371
|
+
stageLabel: 'dev',
|
|
372
|
+
// Always sent explicitly rather than omitted: Console defaults an unset
|
|
373
|
+
// field to v2-enabled for any generation >= 8.8, and the switch to v2 is
|
|
374
|
+
// one-way -- a cluster that ends up v2-enabled can never be brought back
|
|
375
|
+
// to v1 for the rest of its life. v2 cannot run job-worker-based user
|
|
376
|
+
// tasks, draft variables, or public start forms, so the v1 leg
|
|
377
|
+
// (@tasklistV1-tagged suites) depends on this being false, and the v2 leg
|
|
378
|
+
// equally depends on it being true.
|
|
379
|
+
tasklistV2Enabled,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
catch (error) {
|
|
383
|
+
// Console does not cancel the create when the client stops waiting: it
|
|
384
|
+
// answers /clusters slower than the request timeout while still creating
|
|
385
|
+
// the cluster. Adopt what the timed-out POST made instead of re-POSTing,
|
|
386
|
+
// which would leave two clusters under the same name and break
|
|
387
|
+
// clickClusterLink in strict mode.
|
|
388
|
+
const recovered = await findClusterByNameWithRetry(page, clusterName);
|
|
389
|
+
if (!recovered)
|
|
390
|
+
throw error;
|
|
391
|
+
console.log(`Cluster "${clusterName}" create timed out but the cluster exists: ` +
|
|
392
|
+
`clusterId=${recovered.uuid} namespace=${recovered.uuid}-zeebe ` +
|
|
393
|
+
`orgId=${orgId} tasklistMode=${tasklistV2Enabled ? 'v2' : 'v1'}`);
|
|
394
|
+
return recovered.uuid;
|
|
395
|
+
}
|
|
335
396
|
if (createResponse.status() !== 200 && createResponse.status() !== 201) {
|
|
336
397
|
throw new Error(`Failed to create cluster "${clusterName}": ` +
|
|
337
398
|
`HTTP ${createResponse.status()} - ${await createResponse.text()}`);
|