@camunda/e2e-test-suite 0.0.982 → 0.0.984

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.
@@ -101,9 +101,9 @@ declare class ModelerCreatePage {
101
101
  clickSecondPlacedGateway(): Promise<void>;
102
102
  private reloadAndWaitForCanvas;
103
103
  /**
104
- * Detects if the page has been redirected to the Keycloak login page
105
- * (session expiry) or is in the middle of an OIDC redirect, and performs
106
- * a re-login if necessary.
104
+ * Detects if the page has been redirected to a login page (session expiry)
105
+ * or is in the middle of an OIDC redirect, and performs a re-login if
106
+ * necessary.
107
107
  */
108
108
  private handleSessionExpiry;
109
109
  clickStartInstanceMainButton(): Promise<void>;
@@ -483,9 +483,9 @@ class ModelerCreatePage {
483
483
  await element.waitFor({ state: 'visible', timeout: 120000 });
484
484
  }
485
485
  /**
486
- * Detects if the page has been redirected to the Keycloak login page
487
- * (session expiry) or is in the middle of an OIDC redirect, and performs
488
- * a re-login if necessary.
486
+ * Detects if the page has been redirected to a login page (session expiry)
487
+ * or is in the middle of an OIDC redirect, and performs a re-login if
488
+ * necessary.
489
489
  */
490
490
  async handleSessionExpiry() {
491
491
  const currentUrl = this.page.url();
@@ -501,12 +501,30 @@ class ModelerCreatePage {
501
501
  const loginPage = new LoginPage_1.LoginPage(this.page);
502
502
  const keycloakBanner = this.page.locator('body#keycloak-bg[data-page-id="login-login"]');
503
503
  const usernameField = this.page.locator('#username');
504
- // Check if we landed on a login page (Keycloak form)
505
- const isOnLoginPage = await keycloakBanner
504
+ // `loginPage.usernameInput` covers the Camunda Identity form, which carries
505
+ // neither the Keycloak body id nor a `#username` input, so neither of the
506
+ // two locators above matches it.
507
+ const loginForm = keycloakBanner
506
508
  .or(usernameField)
507
- .isVisible({ timeout: 5000 })
508
- .catch(() => false);
509
- if (!isOnLoginPage) {
509
+ .or(loginPage.usernameInput)
510
+ .first();
511
+ // `Run` and the login form are mutually exclusive, so race them rather than
512
+ // sampling one: an expired session redirects through the OIDC callback
513
+ // after the reload has already resolved, so a point-in-time visibility
514
+ // check still sees the Modeler document and misses the login form that
515
+ // replaces it a moment later. The race also costs nothing on the happy
516
+ // path — it returns as soon as `Run` renders.
517
+ const landedOn = await Promise.race([
518
+ loginForm
519
+ .waitFor({ state: 'visible', timeout: 15000 })
520
+ .then(() => 'login')
521
+ .catch(() => 'unknown'),
522
+ this.startInstanceMainButton
523
+ .waitFor({ state: 'visible', timeout: 15000 })
524
+ .then(() => 'app')
525
+ .catch(() => 'unknown'),
526
+ ]);
527
+ if (landedOn !== 'login') {
510
528
  return;
511
529
  }
512
530
  console.warn('clickStartInstanceMainButton: session expired — performing re-login');
@@ -4,6 +4,7 @@ exports.TaskPanelPage = void 0;
4
4
  const test_1 = require("@playwright/test");
5
5
  const constants_1 = require("../../utils/constants");
6
6
  const sleep_1 = require("../../utils/sleep");
7
+ const gatewayErrors_1 = require("../../utils/gatewayErrors");
7
8
  class TaskPanelPage {
8
9
  page;
9
10
  availableTasks;
@@ -39,6 +40,9 @@ class TaskPanelPage {
39
40
  }
40
41
  async openTask(name, assignButton = true) {
41
42
  const maxAttempts = 10;
43
+ // Shared by every attempt so that a sustained Tasklist outage still fails
44
+ // this call rather than stretching it by ten times this budget.
45
+ const gatewayRecoveryDeadline = Date.now() + constants_1._1_SECOND_IN_MS * 60;
42
46
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
43
47
  try {
44
48
  await this.page.waitForLoadState('domcontentloaded');
@@ -56,7 +60,7 @@ class TaskPanelPage {
56
60
  catch (error) {
57
61
  if (attempt < maxAttempts) {
58
62
  console.warn(`Attempt ${attempt} failed. Reloading page...`);
59
- await this.page.reload();
63
+ await (0, gatewayErrors_1.reloadPastGatewayError)(this.page, gatewayRecoveryDeadline);
60
64
  await this.page.waitForLoadState('domcontentloaded');
61
65
  }
62
66
  else {
@@ -4,6 +4,8 @@ exports.modelDiagramFromFile = exports.modelAndRunConnectorsTimerEventDiagram =
4
4
  const test_1 = require("@playwright/test");
5
5
  const fileUpload_1 = require("../../utils/fileUpload");
6
6
  const sleep_1 = require("../../utils/sleep");
7
+ const gatewayErrors_1 = require("../../utils/gatewayErrors");
8
+ const constants_1 = require("../../utils/constants");
7
9
  // Tasklist renders "No permission for Tasklist" as a terminal page: it reads the
8
10
  // permissions off the token minted at login and never re-evaluates them, so a
9
11
  // user whose Identity role assignment has not propagated yet stays stuck there.
@@ -61,6 +63,9 @@ async function createAndRunProcess(modelerHomePage, modelerCreatePage, processNa
61
63
  }
62
64
  exports.createAndRunProcess = createAndRunProcess;
63
65
  async function completeTaskWithRetry(page, taskPanelPage, taskDetailsPage, taskName, taskPriority, maxRetries = 5) {
66
+ // Shared by every attempt so that a sustained Tasklist outage still fails
67
+ // this call rather than stretching it by five times this budget.
68
+ const gatewayRecoveryDeadline = Date.now() + constants_1._1_SECOND_IN_MS * 60;
64
69
  for (let attempt = 0; attempt < maxRetries; attempt++) {
65
70
  try {
66
71
  await page.waitForLoadState('domcontentloaded');
@@ -98,7 +103,7 @@ async function completeTaskWithRetry(page, taskPanelPage, taskDetailsPage, taskN
98
103
  catch (error) {
99
104
  console.warn(`Attempt ${attempt + 1} failed for completing task ${taskName}:`, error instanceof Error ? error.message : error);
100
105
  if (attempt < maxRetries - 1) {
101
- await page.reload();
106
+ await (0, gatewayErrors_1.reloadPastGatewayError)(page, gatewayRecoveryDeadline);
102
107
  await page.waitForLoadState('domcontentloaded');
103
108
  await (0, sleep_1.sleep)(1000);
104
109
  }
@@ -33,6 +33,7 @@ const assertUnauthorizedResponseBody = async (response) => {
33
33
  };
34
34
  _8_10_1.test.describe('API optimize SaaS Tests', () => {
35
35
  let optimizeCookie;
36
+ let csrfToken;
36
37
  let optimizeBearerToken;
37
38
  let collectionIdValue;
38
39
  let dashboardIdValue;
@@ -41,16 +42,18 @@ _8_10_1.test.describe('API optimize SaaS Tests', () => {
41
42
  let baseUrl;
42
43
  _8_10_1.test.beforeAll(async ({ browser, request }) => {
43
44
  const page = await browser.newPage();
44
- optimizeCookie = await (0, apiHelpers_1.getOptimizeCoockie)(page);
45
+ ({ optimizeCookie, csrfToken } = await (0, apiHelpers_1.loginToOptimizeSession)(page));
45
46
  await page.close();
46
47
  optimizeBearerToken = await (0, apiHelpers_1.authSaasAPI)(process.env.OPTIMIZE_API_TOKEN_AUDIENCE);
47
48
  collectionIdValue = await (0, apiHelpers_1.createCollection)(request, {
48
49
  name: await (0, randomName_1.randomNameAgregator)('Test Collection'),
49
50
  optimizeCookie,
51
+ csrfToken,
50
52
  });
51
53
  baseUrl = process.env.CAMUNDA_OPTIMIZE_BASE_URL;
52
54
  await (0, apiHelpers_1.updateCollectionScope)(request, {
53
55
  optimizeCookie,
56
+ csrfToken,
54
57
  collectionId: collectionIdValue,
55
58
  data: [
56
59
  {
@@ -64,10 +67,12 @@ _8_10_1.test.describe('API optimize SaaS Tests', () => {
64
67
  dashboardIdValue = await (0, apiHelpers_1.createDashboard)(request, {
65
68
  name: dashboardNameValue,
66
69
  optimizeCookie,
70
+ csrfToken,
67
71
  collectionId: collectionIdValue,
68
72
  });
69
73
  reportId = await (0, apiHelpers_1.createSingleProcessReport)(request, {
70
74
  optimizeCookie,
75
+ csrfToken,
71
76
  collectionId: collectionIdValue,
72
77
  name: 'Blank report',
73
78
  definitions: [
@@ -189,6 +194,7 @@ _8_10_1.test.describe('API optimize SaaS Tests', () => {
189
194
  const dashboardToDeleteId = await (0, apiHelpers_1.createDashboard)(request, {
190
195
  name: await (0, randomName_1.randomNameAgregator)('Dashboard To Delete'),
191
196
  optimizeCookie,
197
+ csrfToken,
192
198
  collectionId: collectionIdValue,
193
199
  });
194
200
  await _8_10_1.test.step('Delete dashboard successfully (200)', async () => {
@@ -284,6 +290,7 @@ _8_10_1.test.describe('API optimize SaaS Tests', () => {
284
290
  (0, _8_10_1.test)('Delete report and verify it is no longer retrievable', async ({ request, }) => {
285
291
  const reportToDeleteId = await (0, apiHelpers_1.createSingleProcessReport)(request, {
286
292
  optimizeCookie,
293
+ csrfToken,
287
294
  collectionId: collectionIdValue,
288
295
  name: await (0, randomName_1.randomNameAgregator)('Report To Delete'),
289
296
  definitions: [
@@ -375,6 +382,7 @@ _8_10_1.test.describe('API optimize SaaS Tests', () => {
375
382
  });
376
383
  _8_10_1.test.describe('API optimize SaaS Tests - Conditional Events', () => {
377
384
  let optimizeCookie;
385
+ let csrfToken;
378
386
  let optimizeBearerToken;
379
387
  let conditionalEventsCollectionId;
380
388
  let conditionalEventsReportId;
@@ -383,15 +391,17 @@ _8_10_1.test.describe('API optimize SaaS Tests - Conditional Events', () => {
383
391
  const CONDITIONAL_EVENTS_PROCESS_KEY = 'conditional-events-auto-process';
384
392
  _8_10_1.test.beforeAll(async ({ browser, request }) => {
385
393
  const page = await browser.newPage();
386
- optimizeCookie = await (0, apiHelpers_1.getOptimizeCoockie)(page);
394
+ ({ optimizeCookie, csrfToken } = await (0, apiHelpers_1.loginToOptimizeSession)(page));
387
395
  await page.close();
388
396
  optimizeBearerToken = await (0, apiHelpers_1.authSaasAPI)(process.env.OPTIMIZE_API_TOKEN_AUDIENCE);
389
397
  conditionalEventsCollectionId = await (0, apiHelpers_1.createCollection)(request, {
390
398
  name: await (0, randomName_1.randomNameAgregator)('Conditional Events Collection'),
391
399
  optimizeCookie,
400
+ csrfToken,
392
401
  });
393
402
  await (0, apiHelpers_1.updateCollectionScope)(request, {
394
403
  optimizeCookie,
404
+ csrfToken,
395
405
  collectionId: conditionalEventsCollectionId,
396
406
  data: [
397
407
  {
@@ -404,6 +414,7 @@ _8_10_1.test.describe('API optimize SaaS Tests - Conditional Events', () => {
404
414
  conditionalEventsReportName = await (0, randomName_1.randomNameAgregator)('Conditional Events Report');
405
415
  conditionalEventsReportId = await (0, apiHelpers_1.createSingleProcessReport)(request, {
406
416
  optimizeCookie,
417
+ csrfToken,
407
418
  collectionId: conditionalEventsCollectionId,
408
419
  name: conditionalEventsReportName,
409
420
  definitions: [
@@ -428,6 +439,7 @@ _8_10_1.test.describe('API optimize SaaS Tests - Conditional Events', () => {
428
439
  const response = await request.post(`${baseUrl}/api/report/process/single`, {
429
440
  headers: {
430
441
  Cookie: optimizeCookie,
442
+ 'X-CSRF-TOKEN': csrfToken,
431
443
  'Content-Type': 'application/json',
432
444
  },
433
445
  data: {
@@ -507,6 +519,7 @@ _8_10_1.test.describe('API optimize SaaS Tests - Conditional Events', () => {
507
519
  (0, _8_10_1.test)('CE-OPT-13: Deleting the Conditional Events report via API succeeds with HTTP 200', async ({ request, }) => {
508
520
  const reportToDeleteId = await (0, apiHelpers_1.createSingleProcessReport)(request, {
509
521
  optimizeCookie,
522
+ csrfToken,
510
523
  collectionId: conditionalEventsCollectionId,
511
524
  name: await (0, randomName_1.randomNameAgregator)('CE Report To Delete'),
512
525
  definitions: [
@@ -527,6 +540,7 @@ _8_10_1.test.describe('API optimize SaaS Tests - Conditional Events', () => {
527
540
  (0, _8_10_1.test)('CE-OPT-14: A deleted Conditional Events report is no longer retrievable via the API', async ({ request, }) => {
528
541
  const reportToDeleteId = await (0, apiHelpers_1.createSingleProcessReport)(request, {
529
542
  optimizeCookie,
543
+ csrfToken,
530
544
  collectionId: conditionalEventsCollectionId,
531
545
  name: await (0, randomName_1.randomNameAgregator)('CE Report Delete Verify'),
532
546
  definitions: [
@@ -566,13 +580,13 @@ _8_10_1.test.describe('API optimize SaaS Tests - Health Readiness', () => {
566
580
  (0, test_1.expect)(response.status()).toBe(200);
567
581
  });
568
582
  });
569
- (0, _8_10_1.test)('Health readiness rejects requests that include an Authorization header', async ({ request, }) => {
570
- await _8_10_1.test.step('GET /api/readyz with Authorization header should be rejected', async () => {
583
+ (0, _8_10_1.test)('Health readiness returns 200 when request includes an Authorization header', async ({ request, }) => {
584
+ await _8_10_1.test.step('GET /api/readyz with Authorization header still returns 200', async () => {
571
585
  const token = await (0, apiHelpers_1.authSaasAPI)(process.env.OPTIMIZE_API_TOKEN_AUDIENCE);
572
586
  const response = await request.get(`${baseUrl}/api/readyz`, {
573
587
  headers: { Authorization: token },
574
588
  });
575
- (0, test_1.expect)([400, 401, 403]).toContain(response.status());
589
+ (0, test_1.expect)(response.status()).toBe(200);
576
590
  });
577
591
  });
578
592
  });
@@ -583,6 +597,15 @@ _8_10_1.test.describe('API optimize SaaS Tests - Sharing', () => {
583
597
  optimizeBearerToken = await (0, apiHelpers_1.authSaasAPI)(process.env.OPTIMIZE_API_TOKEN_AUDIENCE);
584
598
  baseUrl = process.env.CAMUNDA_OPTIMIZE_BASE_URL;
585
599
  });
600
+ // Sharing is cluster-global, so leaving it off would break every
601
+ // concurrently running spec that shares a report or dashboard.
602
+ _8_10_1.test.afterAll(async ({ request }) => {
603
+ await (0, apiHelpers_1.setOptimizeSharing)(request, {
604
+ baseUrl,
605
+ bearerToken: optimizeBearerToken,
606
+ enabled: true,
607
+ });
608
+ });
586
609
  (0, _8_10_1.test)('Enable sharing with valid token returns 200 or 204', async ({ request, }) => {
587
610
  await _8_10_1.test.step('POST /api/public/share/enable with valid token (200 or 204)', async () => {
588
611
  const response = await request.post(`${baseUrl}/api/public/share/enable`, { headers: { Authorization: optimizeBearerToken } });
@@ -606,6 +629,13 @@ _8_10_1.test.describe('API optimize SaaS Tests - Sharing', () => {
606
629
  const response = await request.post(`${baseUrl}/api/public/share/disable`, { headers: { Authorization: optimizeBearerToken } });
607
630
  (0, test_1.expect)([200, 204]).toContain(response.status());
608
631
  });
632
+ await _8_10_1.test.step('Re-enable sharing to restore the cluster-wide setting', async () => {
633
+ await (0, apiHelpers_1.setOptimizeSharing)(request, {
634
+ baseUrl,
635
+ bearerToken: optimizeBearerToken,
636
+ enabled: true,
637
+ });
638
+ });
609
639
  });
610
640
  (0, _8_10_1.test)('Disable sharing without token returns 401', async ({ request }) => {
611
641
  await _8_10_1.test.step('POST /api/public/share/disable without token (401)', async () => {
@@ -637,17 +667,19 @@ _8_10_1.test.describe('API optimize SaaS Tests - Sharing', () => {
637
667
  _8_10_1.test.describe('API optimize SaaS Tests - Dashboard and Report GET edge cases', () => {
638
668
  let optimizeBearerToken;
639
669
  let optimizeCookie;
670
+ let csrfToken;
640
671
  let collectionId;
641
672
  let baseUrl;
642
673
  _8_10_1.test.beforeAll(async ({ browser, request }) => {
643
674
  const page = await browser.newPage();
644
- optimizeCookie = await (0, apiHelpers_1.getOptimizeCoockie)(page);
675
+ ({ optimizeCookie, csrfToken } = await (0, apiHelpers_1.loginToOptimizeSession)(page));
645
676
  await page.close();
646
677
  optimizeBearerToken = await (0, apiHelpers_1.authSaasAPI)(process.env.OPTIMIZE_API_TOKEN_AUDIENCE);
647
678
  baseUrl = process.env.CAMUNDA_OPTIMIZE_BASE_URL;
648
679
  collectionId = await (0, apiHelpers_1.createCollection)(request, {
649
680
  name: await (0, randomName_1.randomNameAgregator)('GET Edge Cases Collection'),
650
681
  optimizeCookie,
682
+ csrfToken,
651
683
  });
652
684
  });
653
685
  _8_10_1.test.afterAll(async ({ request }) => {
@@ -695,6 +727,7 @@ _8_10_1.test.describe('API optimize SaaS Tests - Dashboard and Report GET edge c
695
727
  _8_10_1.test.describe('API optimize SaaS Tests - Export edge cases', () => {
696
728
  let optimizeBearerToken;
697
729
  let optimizeCookie;
730
+ let csrfToken;
698
731
  let collectionId;
699
732
  let dashboardId1;
700
733
  let dashboardId2;
@@ -703,26 +736,30 @@ _8_10_1.test.describe('API optimize SaaS Tests - Export edge cases', () => {
703
736
  let baseUrl;
704
737
  _8_10_1.test.beforeAll(async ({ browser, request }) => {
705
738
  const page = await browser.newPage();
706
- optimizeCookie = await (0, apiHelpers_1.getOptimizeCoockie)(page);
739
+ ({ optimizeCookie, csrfToken } = await (0, apiHelpers_1.loginToOptimizeSession)(page));
707
740
  await page.close();
708
741
  optimizeBearerToken = await (0, apiHelpers_1.authSaasAPI)(process.env.OPTIMIZE_API_TOKEN_AUDIENCE);
709
742
  baseUrl = process.env.CAMUNDA_OPTIMIZE_BASE_URL;
710
743
  collectionId = await (0, apiHelpers_1.createCollection)(request, {
711
744
  name: await (0, randomName_1.randomNameAgregator)('Export Edge Cases Collection'),
712
745
  optimizeCookie,
746
+ csrfToken,
713
747
  });
714
748
  dashboardId1 = await (0, apiHelpers_1.createDashboard)(request, {
715
749
  name: await (0, randomName_1.randomNameAgregator)('Export Dashboard 1'),
716
750
  optimizeCookie,
751
+ csrfToken,
717
752
  collectionId,
718
753
  });
719
754
  dashboardId2 = await (0, apiHelpers_1.createDashboard)(request, {
720
755
  name: await (0, randomName_1.randomNameAgregator)('Export Dashboard 2'),
721
756
  optimizeCookie,
757
+ csrfToken,
722
758
  collectionId,
723
759
  });
724
760
  reportId1 = await (0, apiHelpers_1.createSingleProcessReport)(request, {
725
761
  optimizeCookie,
762
+ csrfToken,
726
763
  collectionId,
727
764
  name: await (0, randomName_1.randomNameAgregator)('Export Report 1'),
728
765
  definitions: [
@@ -737,6 +774,7 @@ _8_10_1.test.describe('API optimize SaaS Tests - Export edge cases', () => {
737
774
  });
738
775
  reportId2 = await (0, apiHelpers_1.createSingleProcessReport)(request, {
739
776
  optimizeCookie,
777
+ csrfToken,
740
778
  collectionId,
741
779
  name: await (0, randomName_1.randomNameAgregator)('Export Report 2'),
742
780
  definitions: [
@@ -7,11 +7,12 @@ const randomName_1 = require("../../utils/randomName");
7
7
  _8_10_1.test.describe.configure({ mode: 'parallel' });
8
8
  _8_10_1.test.describe('API Optimize Collections Lifecycle Tests @optimize-collections-api @8.10', () => {
9
9
  let optimizeCookie;
10
+ let csrfToken;
10
11
  let optimizeBearerToken;
11
12
  let baseUrl;
12
13
  _8_10_1.test.beforeAll(async ({ browser }) => {
13
14
  const page = await browser.newPage();
14
- optimizeCookie = await (0, apiHelpers_1.getOptimizeCoockie)(page);
15
+ ({ optimizeCookie, csrfToken } = await (0, apiHelpers_1.loginToOptimizeSession)(page));
15
16
  await page.close();
16
17
  optimizeBearerToken = await (0, apiHelpers_1.authSaasAPI)(process.env.OPTIMIZE_API_TOKEN_AUDIENCE);
17
18
  baseUrl = process.env.CAMUNDA_OPTIMIZE_BASE_URL;
@@ -21,6 +22,7 @@ _8_10_1.test.describe('API Optimize Collections Lifecycle Tests @optimize-collec
21
22
  const collectionId = await (0, apiHelpers_1.createCollection)(request, {
22
23
  name: collectionName,
23
24
  optimizeCookie,
25
+ csrfToken,
24
26
  });
25
27
  await _8_10_1.test.step('GET /api/collection/{id} returns the created collection (200)', async () => {
26
28
  const response = await request.get(`${baseUrl}/api/collection/${collectionId}`, { headers: { Cookie: optimizeCookie } });
@@ -38,12 +40,14 @@ _8_10_1.test.describe('API Optimize Collections Lifecycle Tests @optimize-collec
38
40
  const collectionId = await (0, apiHelpers_1.createCollection)(request, {
39
41
  name: originalName,
40
42
  optimizeCookie,
43
+ csrfToken,
41
44
  });
42
45
  const renamedName = await (0, randomName_1.randomNameAgregator)('Collection Renamed');
43
46
  await _8_10_1.test.step('PUT /api/collection/{id} updates the collection name', async () => {
44
47
  const response = await request.put(`${baseUrl}/api/collection/${collectionId}`, {
45
48
  headers: {
46
49
  Cookie: optimizeCookie,
50
+ 'X-CSRF-TOKEN': csrfToken,
47
51
  'Content-Type': 'application/json',
48
52
  },
49
53
  data: { name: renamedName },
@@ -65,11 +69,13 @@ _8_10_1.test.describe('API Optimize Collections Lifecycle Tests @optimize-collec
65
69
  const collectionId = await (0, apiHelpers_1.createCollection)(request, {
66
70
  name: collectionName,
67
71
  optimizeCookie,
72
+ csrfToken,
68
73
  });
69
74
  const definitionKey = 'customer_onboarding_en';
70
75
  await _8_10_1.test.step('PUT /api/collection/{id}/scope adds a process definition', async () => {
71
76
  await (0, apiHelpers_1.updateCollectionScope)(request, {
72
77
  optimizeCookie,
78
+ csrfToken,
73
79
  collectionId,
74
80
  data: [
75
81
  {
@@ -97,6 +103,7 @@ _8_10_1.test.describe('API Optimize Collections Lifecycle Tests @optimize-collec
97
103
  const collectionId = await (0, apiHelpers_1.createCollection)(request, {
98
104
  name: collectionName,
99
105
  optimizeCookie,
106
+ csrfToken,
100
107
  });
101
108
  let deleteSucceeded = false;
102
109
  await _8_10_1.test.step('DELETE /api/public/collection/{id} is accepted (200; SaaS may respond 500)', async () => {
@@ -126,6 +133,7 @@ _8_10_1.test.describe('API Optimize Collections Lifecycle Tests @optimize-collec
126
133
  const collectionId = await (0, apiHelpers_1.createCollection)(request, {
127
134
  name: collectionName,
128
135
  optimizeCookie,
136
+ csrfToken,
129
137
  });
130
138
  await _8_10_1.test.step('GET /api/collection/{id} without cookie (401)', async () => {
131
139
  const response = await request.get(`${baseUrl}/api/collection/${collectionId}`);
@@ -140,6 +148,7 @@ _8_10_1.test.describe('API Optimize Collections Lifecycle Tests @optimize-collec
140
148
  const collectionId = await (0, apiHelpers_1.createCollection)(request, {
141
149
  name: collectionName,
142
150
  optimizeCookie,
151
+ csrfToken,
143
152
  });
144
153
  await _8_10_1.test.step('DELETE /api/public/collection/{id} without token (401)', async () => {
145
154
  const response = await request.delete(`${baseUrl}/api/public/collection/${collectionId}`);
@@ -154,6 +163,7 @@ _8_10_1.test.describe('API Optimize Collections Lifecycle Tests @optimize-collec
154
163
  const collectionId = await (0, apiHelpers_1.createCollection)(request, {
155
164
  name: collectionName,
156
165
  optimizeCookie,
166
+ csrfToken,
157
167
  });
158
168
  await _8_10_1.test.step('DELETE /api/public/collection/{id} with invalid token (401)', async () => {
159
169
  const response = await request.delete(`${baseUrl}/api/public/collection/${collectionId}`, { headers: { Authorization: 'Bearer invalid_token' } });
@@ -25,6 +25,7 @@ let originalFilters = null;
25
25
  let dataFiltersAvailable = false;
26
26
  let zeebeToken;
27
27
  let optimizeCookie;
28
+ let csrfToken;
28
29
  let optimizeExportToken;
29
30
  let optimizeBaseUrl;
30
31
  const createdCollectionIds = [];
@@ -81,10 +82,12 @@ const getOrCreateReport = async (request, definitionKey) => {
81
82
  const collectionId = await (0, apiHelpers_1.createCollection)(request, {
82
83
  name: await (0, randomName_1.randomNameAgregator)('ExporterFilters E2E'),
83
84
  optimizeCookie,
85
+ csrfToken,
84
86
  });
85
87
  createdCollectionIds.push(collectionId);
86
88
  await (0, apiHelpers_1.updateCollectionScope)(request, {
87
89
  optimizeCookie,
90
+ csrfToken,
88
91
  collectionId,
89
92
  data: [
90
93
  {
@@ -97,6 +100,7 @@ const getOrCreateReport = async (request, definitionKey) => {
97
100
  const reportResponse = await request.post(`${optimizeBaseUrl}/api/report/process/single`, {
98
101
  headers: {
99
102
  Cookie: optimizeCookie,
103
+ 'X-CSRF-TOKEN': csrfToken,
100
104
  'Content-Type': 'application/json',
101
105
  },
102
106
  data: {
@@ -184,7 +188,7 @@ _8_10_1.test.beforeAll(async ({ browser }) => {
184
188
  optimizeExportToken = await (0, apiHelpers_1.authSaasAPI)(process.env.OPTIMIZE_API_TOKEN_AUDIENCE);
185
189
  optimizeBaseUrl = process.env.CAMUNDA_OPTIMIZE_BASE_URL;
186
190
  const page = await browser.newPage();
187
- optimizeCookie = await (0, apiHelpers_1.getOptimizeCoockie)(page);
191
+ ({ optimizeCookie, csrfToken } = await (0, apiHelpers_1.loginToOptimizeSession)(page));
188
192
  await page.close();
189
193
  }
190
194
  catch (error) {
@@ -36,6 +36,7 @@ const createNumberReport = async (request, options) => {
36
36
  const response = await request.post(`${options.baseUrl}/api/report/process/single`, {
37
37
  headers: {
38
38
  Cookie: options.optimizeCookie,
39
+ 'X-CSRF-TOKEN': options.csrfToken,
39
40
  'Content-Type': 'application/json',
40
41
  },
41
42
  data: {
@@ -67,6 +68,7 @@ const createRawDataReport = async (request, options) => {
67
68
  const response = await request.post(`${options.baseUrl}/api/report/process/single`, {
68
69
  headers: {
69
70
  Cookie: options.optimizeCookie,
71
+ 'X-CSRF-TOKEN': options.csrfToken,
70
72
  'Content-Type': 'application/json',
71
73
  },
72
74
  data: {
@@ -96,6 +98,7 @@ const createRawDataReport = async (request, options) => {
96
98
  };
97
99
  _8_10_1.test.describe('API Optimize Sharing, Alerts & Export Tests @optimize-sharing-alerts-export-api @8.10', () => {
98
100
  let optimizeCookie;
101
+ let csrfToken;
99
102
  let optimizeBearerToken;
100
103
  let baseUrl;
101
104
  let collectionId;
@@ -104,16 +107,18 @@ _8_10_1.test.describe('API Optimize Sharing, Alerts & Export Tests @optimize-sha
104
107
  let dashboardId;
105
108
  _8_10_1.test.beforeAll(async ({ browser, request }) => {
106
109
  const page = await browser.newPage();
107
- optimizeCookie = await (0, apiHelpers_1.getOptimizeCoockie)(page);
110
+ ({ optimizeCookie, csrfToken } = await (0, apiHelpers_1.loginToOptimizeSession)(page));
108
111
  await page.close();
109
112
  optimizeBearerToken = await (0, apiHelpers_1.authSaasAPI)(process.env.OPTIMIZE_API_TOKEN_AUDIENCE);
110
113
  baseUrl = process.env.CAMUNDA_OPTIMIZE_BASE_URL;
111
114
  collectionId = await (0, apiHelpers_1.createCollection)(request, {
112
115
  name: await (0, randomName_1.randomNameAgregator)('Sharing Alerts Collection'),
113
116
  optimizeCookie,
117
+ csrfToken,
114
118
  });
115
119
  await (0, apiHelpers_1.updateCollectionScope)(request, {
116
120
  optimizeCookie,
121
+ csrfToken,
117
122
  collectionId,
118
123
  data: [
119
124
  {
@@ -126,18 +131,21 @@ _8_10_1.test.describe('API Optimize Sharing, Alerts & Export Tests @optimize-sha
126
131
  numberReportId = await createNumberReport(request, {
127
132
  baseUrl,
128
133
  optimizeCookie,
134
+ csrfToken,
129
135
  collectionId,
130
136
  name: await (0, randomName_1.randomNameAgregator)('Number Report'),
131
137
  });
132
138
  rawDataReportId = await createRawDataReport(request, {
133
139
  baseUrl,
134
140
  optimizeCookie,
141
+ csrfToken,
135
142
  collectionId,
136
143
  name: await (0, randomName_1.randomNameAgregator)('Raw Data Report'),
137
144
  });
138
145
  dashboardId = await (0, apiHelpers_1.createDashboard)(request, {
139
146
  name: await (0, randomName_1.randomNameAgregator)('Sharing Dashboard'),
140
147
  optimizeCookie,
148
+ csrfToken,
141
149
  collectionId,
142
150
  });
143
151
  });
@@ -188,10 +196,18 @@ _8_10_1.test.describe('API Optimize Sharing, Alerts & Export Tests @optimize-sha
188
196
  });
189
197
  (0, _8_10_1.test)('Enable, evaluate and delete a report share', async ({ request }) => {
190
198
  let reportShareId = '';
199
+ await _8_10_1.test.step('Ensure sharing is enabled cluster-wide', async () => {
200
+ await (0, apiHelpers_1.setOptimizeSharing)(request, {
201
+ baseUrl,
202
+ bearerToken: optimizeBearerToken,
203
+ enabled: true,
204
+ });
205
+ });
191
206
  await _8_10_1.test.step('POST /api/share/report enables sharing (200)', async () => {
192
207
  const response = await request.post(`${baseUrl}/api/share/report`, {
193
208
  headers: {
194
209
  Cookie: optimizeCookie,
210
+ 'X-CSRF-TOKEN': csrfToken,
195
211
  'Content-Type': 'application/json',
196
212
  },
197
213
  data: { reportId: numberReportId },
@@ -210,7 +226,7 @@ _8_10_1.test.describe('API Optimize Sharing, Alerts & Export Tests @optimize-sha
210
226
  (0, test_1.expect)([200, 201]).toContain(response.status());
211
227
  });
212
228
  await _8_10_1.test.step('DELETE /api/share/report/{shareId} removes the share', async () => {
213
- const response = await request.delete(`${baseUrl}/api/share/report/${reportShareId}`, { headers: { Cookie: optimizeCookie } });
229
+ const response = await request.delete(`${baseUrl}/api/share/report/${reportShareId}`, { headers: { Cookie: optimizeCookie, 'X-CSRF-TOKEN': csrfToken } });
214
230
  (0, test_1.expect)([200, 204]).toContain(response.status());
215
231
  });
216
232
  });
@@ -225,10 +241,18 @@ _8_10_1.test.describe('API Optimize Sharing, Alerts & Export Tests @optimize-sha
225
241
  });
226
242
  (0, _8_10_1.test)('Enable and delete a dashboard share', async ({ request }) => {
227
243
  let dashboardShareId = '';
244
+ await _8_10_1.test.step('Ensure sharing is enabled cluster-wide', async () => {
245
+ await (0, apiHelpers_1.setOptimizeSharing)(request, {
246
+ baseUrl,
247
+ bearerToken: optimizeBearerToken,
248
+ enabled: true,
249
+ });
250
+ });
228
251
  await _8_10_1.test.step('POST /api/share/dashboard enables sharing (200)', async () => {
229
252
  const response = await request.post(`${baseUrl}/api/share/dashboard`, {
230
253
  headers: {
231
254
  Cookie: optimizeCookie,
255
+ 'X-CSRF-TOKEN': csrfToken,
232
256
  'Content-Type': 'application/json',
233
257
  },
234
258
  data: { dashboardId },
@@ -240,7 +264,7 @@ _8_10_1.test.describe('API Optimize Sharing, Alerts & Export Tests @optimize-sha
240
264
  (0, test_1.expect)(dashboardShareId.length).toBeGreaterThan(0);
241
265
  });
242
266
  await _8_10_1.test.step('DELETE /api/share/dashboard/{shareId} removes the share', async () => {
243
- const response = await request.delete(`${baseUrl}/api/share/dashboard/${dashboardShareId}`, { headers: { Cookie: optimizeCookie } });
267
+ const response = await request.delete(`${baseUrl}/api/share/dashboard/${dashboardShareId}`, { headers: { Cookie: optimizeCookie, 'X-CSRF-TOKEN': csrfToken } });
244
268
  (0, test_1.expect)([200, 204]).toContain(response.status());
245
269
  });
246
270
  });
@@ -251,6 +275,7 @@ _8_10_1.test.describe('API Optimize Sharing, Alerts & Export Tests @optimize-sha
251
275
  const response = await request.post(`${baseUrl}/api/alert`, {
252
276
  headers: {
253
277
  Cookie: optimizeCookie,
278
+ 'X-CSRF-TOKEN': csrfToken,
254
279
  'Content-Type': 'application/json',
255
280
  },
256
281
  data: {
@@ -281,7 +306,7 @@ _8_10_1.test.describe('API Optimize Sharing, Alerts & Export Tests @optimize-sha
281
306
  });
282
307
  await _8_10_1.test.step('DELETE /api/alert/{id} removes the alert', async () => {
283
308
  const response = await request.delete(`${baseUrl}/api/alert/${alertId}`, {
284
- headers: { Cookie: optimizeCookie },
309
+ headers: { Cookie: optimizeCookie, 'X-CSRF-TOKEN': csrfToken },
285
310
  });
286
311
  (0, test_1.expect)([200, 204]).toContain(response.status());
287
312
  });
@@ -14,6 +14,7 @@ _8_10_1.test.describe.configure({ mode: 'parallel' });
14
14
  _8_10_1.test.describe('Optimize Sharing & Export UI Tests @optimize-sharing-alerts-export-ui @8.10', () => {
15
15
  let baseUrl;
16
16
  let optimizeCookie;
17
+ let csrfToken;
17
18
  let optimizeBearerToken;
18
19
  let collectionId;
19
20
  let reportId;
@@ -22,15 +23,17 @@ _8_10_1.test.describe('Optimize Sharing & Export UI Tests @optimize-sharing-aler
22
23
  _8_10_1.test.beforeAll(async ({ browser, request }) => {
23
24
  baseUrl = process.env.CAMUNDA_OPTIMIZE_BASE_URL;
24
25
  const seedPage = await browser.newPage();
25
- optimizeCookie = await (0, apiHelpers_1.getOptimizeCoockie)(seedPage);
26
+ ({ optimizeCookie, csrfToken } = await (0, apiHelpers_1.loginToOptimizeSession)(seedPage));
26
27
  await seedPage.close();
27
28
  optimizeBearerToken = await (0, apiHelpers_1.authSaasAPI)(process.env.OPTIMIZE_API_TOKEN_AUDIENCE);
28
29
  collectionId = await (0, apiHelpers_1.createCollection)(request, {
29
30
  name: await (0, randomName_1.randomNameAgregator)('Sharing UI Collection'),
30
31
  optimizeCookie,
32
+ csrfToken,
31
33
  });
32
34
  await (0, apiHelpers_1.updateCollectionScope)(request, {
33
35
  optimizeCookie,
36
+ csrfToken,
34
37
  collectionId,
35
38
  data: [
36
39
  {
@@ -47,6 +50,7 @@ _8_10_1.test.describe('Optimize Sharing & Export UI Tests @optimize-sharing-aler
47
50
  const reportResponse = await (0, apiHelpers_1.retryOn500)(() => request.post(`${baseUrl}/api/report/process/single`, {
48
51
  headers: {
49
52
  Cookie: optimizeCookie,
53
+ 'X-CSRF-TOKEN': csrfToken,
50
54
  'Content-Type': 'application/json',
51
55
  },
52
56
  data: {
@@ -75,6 +79,7 @@ _8_10_1.test.describe('Optimize Sharing & Export UI Tests @optimize-sharing-aler
75
79
  dashboardId = await (0, apiHelpers_1.createDashboard)(request, {
76
80
  name: await (0, randomName_1.randomNameAgregator)('Sharing UI Dashboard'),
77
81
  optimizeCookie,
82
+ csrfToken,
78
83
  collectionId,
79
84
  });
80
85
  // Optimize only renders the CSV download action on raw-data reports
@@ -85,6 +90,7 @@ _8_10_1.test.describe('Optimize Sharing & Export UI Tests @optimize-sharing-aler
85
90
  const csvReportResponse = await (0, apiHelpers_1.retryOn500)(() => request.post(`${baseUrl}/api/report/process/single`, {
86
91
  headers: {
87
92
  Cookie: optimizeCookie,
93
+ 'X-CSRF-TOKEN': csrfToken,
88
94
  'Content-Type': 'application/json',
89
95
  },
90
96
  data: {
@@ -111,7 +117,14 @@ _8_10_1.test.describe('Optimize Sharing & Export UI Tests @optimize-sharing-aler
111
117
  (0, test_1.expect)(csvReportResponse.status()).toBe(200);
112
118
  csvReportId = (await csvReportResponse.json()).id;
113
119
  });
114
- _8_10_1.test.beforeEach(async ({ page, loginPage }, testInfo) => {
120
+ // The share button is rendered from ui-configuration, which the app reads once
121
+ // per page load, so the setting has to be right before we navigate.
122
+ _8_10_1.test.beforeEach(async ({ page, loginPage, request }, testInfo) => {
123
+ await (0, apiHelpers_1.setOptimizeSharing)(request, {
124
+ baseUrl,
125
+ bearerToken: optimizeBearerToken,
126
+ enabled: true,
127
+ });
115
128
  await (0, UtilitiesPage_1.loginWithRetry)(page, loginPage, testUser, (testInfo.workerIndex + 1) * 1000);
116
129
  });
117
130
  _8_10_1.test.afterEach(async ({ page }, testInfo) => {
@@ -19,7 +19,8 @@ if (process.env.IS_MT === 'true') {
19
19
  await (0, _setup_1.captureScreenshot)(page, testInfo);
20
20
  await (0, _setup_1.captureFailureVideo)(page, testInfo);
21
21
  });
22
- (0, SM_8_10_1.test)('Main User Can Create & Access Tenant & User Without Access Cannot - User Task Flow', async ({ page, modelerHomePage, modelerCreatePage, taskPanelPage, operateProcessInstancePage, operateProcessesPage, taskDetailsPage, operateHomePage, navigationPage, ocTenantPage, optimizeHomePage, optimizeDashboardPage, browser, ocIdentityHomePage, managementIdentityTenantPage, managementIdentityPage, keycloakAdminPage, keycloakLoginPage, ocIdentityMappingRulesPage, ocIdentityRolesPage, }) => {
22
+ // Skipped due to bug #60836: https://github.com/camunda/camunda/issues/60836
23
+ SM_8_10_1.test.skip('Main User Can Create & Access Tenant & User Without Access Cannot - User Task Flow', async ({ page, modelerHomePage, modelerCreatePage, taskPanelPage, operateProcessInstancePage, operateProcessesPage, taskDetailsPage, operateHomePage, navigationPage, ocTenantPage, optimizeHomePage, optimizeDashboardPage, browser, ocIdentityHomePage, managementIdentityTenantPage, managementIdentityPage, keycloakAdminPage, keycloakLoginPage, ocIdentityMappingRulesPage, ocIdentityRolesPage, }) => {
23
24
  SM_8_10_1.test.slow();
24
25
  const randomString = await (0, _setup_1.generateRandomStringAsync)(3);
25
26
  const processName = 'Main_User_Task_Flow_' + randomString;
@@ -127,7 +128,8 @@ if (process.env.IS_MT === 'true') {
127
128
  }
128
129
  });
129
130
  });
130
- (0, SM_8_10_1.test)('Main User Can Create & Access Tenant - Connectors Flow @tasklistV2', async ({ page, modelerHomePage, modelerCreatePage, operateProcessInstancePage, operateProcessesPage, operateHomePage, navigationPage, managementIdentityTenantPage, connectorSettingsPage, connectorMarketplacePage, optimizeHomePage, optimizeDashboardPage, ocIdentityHomePage, ocTenantPage, }) => {
131
+ // Skipped due to bug #60836: https://github.com/camunda/camunda/issues/60836
132
+ SM_8_10_1.test.skip('Main User Can Create & Access Tenant - Connectors Flow @tasklistV2', async ({ page, modelerHomePage, modelerCreatePage, operateProcessInstancePage, operateProcessesPage, operateHomePage, navigationPage, managementIdentityTenantPage, connectorSettingsPage, connectorMarketplacePage, optimizeHomePage, optimizeDashboardPage, ocIdentityHomePage, ocTenantPage, }) => {
131
133
  SM_8_10_1.test.slow();
132
134
  const randomString = await (0, _setup_1.generateRandomStringAsync)(3);
133
135
  const processName = 'Main_User_Connectors_Flow_' + randomString;
@@ -259,7 +261,8 @@ if (process.env.IS_MT === 'true') {
259
261
  });
260
262
  }
261
263
  });
262
- (0, SM_8_10_1.test)('Second User Can Access Tenant Created By Main User - Connectors Flow @tasklistV2', async ({ page, modelerHomePage, modelerCreatePage, operateProcessInstancePage, operateProcessesPage, operateHomePage, navigationPage, managementIdentityTenantPage, connectorSettingsPage, connectorMarketplacePage, optimizeHomePage, optimizeDashboardPage, ocIdentityHomePage, ocTenantPage, managementIdentityPage, keycloakAdminPage, keycloakLoginPage, ocIdentityRolesPage, ocIdentityMappingRulesPage, browser, }) => {
264
+ // Skipped due to bug #60836: https://github.com/camunda/camunda/issues/60836
265
+ SM_8_10_1.test.skip('Second User Can Access Tenant Created By Main User - Connectors Flow @tasklistV2', async ({ page, modelerHomePage, modelerCreatePage, operateProcessInstancePage, operateProcessesPage, operateHomePage, navigationPage, managementIdentityTenantPage, connectorSettingsPage, connectorMarketplacePage, optimizeHomePage, optimizeDashboardPage, ocIdentityHomePage, ocTenantPage, managementIdentityPage, keycloakAdminPage, keycloakLoginPage, ocIdentityRolesPage, ocIdentityMappingRulesPage, browser, }) => {
263
266
  SM_8_10_1.test.slow();
264
267
  const randomString = await (0, _setup_1.generateRandomStringAsync)(3);
265
268
  const processName = 'Second_User_Connectors_Flow_' + randomString;
@@ -330,7 +333,8 @@ if (process.env.IS_MT === 'true') {
330
333
  });
331
334
  }
332
335
  });
333
- (0, SM_8_10_1.test)('Default Tenant Is Available And Used When Tenant Not Set In Deployment', async ({ page, modelerHomePage, modelerCreatePage, taskPanelPage, operateProcessInstancePage, operateProcessesPage, taskDetailsPage, operateHomePage, navigationPage, managementIdentityTenantPage, optimizeHomePage, optimizeDashboardPage, ocIdentityHomePage, ocTenantPage, }) => {
336
+ // Skipped due to bug #60836: https://github.com/camunda/camunda/issues/60836
337
+ SM_8_10_1.test.skip('Default Tenant Is Available And Used When Tenant Not Set In Deployment', async ({ page, modelerHomePage, modelerCreatePage, taskPanelPage, operateProcessInstancePage, operateProcessesPage, taskDetailsPage, operateHomePage, navigationPage, managementIdentityTenantPage, optimizeHomePage, optimizeDashboardPage, ocIdentityHomePage, ocTenantPage, }) => {
334
338
  SM_8_10_1.test.slow();
335
339
  const randomString = await (0, _setup_1.generateRandomStringAsync)(3);
336
340
  const processName = 'Default_Tenant_Flow_' + randomString;
@@ -466,7 +470,8 @@ if (process.env.IS_MT === 'true') {
466
470
  await operateProcessInstancePage.tenantNameAssertion(tenantName2);
467
471
  });
468
472
  });
469
- (0, SM_8_10_1.test)('Revoking Tenant Access User Flow', async ({ page, modelerHomePage, modelerCreatePage, taskPanelPage, operateProcessInstancePage, operateProcessesPage, taskDetailsPage, operateHomePage, navigationPage, managementIdentityTenantPage, optimizeHomePage, optimizeDashboardPage, ocIdentityHomePage, ocTenantPage, }) => {
473
+ // Skipped due to bug #60836: https://github.com/camunda/camunda/issues/60836
474
+ SM_8_10_1.test.skip('Revoking Tenant Access User Flow', async ({ page, modelerHomePage, modelerCreatePage, taskPanelPage, operateProcessInstancePage, operateProcessesPage, taskDetailsPage, operateHomePage, navigationPage, managementIdentityTenantPage, optimizeHomePage, optimizeDashboardPage, ocIdentityHomePage, ocTenantPage, }) => {
470
475
  SM_8_10_1.test.slow();
471
476
  const randomString = await (0, _setup_1.generateRandomStringAsync)(3);
472
477
  const processName1 = 'Revoke_Tenant_Flow_1' + randomString;
@@ -35,26 +35,39 @@ interface ReportDefinition {
35
35
  }
36
36
  interface CreateSingleProcessReportOptions {
37
37
  optimizeCookie: string;
38
+ csrfToken?: string;
38
39
  collectionId: string;
39
40
  name: string;
40
41
  description?: string | null;
41
42
  definitions?: ReportDefinition[];
42
43
  configuration?: Record<string, unknown>;
43
44
  }
45
+ export declare function loginToOptimizeSession(page: Page): Promise<{
46
+ optimizeCookie: string;
47
+ csrfToken: string;
48
+ }>;
44
49
  export declare function getOptimizeCoockie(page: Page): Promise<string>;
45
50
  export declare function getOptimizeCookieSm(page: Page): Promise<string>;
46
51
  export declare function createCollection(request: APIRequestContext, options: {
47
52
  name: string;
48
53
  optimizeCookie: string;
54
+ csrfToken?: string;
49
55
  }): Promise<string>;
50
56
  export declare function createDashboard(request: APIRequestContext, options: {
51
57
  name: string;
52
58
  optimizeCookie: string;
59
+ csrfToken?: string;
53
60
  collectionId?: string;
54
61
  }): Promise<any>;
62
+ export declare function setOptimizeSharing(request: APIRequestContext, options: {
63
+ baseUrl: string;
64
+ bearerToken: string;
65
+ enabled: boolean;
66
+ }): Promise<void>;
55
67
  export declare function createSingleProcessReport(request: APIRequestContext, options: CreateSingleProcessReportOptions): Promise<any>;
56
68
  export declare function updateCollectionScope(request: APIRequestContext, options: {
57
69
  optimizeCookie: string;
70
+ csrfToken?: string;
58
71
  collectionId: string;
59
72
  data: Array<{
60
73
  definitionKey: string;
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.updateClusterDataFilters = exports.getClusterDataFilters = exports.getTestClusterUuid = exports.authConsoleManagementAPI = exports.updateCollectionScope = exports.createSingleProcessReport = exports.createDashboard = exports.createCollection = exports.getOptimizeCookieSm = exports.getOptimizeCoockie = exports.createStringClusterVariable = exports.createJsonClusterVariable = exports.waitForProcessInstanceVisible = exports.createProcessInstance = exports.deployProcess = exports.waitForOrchestrationClusterReady = exports.waitForInboundWebhookRegistered = exports.waitForConnectorsReady = exports.validateMcpServerHealth = exports.authC8runAPI = exports.authSmAPI = exports.authSaasAPI = exports.authAPI = exports.buildZeebeApiUrl = exports.retryOn500 = exports.sendRequestAndAssertResponse = exports.assertUnauthorizedResponseBody = exports.expectUnauthorizedErrorBody = exports.assertResponseStatus = exports.getApiRequestContext = void 0;
6
+ exports.updateClusterDataFilters = exports.getClusterDataFilters = exports.getTestClusterUuid = exports.authConsoleManagementAPI = exports.updateCollectionScope = exports.createSingleProcessReport = exports.setOptimizeSharing = exports.createDashboard = exports.createCollection = exports.getOptimizeCookieSm = exports.getOptimizeCoockie = exports.loginToOptimizeSession = exports.createStringClusterVariable = exports.createJsonClusterVariable = exports.waitForProcessInstanceVisible = exports.createProcessInstance = exports.deployProcess = exports.waitForOrchestrationClusterReady = exports.waitForInboundWebhookRegistered = exports.waitForConnectorsReady = exports.validateMcpServerHealth = exports.authC8runAPI = exports.authSmAPI = exports.authSaasAPI = exports.authAPI = exports.buildZeebeApiUrl = exports.retryOn500 = exports.sendRequestAndAssertResponse = exports.assertUnauthorizedResponseBody = exports.expectUnauthorizedErrorBody = exports.assertResponseStatus = exports.getApiRequestContext = void 0;
7
7
  const test_1 = require("@playwright/test");
8
8
  const sleep_1 = require("./sleep");
9
9
  const randomSleep_1 = require("./randomSleep");
@@ -666,7 +666,50 @@ async function createStringClusterVariable(authToken, environment = 'saas', cust
666
666
  await createClusterVariableInternal(authToken ?? '', environment, variableName, variableValue, 'String', clusterType);
667
667
  }
668
668
  exports.createStringClusterVariable = createStringClusterVariable;
669
- async function getOptimizeCoockie(page) {
669
+ // CSL (8.10+) protects writes with a CSRF token in addition to the session
670
+ // cookie (see optimize/client/src/modules/csrf.ts); the legacy security stack
671
+ // used by getOptimizeCoockie/getOptimizeCookieSm (8.8/8.9/SM) never issues
672
+ // one, so it stays undefined there and this header is simply omitted.
673
+ function optimizeCookieHeaders(options) {
674
+ const headers = { Cookie: options.optimizeCookie };
675
+ if (options.csrfToken)
676
+ headers['X-CSRF-TOKEN'] = options.csrfToken;
677
+ return headers;
678
+ }
679
+ const OPTIMIZE_SESSION_UNAUTHENTICATED = 'OPTIMIZE_SESSION_UNAUTHENTICATED';
680
+ // Verifying the session is part of logging in, not a wait that can be retried
681
+ // afterwards: on SaaS the OIDC callback (GET /{clusterId}/sso-callback) answers
682
+ // 401 for a share of concurrent logins, and because that 401 page is still
683
+ // served from the Optimize base URL, loginToApp's
684
+ // waitForURL(url.startsWith(appName), {waitUntil: 'commit'}) resolves on it and
685
+ // reports success. The session then stays unauthenticated for good -- no amount
686
+ // of polling recovers it, only a fresh login does. So probe an endpoint that is
687
+ // on the CSL API chain and let performOptimizeInteractiveLogin's retry loop
688
+ // start over when it comes back unauthenticated.
689
+ //
690
+ // The probe doubles as the CSRF-token source: CSL's CSRF setup is a
691
+ // double-submit cookie (Spring's CookieCsrfTokenRepository), so the first
692
+ // authenticated GET is what issues the X-CSRF-TOKEN cookie, echoing the token
693
+ // as an x-csrf-token response header. Returns that token when authenticated,
694
+ // null when not.
695
+ async function probeOptimizeSession(page) {
696
+ const optimizeUrl = process.env.CAMUNDA_OPTIMIZE_BASE_URL;
697
+ const response = await page
698
+ .context()
699
+ .request.get(`${optimizeUrl}/api/identity/current/user`);
700
+ // Require both a successful response AND a token, mirroring the product's
701
+ // own rule for when to trust the header (optimize/client/src/modules/csrf.ts
702
+ // storeCsrfToken): a non-2xx (a transient 5xx, or a 401/403 from a stale
703
+ // session) or a 2xx with no token yet must be treated as unauthenticated,
704
+ // not as "authenticated with an empty token" -- the latter would exit the
705
+ // login retry loop with a cookie that then fails every write.
706
+ const token = response.headers()['x-csrf-token'];
707
+ if (!response.ok() || !token) {
708
+ return null;
709
+ }
710
+ return token;
711
+ }
712
+ async function performOptimizeInteractiveLogin(page, callerLabel, verifySession) {
670
713
  const optimizeUrl = process.env.CAMUNDA_OPTIMIZE_BASE_URL;
671
714
  const maxLoginAttempts = 4;
672
715
  // This hook runs once per worker that picks up any test from an Optimize
@@ -675,7 +718,13 @@ async function getOptimizeCoockie(page) {
675
718
  // spread that burst -- it is enough on its own to trip Auth0's tenant-wide
676
719
  // rate limit, which then fails every worker at once. Retries back off via
677
720
  // randomSleep below, so this only needs to run once.
678
- await (0, randomSleep_1.randomSleep)(0, 15000);
721
+ //
722
+ // The window has to be wider than a rejection burst, not just wider than the
723
+ // instant the workers start: 8.10 has eight Optimize describes each doing one
724
+ // interactive login, and the sso-callback rejections below come in bursts
725
+ // lasting tens of seconds, so a 15s spread put every login inside the same
726
+ // burst and the losers then spent their whole attempt budget in it.
727
+ await (0, randomSleep_1.randomSleep)(0, 60000);
679
728
  // Auth0 can fail or rate-limit concurrent logins. Retry the whole flow if
680
729
  // loginToApp throws (e.g. waitForURL times out after a bad Auth0 redirect).
681
730
  for (let loginAttempt = 0; loginAttempt < maxLoginAttempts; loginAttempt++) {
@@ -683,12 +732,16 @@ async function getOptimizeCoockie(page) {
683
732
  await page.goto('about:blank');
684
733
  await page.context().clearCookies();
685
734
  await (0, utils_1.loginToApp)(optimizeUrl, page);
686
- break;
735
+ if (verifySession && !(await verifySession(page))) {
736
+ throw new Error(OPTIMIZE_SESSION_UNAUTHENTICATED);
737
+ }
738
+ return;
687
739
  }
688
740
  catch (error) {
689
741
  if (loginAttempt < maxLoginAttempts - 1) {
690
742
  const rateLimited = String(error).includes(utils_1.AUTH0_RATE_LIMIT);
691
- console.warn(`getOptimizeCoockie: login attempt ${loginAttempt + 1} failed, retrying after backoff...`, error);
743
+ const unauthenticated = String(error).includes(OPTIMIZE_SESSION_UNAUTHENTICATED);
744
+ console.warn(`${callerLabel}: login attempt ${loginAttempt + 1} failed, retrying after backoff...`, error);
692
745
  await page.goto('about:blank');
693
746
  if (rateLimited) {
694
747
  // Auth0 rate-limit windows commonly last 30-90s. Back off long
@@ -696,6 +749,19 @@ async function getOptimizeCoockie(page) {
696
749
  // the same window and burns the attempt budget for nothing.
697
750
  await (0, randomSleep_1.randomSleep)(60000, 90000);
698
751
  }
752
+ else if (unauthenticated) {
753
+ // The rejected callbacks come in bursts lasting tens of seconds
754
+ // (every login inside the burst window fails, isolated ones all
755
+ // succeed), so back off past the burst rather than retrying inside
756
+ // it. Widen the band on each attempt instead of re-drawing from a
757
+ // fixed one: every worker caught by the same burst backs off from
758
+ // the same distribution at the same moment, so a fixed band keeps
759
+ // the herd synchronised and they re-collide on the next attempt.
760
+ // Growing the ceiling fans them out while the floor still lets a
761
+ // lucky worker recover quickly. Worst case stays well inside the
762
+ // 12-minute hook timeout.
763
+ await (0, randomSleep_1.randomSleep)(20000, 45000 + loginAttempt * 45000);
764
+ }
699
765
  else {
700
766
  await (0, randomSleep_1.randomSleep)(10000, 20000);
701
767
  }
@@ -705,6 +771,38 @@ async function getOptimizeCoockie(page) {
705
771
  }
706
772
  }
707
773
  }
774
+ }
775
+ // 8.10+ (CSL) replaces the legacy X-Optimize-* cookies below with its own
776
+ // stateful OIDC webapp session (ADR-0038:
777
+ // https://github.com/camunda/camunda-security-library/blob/main/docs/adr/0038-optimize-reuses-stateful-oidc-webapp-chain.md).
778
+ // The session cookie is just as portable as the legacy one -- extract it as a
779
+ // plain string (and the accompanying CSRF token) and the caller can close the
780
+ // page immediately, exactly like getOptimizeCoockie below.
781
+ async function loginToOptimizeSession(page) {
782
+ // A write is only valid if the X-CSRF-TOKEN cookie AND a matching header are
783
+ // both present, and it is the probe inside the login that issues that cookie
784
+ // -- so the probe has to run before cookies() is read here, which it does.
785
+ let csrfToken = '';
786
+ await performOptimizeInteractiveLogin(page, 'loginToOptimizeSession', async (loggedInPage) => {
787
+ const token = await probeOptimizeSession(loggedInPage);
788
+ if (token === null) {
789
+ return false;
790
+ }
791
+ csrfToken = token;
792
+ return true;
793
+ });
794
+ // Scoped to optimizeUrl: an unscoped cookies() call returns the whole
795
+ // browser context jar, including cookies set on the Auth0/Console origins
796
+ // visited during the interactive login, which would otherwise get flattened
797
+ // into a Cookie header Optimize never asked for.
798
+ const optimizeCookie = (await page.context().cookies(process.env.CAMUNDA_OPTIMIZE_BASE_URL))
799
+ .map((cookie) => `${cookie.name}=${cookie.value}`)
800
+ .join('; ');
801
+ return { optimizeCookie, csrfToken };
802
+ }
803
+ exports.loginToOptimizeSession = loginToOptimizeSession;
804
+ async function getOptimizeCoockie(page) {
805
+ await performOptimizeInteractiveLogin(page, 'getOptimizeCoockie');
708
806
  const context = page.context();
709
807
  // Optimize sets its session cookies asynchronously after the Auth0 redirect.
710
808
  // Poll storage state until both cookies are present (up to ~30 s).
@@ -782,9 +880,7 @@ exports.getOptimizeCookieSm = getOptimizeCookieSm;
782
880
  async function createCollection(request, options) {
783
881
  return retryOnTransientApiError(async () => {
784
882
  const optimizeResponse = await request.post(`${process.env.CAMUNDA_OPTIMIZE_BASE_URL}/api/collection`, {
785
- headers: {
786
- Cookie: options.optimizeCookie,
787
- },
883
+ headers: optimizeCookieHeaders(options),
788
884
  data: {
789
885
  name: options.name,
790
886
  },
@@ -798,9 +894,7 @@ exports.createCollection = createCollection;
798
894
  async function createDashboard(request, options) {
799
895
  return retryOnTransientApiError(async () => {
800
896
  const optimizeResponse = await request.post(`${process.env.CAMUNDA_OPTIMIZE_BASE_URL}/api/dashboard`, {
801
- headers: {
802
- Cookie: options.optimizeCookie,
803
- },
897
+ headers: optimizeCookieHeaders(options),
804
898
  data: {
805
899
  collectionId: options.collectionId,
806
900
  name: options.name,
@@ -819,6 +913,23 @@ async function createDashboard(request, options) {
819
913
  }, 'createDashboard');
820
914
  }
821
915
  exports.createDashboard = createDashboard;
916
+ // Sharing is a cluster-global Optimize setting, not a per-entity one: with it
917
+ // off, POST /api/share/{report,dashboard} answers 500 (SharingNotAllowedException
918
+ // has no exception mapper) and the UI renders the share button disabled. Specs
919
+ // that toggle it therefore have to restore it, and specs that depend on it have
920
+ // to assert the precondition themselves -- otherwise they see each other's
921
+ // writes when they run concurrently.
922
+ async function setOptimizeSharing(request, options) {
923
+ const action = options.enabled ? 'enable' : 'disable';
924
+ return retryOnTransientApiError(async () => {
925
+ const response = await request.post(`${options.baseUrl}/api/public/share/${action}`, { headers: { Authorization: options.bearerToken } });
926
+ if (![200, 204].includes(response.status())) {
927
+ throw new Error(`Expected 200 or 204 from /api/public/share/${action}, Received: ` +
928
+ `${response.status()} ${await response.text()}`);
929
+ }
930
+ }, `setOptimizeSharing(${action})`);
931
+ }
932
+ exports.setOptimizeSharing = setOptimizeSharing;
822
933
  async function createSingleProcessReport(request, options) {
823
934
  // Default configuration (can be overridden)
824
935
  const defaultConfiguration = {
@@ -880,7 +991,7 @@ async function createSingleProcessReport(request, options) {
880
991
  return retryOnTransientApiError(async () => {
881
992
  const response = await request.post(`${process.env.CAMUNDA_OPTIMIZE_BASE_URL}/api/report/process/single`, {
882
993
  headers: {
883
- Cookie: options.optimizeCookie,
994
+ ...optimizeCookieHeaders(options),
884
995
  'Content-Type': 'application/json',
885
996
  },
886
997
  data: payload,
@@ -899,7 +1010,7 @@ async function updateCollectionScope(request, options) {
899
1010
  return retryOnTransientApiError(async () => {
900
1011
  const response = await request.put(url, {
901
1012
  headers: {
902
- Cookie: options.optimizeCookie,
1013
+ ...optimizeCookieHeaders(options),
903
1014
  'Content-Type': 'application/json',
904
1015
  },
905
1016
  data: options.data,
@@ -0,0 +1,3 @@
1
+ import { Page } from '@playwright/test';
2
+ export declare function isGatewayErrorPage(page: Page): Promise<boolean>;
3
+ export declare function reloadPastGatewayError(page: Page, deadline: number): Promise<void>;
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.reloadPastGatewayError = exports.isGatewayErrorPage = void 0;
4
+ const constants_1 = require("./constants");
5
+ const GATEWAY_ERROR_HEADING = /50[234]\s+(Bad Gateway|Service Temporarily Unavailable|Gateway Time-?out)/i;
6
+ async function isGatewayErrorPage(page) {
7
+ return page
8
+ .getByRole('heading', { name: GATEWAY_ERROR_HEADING })
9
+ .first()
10
+ .isVisible()
11
+ .catch(() => false);
12
+ }
13
+ exports.isGatewayErrorPage = isGatewayErrorPage;
14
+ // While a component's pod restarts, its ingress answers with an nginx 50x page
15
+ // that carries none of the app. A 5xx does not reject `page.reload`, so a retry
16
+ // loop that reloads after every failed attempt keeps re-rendering that error
17
+ // page and spends its whole attempt budget in seconds, long before the
18
+ // component is back. Reload until the component answers again, bounded by
19
+ // `deadline` so a sustained outage still fails the caller.
20
+ async function reloadPastGatewayError(page, deadline) {
21
+ for (let attempt = 1;; attempt++) {
22
+ const response = await page.reload({ waitUntil: 'domcontentloaded' });
23
+ const status = response?.status();
24
+ const unavailable = (status !== undefined && status >= 500) ||
25
+ (await isGatewayErrorPage(page));
26
+ if (!unavailable || Date.now() >= deadline) {
27
+ return;
28
+ }
29
+ console.warn(`reloadPastGatewayError: ${page.url()} answered ${status} — waiting for the component to come back (attempt ${attempt})`);
30
+ await page.waitForTimeout(Math.min(constants_1._1_SECOND_IN_MS * 5 * attempt, deadline - Date.now()));
31
+ }
32
+ }
33
+ exports.reloadPastGatewayError = reloadPastGatewayError;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.982",
3
+ "version": "0.0.984",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",