@camunda/e2e-test-suite 0.0.1247 → 0.0.1249
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/pages/8.9/AppsPage.d.ts +2 -0
- package/dist/pages/8.9/AppsPage.js +114 -0
- package/dist/pages/8.9/ClusterDetailsPage.d.ts +3 -0
- package/dist/pages/8.9/ClusterDetailsPage.js +92 -25
- package/dist/pages/8.9/ClusterPage.js +49 -8
- package/dist/pages/8.9/ClusterSecretsPage.js +47 -2
- package/dist/pages/8.9/HomePage.d.ts +3 -0
- package/dist/pages/8.9/HomePage.js +44 -5
- package/dist/pages/8.9/LoginPage.d.ts +1 -0
- package/dist/pages/8.9/LoginPage.js +19 -0
- package/dist/pages/8.9/ModelerHomePage.d.ts +7 -0
- package/dist/pages/8.9/ModelerHomePage.js +122 -3
- package/dist/pages/8.9/UtilitiesPage.js +60 -2
- package/dist/pages/SM-8.10/NavigationPage.js +1 -0
- package/dist/tests/8.9/test-setup.spec.js +11 -4
- package/dist/utils/authErrorClassification.d.ts +5 -0
- package/dist/utils/authErrorClassification.js +49 -1
- package/dist/utils/consoleApiHelpers.d.ts +1 -0
- package/dist/utils/consoleApiHelpers.js +183 -30
- package/dist/utils/resetSession.js +7 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.waitForAiFeaturesEnabledViaApi = exports.areAiFeaturesEnabledViaApi = exports.waitForClusterRestartHealthyViaApi = exports.waitForClusterHealthyViaApi = exports.ensureClientAuditLoggingViaApi = exports.ensureTasklistV2Enabled = exports.ensureClusterViaApi = exports.createClusterViaApi = exports.deleteClusterViaApi = void 0;
|
|
3
|
+
exports.waitForAiFeaturesEnabledViaApi = exports.areAiFeaturesEnabledViaApi = exports.waitForClusterRestartHealthyViaApi = exports.waitForClusterHealthyViaApi = exports.ensureClientAuditLoggingViaApi = exports.ensureTasklistV2Enabled = exports.ensureClusterViaApi = exports.createClusterViaApi = exports.deleteClusterViaApi = exports.resetHubBearerToken = void 0;
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
5
|
const sleep_1 = require("./sleep");
|
|
6
6
|
const PLAN_TYPE_ID = '574cde5f-54ae-43c1-851a-e6fb4f03423a';
|
|
@@ -31,10 +31,103 @@ function getOrgId() {
|
|
|
31
31
|
throw new Error('ORG_ID env var is not set');
|
|
32
32
|
return orgId;
|
|
33
33
|
}
|
|
34
|
+
// Camunda Hub, which replaced the Console frontend on SaaS, keeps its Auth0
|
|
35
|
+
// token out of localStorage but sends it as a Bearer header on its own calls
|
|
36
|
+
// to the Console/Accounts APIs (it polls console.cloud.*/external/events), so
|
|
37
|
+
// remember the latest one seen per page.
|
|
38
|
+
const observedBearerTokens = new WeakMap();
|
|
39
|
+
const tokenObservedPages = new WeakSet();
|
|
40
|
+
// Bumped by resetHubBearerToken() on every session reset. Both the listener
|
|
41
|
+
// below and getHubBearerToken()'s waitForRequest fallback read this
|
|
42
|
+
// synchronously right before their async header/token extraction and
|
|
43
|
+
// re-check it after: a request that started under the previous session but
|
|
44
|
+
// resolves after the epoch moved on is discarded instead of being cached or
|
|
45
|
+
// returned as if it belonged to the new session.
|
|
46
|
+
const sessionEpoch = new WeakMap();
|
|
47
|
+
const CLOUD_API_HOST = /^https:\/\/(console|accounts)\.cloud\./;
|
|
48
|
+
const BEARER_WAIT_TIMEOUT_MS = 60000;
|
|
49
|
+
function bearerTokenOf(url, authorization) {
|
|
50
|
+
if (!CLOUD_API_HOST.test(url) || !authorization?.startsWith('Bearer ')) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return authorization.slice('Bearer '.length);
|
|
54
|
+
}
|
|
55
|
+
function observeBearerTokens(page) {
|
|
56
|
+
if (tokenObservedPages.has(page)) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
tokenObservedPages.add(page);
|
|
60
|
+
// allHeaders(): headers() only exposes the provisional request headers,
|
|
61
|
+
// which do not carry Authorization.
|
|
62
|
+
page.on('request', async (request) => {
|
|
63
|
+
if (!CLOUD_API_HOST.test(request.url())) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const epochAtRequestTime = sessionEpoch.get(page) ?? 0;
|
|
67
|
+
const headers = await request.allHeaders().catch(() => undefined);
|
|
68
|
+
const token = bearerTokenOf(request.url(), headers?.authorization);
|
|
69
|
+
if (token && sessionEpoch.get(page) === epochAtRequestTime) {
|
|
70
|
+
observedBearerTokens.set(page, token);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
// Exported so LoginPage can call this before a new login begins. Without it,
|
|
75
|
+
// a page that signs in as a second user keeps answering getHubBearerToken()
|
|
76
|
+
// with the first user's cached token until some later console.cloud/
|
|
77
|
+
// accounts.cloud request happens to fire -- API calls made in between (e.g.
|
|
78
|
+
// role-change verification right after switching users) would silently run
|
|
79
|
+
// as the wrong user instead of failing loudly.
|
|
80
|
+
//
|
|
81
|
+
// Only the cached VALUE is cleared -- tokenObservedPages (and the listener it
|
|
82
|
+
// guards) is deliberately left alone; the listener doesn't care which user's
|
|
83
|
+
// token it's writing, so removing its registration marker here would attach
|
|
84
|
+
// a second page.on('request') listener on top of the still-attached first
|
|
85
|
+
// one on the next observeBearerTokens() call, and a third after the next
|
|
86
|
+
// reset, and so on. Bumping sessionEpoch guards against an in-flight request
|
|
87
|
+
// from the PREVIOUS session resolving its allHeaders() after this clear
|
|
88
|
+
// runs, which would otherwise repopulate the cache with the stale token.
|
|
89
|
+
function resetHubBearerToken(page) {
|
|
90
|
+
observedBearerTokens.delete(page);
|
|
91
|
+
sessionEpoch.set(page, (sessionEpoch.get(page) ?? 0) + 1);
|
|
92
|
+
}
|
|
93
|
+
exports.resetHubBearerToken = resetHubBearerToken;
|
|
94
|
+
async function getHubBearerToken(page) {
|
|
95
|
+
observeBearerTokens(page);
|
|
96
|
+
const deadline = Date.now() + BEARER_WAIT_TIMEOUT_MS;
|
|
97
|
+
while (Date.now() < deadline) {
|
|
98
|
+
const cached = observedBearerTokens.get(page);
|
|
99
|
+
if (cached) {
|
|
100
|
+
return cached;
|
|
101
|
+
}
|
|
102
|
+
// This waitForRequest -- unlike the page.on('request') listener above --
|
|
103
|
+
// resolves and extracts a token directly, bypassing the cache entirely.
|
|
104
|
+
// Capture the epoch before waiting so a session reset that lands while
|
|
105
|
+
// this await is pending (letting an in-flight request from the OLD
|
|
106
|
+
// session satisfy the host-only predicate below) is caught: such a
|
|
107
|
+
// request's token is discarded instead of being returned as if it were
|
|
108
|
+
// the new session's.
|
|
109
|
+
const epochBeforeWait = sessionEpoch.get(page) ?? 0;
|
|
110
|
+
const request = await page
|
|
111
|
+
.waitForRequest((r) => CLOUD_API_HOST.test(r.url()), {
|
|
112
|
+
timeout: Math.max(deadline - Date.now(), 1),
|
|
113
|
+
})
|
|
114
|
+
.catch(() => null);
|
|
115
|
+
if (!request) {
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
const headers = await request.allHeaders().catch(() => undefined);
|
|
119
|
+
const token = bearerTokenOf(request.url(), headers?.authorization);
|
|
120
|
+
if (token && sessionEpoch.get(page) === epochBeforeWait) {
|
|
121
|
+
return token;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return observedBearerTokens.get(page) ?? null;
|
|
125
|
+
}
|
|
34
126
|
// Auth0 SPA JS stores tokens in localStorage under keys starting with @@auth0spajs@@
|
|
35
127
|
// cacheLocation: 'localstorage' is configured in the Console frontend auth store
|
|
36
128
|
async function getUserToken(page) {
|
|
37
|
-
|
|
129
|
+
observeBearerTokens(page);
|
|
130
|
+
const storedToken = await page.evaluate(() => {
|
|
38
131
|
for (const key of Object.keys(localStorage)) {
|
|
39
132
|
if (key.startsWith('@@auth0spajs@@')) {
|
|
40
133
|
try {
|
|
@@ -49,9 +142,11 @@ async function getUserToken(page) {
|
|
|
49
142
|
}
|
|
50
143
|
return null;
|
|
51
144
|
});
|
|
145
|
+
const token = storedToken ?? (await getHubBearerToken(page));
|
|
52
146
|
if (!token) {
|
|
53
|
-
throw new Error('Could not extract access token from browser localStorage
|
|
54
|
-
'
|
|
147
|
+
throw new Error('Could not extract an access token from either browser localStorage ' +
|
|
148
|
+
'or an observed Hub Bearer header — ensure loginWithRetry has been ' +
|
|
149
|
+
'called before any cluster API operations');
|
|
55
150
|
}
|
|
56
151
|
return token;
|
|
57
152
|
}
|
|
@@ -263,6 +358,27 @@ function pickGenerationByName(channels, generationName) {
|
|
|
263
358
|
throw new Error(`Source generation "${generationName}" not found in any channel's ` +
|
|
264
359
|
`allowed generations`);
|
|
265
360
|
}
|
|
361
|
+
// Resolve the version-keyed snapshot generation live by name. The snapshot is
|
|
362
|
+
// named "<minor> Cross Component Test Suite" (see .github/c8_versions.json) and
|
|
363
|
+
// is periodically recreated in Console INT with a fresh UUID -- and can move
|
|
364
|
+
// channels -- which invalidates the hardcoded GENERATION_MAP/CHANNEL_ID pairing.
|
|
365
|
+
// Matching is whitespace-normalised and case-insensitive so a cosmetic rename
|
|
366
|
+
// does not miss it; the recreated-and-renamed predecessor ("Old - <minor> ...")
|
|
367
|
+
// no longer normalises to the wanted name and is also dropped from
|
|
368
|
+
// allowedGenerations, so it is never picked.
|
|
369
|
+
function pickSnapshotGeneration(channels, minorVersion) {
|
|
370
|
+
const normalise = (value) => value.replace(/\s+/g, ' ').trim().toLowerCase();
|
|
371
|
+
const wanted = normalise(`${minorVersion} Cross Component Test Suite`);
|
|
372
|
+
for (const channel of channels) {
|
|
373
|
+
const match = (channel.allowedGenerations ?? []).find((generation) => normalise(generation.name) === wanted);
|
|
374
|
+
if (match) {
|
|
375
|
+
console.log(`Resolved snapshot generation "${match.name}" for Camunda ` +
|
|
376
|
+
`${minorVersion} from channel "${channel.name}" (${match.uuid})`);
|
|
377
|
+
return { channelId: channel.uuid, generationId: match.uuid };
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return undefined;
|
|
381
|
+
}
|
|
266
382
|
// Migration source cluster target: honor an explicit Source Generation (passed
|
|
267
383
|
// via CLUSTER_VERSION) when set; otherwise auto-select the latest stable
|
|
268
384
|
// generation for the source minor version.
|
|
@@ -341,6 +457,11 @@ async function createClusterViaApi(page, clusterName, region = 'GCP', tasklistV2
|
|
|
341
457
|
// operator's chosen generation is used instead of the version-keyed snapshot.
|
|
342
458
|
let channelId = CHANNEL_ID;
|
|
343
459
|
let generationId;
|
|
460
|
+
// Only the default (snapshot) path uses the hardcoded GENERATION_MAP/CHANNEL_ID
|
|
461
|
+
// pairing that a Console generation rotation can invalidate; the explicit
|
|
462
|
+
// paths already resolve live (or carry a real per-run UUID), so their 400s
|
|
463
|
+
// are genuine and must not be retried.
|
|
464
|
+
let usedSnapshotDefault = false;
|
|
344
465
|
const requestedGeneration = requestedGenerationName();
|
|
345
466
|
if (process.env.CLUSTER_GENERATION_ID) {
|
|
346
467
|
generationId = process.env.CLUSTER_GENERATION_ID;
|
|
@@ -357,34 +478,15 @@ async function createClusterViaApi(page, clusterName, region = 'GCP', tasklistV2
|
|
|
357
478
|
}
|
|
358
479
|
else {
|
|
359
480
|
generationId = resolveGenerationId();
|
|
481
|
+
usedSnapshotDefault = true;
|
|
360
482
|
}
|
|
361
483
|
const k8sContextId = region.toUpperCase() === 'AWS' ? AWS_REGION_ID : GCP_REGION_ID;
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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.
|
|
484
|
+
// Console does not cancel the create when the client stops waiting: it
|
|
485
|
+
// answers /clusters slower than the request timeout while still creating the
|
|
486
|
+
// cluster. Adopt what the timed-out POST made instead of re-POSTing, which
|
|
487
|
+
// would leave two clusters under the same name and break clickClusterLink in
|
|
488
|
+
// strict mode.
|
|
489
|
+
const recoverTimedOutCreate = async (error) => {
|
|
388
490
|
const recovered = await findClusterByNameWithRetry(page, clusterName);
|
|
389
491
|
if (!recovered)
|
|
390
492
|
throw error;
|
|
@@ -392,6 +494,57 @@ async function createClusterViaApi(page, clusterName, region = 'GCP', tasklistV2
|
|
|
392
494
|
`clusterId=${recovered.uuid} namespace=${recovered.uuid}-zeebe ` +
|
|
393
495
|
`orgId=${orgId} tasklistMode=${tasklistV2Enabled ? 'v2' : 'v1'}`);
|
|
394
496
|
return recovered.uuid;
|
|
497
|
+
};
|
|
498
|
+
const attemptCreate = (chId, genId) => clusterApiRequest(page, 'POST', `${getConsoleBaseUrl()}/api/orgs/${orgId}/clusters`, {
|
|
499
|
+
name: clusterName,
|
|
500
|
+
planTypeId: PLAN_TYPE_ID,
|
|
501
|
+
channelId: chId,
|
|
502
|
+
generationId: genId,
|
|
503
|
+
k8sContextId,
|
|
504
|
+
autoUpdate: true,
|
|
505
|
+
stageLabel: 'dev',
|
|
506
|
+
// Always sent explicitly rather than omitted: Console defaults an unset
|
|
507
|
+
// field to v2-enabled for any generation >= 8.8, and the switch to v2 is
|
|
508
|
+
// one-way -- a cluster that ends up v2-enabled can never be brought back
|
|
509
|
+
// to v1 for the rest of its life. v2 cannot run job-worker-based user
|
|
510
|
+
// tasks, draft variables, or public start forms, so the v1 leg
|
|
511
|
+
// (@tasklistV1-tagged suites) depends on this being false, and the v2 leg
|
|
512
|
+
// equally depends on it being true.
|
|
513
|
+
tasklistV2Enabled,
|
|
514
|
+
});
|
|
515
|
+
let createResponse;
|
|
516
|
+
try {
|
|
517
|
+
createResponse = await attemptCreate(channelId, generationId);
|
|
518
|
+
}
|
|
519
|
+
catch (error) {
|
|
520
|
+
return recoverTimedOutCreate(error);
|
|
521
|
+
}
|
|
522
|
+
// The version-keyed snapshot generation is periodically recreated in Console
|
|
523
|
+
// INT with a fresh UUID (and can change channel), which invalidates the
|
|
524
|
+
// hardcoded GENERATION_MAP/CHANNEL_ID pairing and rejects the create with
|
|
525
|
+
// HTTP 400 "Generation is not part of allowed generations of selected
|
|
526
|
+
// channel". On the default (snapshot) path, re-resolve the generation live by
|
|
527
|
+
// name and retry once so a rotation self-heals instead of failing every setup
|
|
528
|
+
// until the UUID is hand-updated. A missing live match falls through to the
|
|
529
|
+
// error below, so the outcome is never worse than before.
|
|
530
|
+
if (usedSnapshotDefault &&
|
|
531
|
+
createResponse.status() === 400 &&
|
|
532
|
+
(await createResponse.text()).includes('allowed generations')) {
|
|
533
|
+
const minorVersion = resolveMinorVersion();
|
|
534
|
+
const snapshot = pickSnapshotGeneration(await fetchClusterChannels(page), minorVersion);
|
|
535
|
+
if (snapshot &&
|
|
536
|
+
(snapshot.channelId !== channelId ||
|
|
537
|
+
snapshot.generationId !== generationId)) {
|
|
538
|
+
console.log(`Snapshot generation for Camunda ${minorVersion} was rotated; ` +
|
|
539
|
+
`retrying cluster create with live-resolved generation ` +
|
|
540
|
+
`${snapshot.generationId} on channel ${snapshot.channelId}`);
|
|
541
|
+
try {
|
|
542
|
+
createResponse = await attemptCreate(snapshot.channelId, snapshot.generationId);
|
|
543
|
+
}
|
|
544
|
+
catch (error) {
|
|
545
|
+
return recoverTimedOutCreate(error);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
395
548
|
}
|
|
396
549
|
if (createResponse.status() !== 200 && createResponse.status() !== 201) {
|
|
397
550
|
throw new Error(`Failed to create cluster "${clusterName}": ` +
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.resetSessionAll = exports.resetSession = void 0;
|
|
4
|
+
const consoleApiHelpers_1 = require("./consoleApiHelpers");
|
|
4
5
|
async function resetSession(browser, page) {
|
|
5
6
|
const context = browser.contexts()[0];
|
|
6
7
|
await context.clearCookies();
|
|
8
|
+
// See loginWithRetry in UtilitiesPage.ts: the Hub bearer-token cache in
|
|
9
|
+
// consoleApiHelpers.ts is keyed by Page for the tab's whole lifetime, so a
|
|
10
|
+
// session reset that re-authenticates as a different user must clear it
|
|
11
|
+
// too, or getUserToken() can return the previous user's cached token.
|
|
12
|
+
(0, consoleApiHelpers_1.resetHubBearerToken)(page);
|
|
7
13
|
await page.reload();
|
|
8
14
|
await page.waitForLoadState('domcontentloaded');
|
|
9
15
|
await page.goto('/');
|
|
@@ -17,6 +23,7 @@ async function resetSessionAll(context, page) {
|
|
|
17
23
|
localStorage.clear();
|
|
18
24
|
sessionStorage.clear();
|
|
19
25
|
});
|
|
26
|
+
(0, consoleApiHelpers_1.resetHubBearerToken)(page);
|
|
20
27
|
}
|
|
21
28
|
await page.reload();
|
|
22
29
|
await page.goto('/');
|