@camunda/e2e-test-suite 0.0.1248 → 0.0.1250

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.
Files changed (35) hide show
  1. package/dist/pages/8.7/AppsPage.d.ts +7 -3
  2. package/dist/pages/8.7/AppsPage.js +187 -3
  3. package/dist/pages/8.7/ClusterPage.js +22 -2
  4. package/dist/pages/8.7/ClusterSecretsPage.js +22 -1
  5. package/dist/pages/8.7/HomePage.d.ts +2 -0
  6. package/dist/pages/8.7/HomePage.js +70 -6
  7. package/dist/pages/8.7/LoginPage.js +15 -0
  8. package/dist/pages/8.7/ModelerCreatePage.d.ts +4 -0
  9. package/dist/pages/8.7/ModelerCreatePage.js +59 -5
  10. package/dist/pages/8.7/ModelerHomePage.d.ts +11 -0
  11. package/dist/pages/8.7/ModelerHomePage.js +184 -10
  12. package/dist/pages/8.7/ProcessApplicationPage.js +27 -7
  13. package/dist/pages/8.7/UtilitiesPage.js +54 -1
  14. package/dist/pages/8.9/AppsPage.d.ts +2 -0
  15. package/dist/pages/8.9/AppsPage.js +114 -0
  16. package/dist/pages/8.9/ClusterDetailsPage.d.ts +3 -0
  17. package/dist/pages/8.9/ClusterDetailsPage.js +92 -25
  18. package/dist/pages/8.9/ClusterPage.js +49 -8
  19. package/dist/pages/8.9/ClusterSecretsPage.js +47 -2
  20. package/dist/pages/8.9/HomePage.d.ts +3 -0
  21. package/dist/pages/8.9/HomePage.js +44 -5
  22. package/dist/pages/8.9/LoginPage.d.ts +1 -0
  23. package/dist/pages/8.9/LoginPage.js +19 -0
  24. package/dist/pages/8.9/ModelerHomePage.d.ts +7 -0
  25. package/dist/pages/8.9/ModelerHomePage.js +122 -3
  26. package/dist/pages/8.9/UtilitiesPage.js +60 -2
  27. package/dist/tests/8.7/test-setup.spec.js +18 -5
  28. package/dist/tests/8.7/web-modeler-user-flows.spec.js +14 -14
  29. package/dist/tests/8.9/test-setup.spec.js +11 -4
  30. package/dist/utils/authErrorClassification.d.ts +5 -0
  31. package/dist/utils/authErrorClassification.js +49 -1
  32. package/dist/utils/consoleApiHelpers.d.ts +1 -0
  33. package/dist/utils/consoleApiHelpers.js +183 -30
  34. package/dist/utils/resetSession.js +7 -0
  35. 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
- const token = await page.evaluate(() => {
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
- 'ensure loginWithRetry has been called before any cluster API operations');
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
- 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.
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('/');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.1248",
3
+ "version": "0.0.1250",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",