@camunda/e2e-test-suite 0.0.1190 → 0.0.1192

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.
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.LoginPage = void 0;
4
4
  const test_1 = require("@playwright/test");
5
5
  const UtilitiesPage_1 = require("./UtilitiesPage");
6
+ const authErrorClassification_1 = require("../../utils/authErrorClassification");
6
7
  const userNameInputValidInputSchema = {
7
8
  _schema: {
8
9
  maxlength: 254,
@@ -47,9 +48,14 @@ class LoginPage {
47
48
  // missing this detection entirely, so a rate-limited login here fell
48
49
  // through to the generic 10-20s retry backoff instead of the 60-90s
49
50
  // needed to outlast a typical rate-limit window.
51
+ //
52
+ // Kept as one locator because every caller wants to stop waiting on
53
+ // either page; which of the two rendered is decided at throw time by
54
+ // classifyAuthErrorPage, since only that distinction says whether the
55
+ // remedy is a higher limit or an allow-listed runner IP.
50
56
  this.rateLimitError = page
51
- .getByText(/rate limit has been reached/i)
52
- .or(page.getByText(/We are sorry, an error occurred/i));
57
+ .getByText(authErrorClassification_1.AUTH0_QUOTA_TEXT)
58
+ .or(page.getByText(authErrorClassification_1.AUTH0_GENERIC_ERROR_TEXT));
53
59
  this.loginButton = page.getByRole('button', {
54
60
  name: 'Continue',
55
61
  exact: true,
@@ -106,13 +112,10 @@ class LoginPage {
106
112
  }
107
113
  // Surface a rate-limit page with the marker loginWithRetry keys its
108
114
  // 60-90s backoff on, rather than burning the remaining attempts on a
109
- // form the provider is not going to serve.
110
- if (await this.rateLimitError
111
- .first()
112
- .isVisible()
113
- .catch(() => false)) {
114
- throw new Error('AUTH0_RATE_LIMIT: identity provider returned an error page');
115
- }
115
+ // form the provider is not going to serve. The marker also records
116
+ // which of the two Auth0 failure modes this was, and the error copy
117
+ // behind it, so a recurrence can be diagnosed from the run log alone.
118
+ await (0, authErrorClassification_1.throwIfAuthErrorPage)(this.page);
116
119
  // Neither the password step nor the email step is on screen: some
117
120
  // interstitial we do not model. Hand back to fillPassword's own wait
118
121
  // instead of re-typing blindly into a page we cannot identify.
@@ -6,6 +6,7 @@ const ModelerHomePage_1 = require("./ModelerHomePage");
6
6
  const HomePage_1 = require("./HomePage");
7
7
  const sleep_1 = require("../../utils/sleep");
8
8
  const randomSleep_1 = require("../../utils/randomSleep");
9
+ const authErrorClassification_1 = require("../../utils/authErrorClassification");
9
10
  const LoginPage_1 = require("./LoginPage");
10
11
  const fileUpload_1 = require("../../utils/fileUpload");
11
12
  const mailSlurpClient_1 = require("../../utils/mailSlurpClient");
@@ -53,12 +54,7 @@ async function waitForLoginForm(page, loginPage, totalTimeout = 90000, roundTime
53
54
  // whole budget waiting for a login form that will never appear. Bailing
54
55
  // out immediately with a typed marker lets loginWithRetry's caller back
55
56
  // off long enough to actually outlast the rate-limit window instead.
56
- if (await loginPage.rateLimitError
57
- .first()
58
- .isVisible()
59
- .catch(() => false)) {
60
- throw new Error('AUTH0_RATE_LIMIT: identity provider returned an error page');
61
- }
57
+ await (0, authErrorClassification_1.throwIfAuthErrorPage)(page);
62
58
  try {
63
59
  // Confirm we reached the login page before filling credentials.
64
60
  // Without this check the locator for 'Email address' can match a hidden
@@ -172,7 +168,7 @@ async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5
172
168
  catch (error) {
173
169
  lastError = error;
174
170
  if (attempt < maxRetries - 1) {
175
- const rateLimited = String(error).includes('AUTH0_RATE_LIMIT');
171
+ const rateLimited = String(error).includes(authErrorClassification_1.AUTH0_RATE_LIMIT);
176
172
  console.warn(`Attempt ${attempt + 1} failed for logging in. Retrying with clean session...`, error);
177
173
  if (rateLimited) {
178
174
  // Auth0 rate-limit windows commonly last 30-90s. Back off long
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.LoginPage = void 0;
4
4
  const test_1 = require("@playwright/test");
5
+ const authErrorClassification_1 = require("../../utils/authErrorClassification");
5
6
  const UtilitiesPage_1 = require("./UtilitiesPage");
6
7
  const sessionRecovery_1 = require("../../utils/sessionRecovery");
7
8
  class LoginPage {
@@ -37,8 +38,8 @@ class LoginPage {
37
38
  });
38
39
  this.errorMessage = page.getByText('Wrong email or password');
39
40
  this.rateLimitError = page
40
- .getByText(/rate limit has been reached/i)
41
- .or(page.getByText(/We are sorry, an error occurred/i));
41
+ .getByText(authErrorClassification_1.AUTH0_QUOTA_TEXT)
42
+ .or(page.getByText(authErrorClassification_1.AUTH0_GENERIC_ERROR_TEXT));
42
43
  }
43
44
  async fillUsername(username) {
44
45
  await this.usernameInput.waitFor({ state: 'visible', timeout: 60000 });
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.waitForLoadingToFinish = exports.assertTestUsesCorrectOrganizationFromModeler = exports.expectCountToBeOneOf = exports.createAPIClient = exports.modelAndRunConnectorsTimerEventDiagram = exports.modelAndRunConnectorsDocHandlingDiagram = exports.deleteCluster = exports.deleteAllUserGroups = exports.assertTestUsesCorrectOrganization = exports.clickInvitationLinkInEmail = exports.assertLatestAlertEmail = exports.enableAuthorizations = exports.disableRBA = exports.runMultipleProcesses = exports.modelDiagramFromFile = exports.runProcess = exports.completeTaskWithRetry = exports.assertPageTextWithRetry = exports.assertLocatorVisibleWithRetry = exports.modelRestConnector = exports.loginWithRetry = exports.forceLogoutIfNeeded = void 0;
4
4
  const test_1 = require("@playwright/test");
5
+ const authErrorClassification_1 = require("../../utils/authErrorClassification");
5
6
  const ModelerHomePage_1 = require("./ModelerHomePage");
6
7
  const HomePage_1 = require("./HomePage");
7
8
  const sleep_1 = require("../../utils/sleep");
@@ -80,12 +81,7 @@ async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5
80
81
  // the usual auth controls. Bail out early on it so the outer retry backs
81
82
  // off long enough to outlast the window instead of burning attempts
82
83
  // against a 60s wait for a login form that will never render.
83
- if (await loginPage.rateLimitError
84
- .first()
85
- .isVisible()
86
- .catch(() => false)) {
87
- throw new Error('AUTH0_RATE_LIMIT: identity provider returned an error page');
88
- }
84
+ await (0, authErrorClassification_1.throwIfAuthErrorPage)(page);
89
85
  await (0, test_1.expect)(loginPage.loginMessage.or(loginPage.passwordHeading)).toBeVisible({ timeout: 60000 });
90
86
  await loginPage.loginWithTestUser(testUser);
91
87
  return;
@@ -93,7 +89,7 @@ async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5
93
89
  catch (error) {
94
90
  lastError = error;
95
91
  if (attempt < maxRetries - 1) {
96
- const rateLimited = String(error).includes('AUTH0_RATE_LIMIT');
92
+ const rateLimited = String(error).includes(authErrorClassification_1.AUTH0_RATE_LIMIT);
97
93
  console.warn(`Attempt ${attempt + 1} failed for logging in. Retrying with clean session...`, error);
98
94
  if (rateLimited) {
99
95
  // Auth0 rate-limit windows commonly last 30-90s. Back off long
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.LoginPage = void 0;
4
4
  const test_1 = require("@playwright/test");
5
+ const authErrorClassification_1 = require("../../utils/authErrorClassification");
5
6
  const UtilitiesPage_1 = require("./UtilitiesPage");
6
7
  const userNameInputValidInputSchema = {
7
8
  _schema: {
@@ -46,8 +47,8 @@ class LoginPage {
46
47
  });
47
48
  this.errorMessage = page.getByText('Wrong email or password');
48
49
  this.rateLimitError = page
49
- .getByText(/rate limit has been reached/i)
50
- .or(page.getByText(/We are sorry, an error occurred/i));
50
+ .getByText(authErrorClassification_1.AUTH0_QUOTA_TEXT)
51
+ .or(page.getByText(authErrorClassification_1.AUTH0_GENERIC_ERROR_TEXT));
51
52
  }
52
53
  async fillUsername(username) {
53
54
  await (0, test_1.expect)(this.usernameInput.locator).toBeVisible({ timeout: 120000 });
@@ -127,7 +128,11 @@ class LoginPage {
127
128
  .catch(() => false),
128
129
  ]);
129
130
  if (rateLimitVisible) {
130
- throw new Error('AUTH0_RATE_LIMIT: identity provider returned an error page');
131
+ // Classify which of the two Auth0 failure modes this was, and capture
132
+ // the error copy, so a recurrence is diagnosable from the run log.
133
+ // Falls back to the bare marker if the page moved on in between.
134
+ throw new Error((await (0, authErrorClassification_1.classifyAuthErrorPage)(this.page)) ??
135
+ `${authErrorClassification_1.AUTH0_RATE_LIMIT}: identity provider returned an error page`);
131
136
  }
132
137
  if (passwordHeadingVisible || passwordInputVisible) {
133
138
  authStep = 'password';
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.waitForLoadingToFinish = exports.assertTestUsesCorrectOrganizationFromModeler = exports.expectCountToBeOneOf = exports.createAPIClient = exports.modelAndRunConnectorsTimerEventDiagram = exports.modelAndRunConnectorsDocHandlingDiagram = exports.deleteCluster = exports.deleteAllUserGroups = exports.assertTestUsesCorrectOrganization = exports.assertLatestAlertEmail = exports.clickInvitationLinkInEmail = exports.enableAuthorizations = exports.disableRBA = exports.runMultipleProcesses = exports.runProcess = exports.completeTaskWithRetry = exports.assertPageTextWithRetry = exports.assertLocatorVisibleWithRetry = exports.modelRestConnector = exports.modelDiagramFromFile = exports.loginWithRetry = void 0;
4
4
  const test_1 = require("@playwright/test");
5
+ const authErrorClassification_1 = require("../../utils/authErrorClassification");
5
6
  const ModelerHomePage_1 = require("./ModelerHomePage");
6
7
  const HomePage_1 = require("./HomePage");
7
8
  const sleep_1 = require("../../utils/sleep");
@@ -51,12 +52,7 @@ async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5
51
52
  // The identity provider occasionally returns a generic error / rate
52
53
  // limit page that has none of the usual auth controls. Bail out early
53
54
  // when that page is visible so the outer retry can back off.
54
- if (await loginPage.rateLimitError
55
- .first()
56
- .isVisible()
57
- .catch(() => false)) {
58
- throw new Error('AUTH0_RATE_LIMIT: identity provider returned an error page');
59
- }
55
+ await (0, authErrorClassification_1.throwIfAuthErrorPage)(page);
60
56
  // Auth0 may restore directly on password entry depending on session
61
57
  // history, so allow either identifier screen or password screen.
62
58
  await (0, test_1.expect)(loginPage.loginMessage.or(loginPage.passwordHeading)).toBeVisible({ timeout: 60000 });
@@ -66,7 +62,7 @@ async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5
66
62
  catch (error) {
67
63
  lastError = error;
68
64
  if (attempt < maxRetries - 1) {
69
- const rateLimited = String(error).includes('AUTH0_RATE_LIMIT');
65
+ const rateLimited = String(error).includes(authErrorClassification_1.AUTH0_RATE_LIMIT);
70
66
  console.warn(`Attempt ${attempt + 1} failed for logging in. Retrying with clean session...`, error);
71
67
  if (rateLimited) {
72
68
  // Auth0 rate-limit windows commonly last 30-90s. Back off long
@@ -0,0 +1,71 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL"
3
+ xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
4
+ xmlns:dc="http://www.omg.org/spec/DD/20100524/DC"
5
+ xmlns:zeebe="http://camunda.org/schema/zeebe/1.0"
6
+ xmlns:di="http://www.omg.org/spec/DD/20100524/DI"
7
+ xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_mcp_meta"
8
+ targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.42.0"
9
+ modeler:executionPlatform="Camunda Cloud" modeler:executionPlatformVersion="8.9.0">
10
+ <bpmn:process id="mcp_remote_client_meta" name="mcp_remote_client_meta" isExecutable="true">
11
+ <bpmn:startEvent id="StartEvent_1">
12
+ <bpmn:outgoing>Flow_start_to_echo</bpmn:outgoing>
13
+ </bpmn:startEvent>
14
+ <bpmn:serviceTask id="Activity_echo_meta"
15
+ zeebe:modelerTemplate="io.camunda.connectors.agenticai.mcp.remoteclient.v0"
16
+ zeebe:modelerTemplateVersion="3">
17
+ <bpmn:extensionElements>
18
+ <zeebe:taskDefinition type="io.camunda.agenticai:mcpremoteclient:1" retries="3" />
19
+ <zeebe:ioMapping>
20
+ <zeebe:input source="http" target="data.transport.type" />
21
+ <zeebe:input source="http://localhost:12001/mcp"
22
+ target="data.transport.http.url" />
23
+ <zeebe:input source="noAuth" target="data.transport.http.authentication.type" />
24
+ <zeebe:input source="=false" target="data.options.clientCache" />
25
+ <zeebe:input source="standalone" target="data.connectorMode.type" />
26
+ <zeebe:input source="tools/call" target="data.connectorMode.operation.type" />
27
+ <zeebe:input source="echoMeta" target="data.connectorMode.operation.toolName" />
28
+ <zeebe:input source="={exampleMetaKey: &#34;exampleMetaValue&#34;}"
29
+ target="data.connectorMode.meta" />
30
+ </zeebe:ioMapping>
31
+ <zeebe:taskHeaders>
32
+ <zeebe:header key="elementTemplateVersion" value="3" />
33
+ <zeebe:header key="elementTemplateId"
34
+ value="io.camunda.connectors.agenticai.mcp.remoteclient.v0" />
35
+ <zeebe:header key="resultVariable" value="echoMetaResult" />
36
+ <zeebe:header key="retryBackoff" value="PT0S" />
37
+ </zeebe:taskHeaders>
38
+ </bpmn:extensionElements>
39
+ <bpmn:incoming>Flow_start_to_echo</bpmn:incoming>
40
+ <bpmn:outgoing>Flow_echo_to_end</bpmn:outgoing>
41
+ </bpmn:serviceTask>
42
+ <bpmn:endEvent id="Event_end">
43
+ <bpmn:incoming>Flow_echo_to_end</bpmn:incoming>
44
+ </bpmn:endEvent>
45
+ <bpmn:sequenceFlow id="Flow_start_to_echo" sourceRef="StartEvent_1"
46
+ targetRef="Activity_echo_meta" />
47
+ <bpmn:sequenceFlow id="Flow_echo_to_end" sourceRef="Activity_echo_meta"
48
+ targetRef="Event_end" />
49
+ </bpmn:process>
50
+ <bpmndi:BPMNDiagram id="BPMNDiagram_1">
51
+ <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="mcp_remote_client_meta">
52
+ <bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
53
+ <dc:Bounds x="152" y="99" width="36" height="36" />
54
+ </bpmndi:BPMNShape>
55
+ <bpmndi:BPMNShape id="Activity_echo_meta_di" bpmnElement="Activity_echo_meta">
56
+ <dc:Bounds x="250" y="77" width="100" height="80" />
57
+ </bpmndi:BPMNShape>
58
+ <bpmndi:BPMNShape id="Event_end_di" bpmnElement="Event_end">
59
+ <dc:Bounds x="412" y="99" width="36" height="36" />
60
+ </bpmndi:BPMNShape>
61
+ <bpmndi:BPMNEdge id="Flow_start_to_echo_di" bpmnElement="Flow_start_to_echo">
62
+ <di:waypoint x="188" y="117" />
63
+ <di:waypoint x="250" y="117" />
64
+ </bpmndi:BPMNEdge>
65
+ <bpmndi:BPMNEdge id="Flow_echo_to_end_di" bpmnElement="Flow_echo_to_end">
66
+ <di:waypoint x="350" y="117" />
67
+ <di:waypoint x="412" y="117" />
68
+ </bpmndi:BPMNEdge>
69
+ </bpmndi:BPMNPlane>
70
+ </bpmndi:BPMNDiagram>
71
+ </bpmn:definitions>
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const promises_1 = require("node:fs/promises");
4
+ const node_os_1 = require("node:os");
5
+ const node_path_1 = require("node:path");
6
+ const c8Run_8_10_1 = require("../../fixtures/c8Run-8.10");
7
+ const test_1 = require("@playwright/test");
8
+ const UtilitiesPage_1 = require("../../pages/c8Run-8.10/UtilitiesPage");
9
+ const _setup_1 = require("../../test-setup.js");
10
+ const apiHelpers_1 = require("../../utils/apiHelpers");
11
+ const zeebeClient_1 = require("../../utils/zeebeClient");
12
+ let processId;
13
+ let bpmnPath;
14
+ c8Run_8_10_1.test.beforeAll(async () => {
15
+ if (process.env.MCP_SERVER_AVAILABLE !== 'true') {
16
+ return;
17
+ }
18
+ await (0, apiHelpers_1.validateMcpServerHealth)();
19
+ await (0, apiHelpers_1.waitForConnectorsReady)(undefined, 300000, [
20
+ 'io.camunda.agenticai:mcpremoteclient:1',
21
+ ]);
22
+ processId = `mcp_remote_client_meta_${await (0, _setup_1.generateRandomStringAsync)(6)}`;
23
+ const tempDir = await (0, promises_1.mkdtemp)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'mcp-meta-'));
24
+ bpmnPath = (0, node_path_1.join)(tempDir, 'mcp_remote_client_meta.bpmn');
25
+ const bpmn = await (0, promises_1.readFile)('./resources/mcp_server/mcp_remote_client_meta.bpmn', 'utf8');
26
+ await (0, promises_1.writeFile)(bpmnPath, bpmn.replaceAll('mcp_remote_client_meta', processId));
27
+ await (0, zeebeClient_1.deploy)([bpmnPath]);
28
+ await (0, zeebeClient_1.createInstances)(processId, 1, 1);
29
+ });
30
+ c8Run_8_10_1.test.afterAll(async () => {
31
+ if (bpmnPath) {
32
+ await (0, promises_1.rm)((0, node_path_1.dirname)(bpmnPath), { recursive: true, force: true });
33
+ }
34
+ });
35
+ c8Run_8_10_1.test.describe('@tasklistV2 MCP Client connector _meta tests', () => {
36
+ c8Run_8_10_1.test.skip(process.env.MCP_SERVER_AVAILABLE !== 'true', 'MCP server is only available on Linux and macOS CI runners');
37
+ c8Run_8_10_1.test.beforeEach(async ({ page, operateLoginPage, operateHomePage }) => {
38
+ await (0, UtilitiesPage_1.navigateToApp)(page, 'operate');
39
+ await operateLoginPage.login('demo', 'demo');
40
+ await operateHomePage.operateBannerIsVisible();
41
+ });
42
+ c8Run_8_10_1.test.afterEach(async ({ page }, testInfo) => {
43
+ await (0, _setup_1.captureScreenshot)(page, testInfo);
44
+ await (0, _setup_1.captureFailureVideo)(page, testInfo);
45
+ });
46
+ (0, c8Run_8_10_1.test)('As a user I can forward MCP _meta and have it echoed back by the server', async ({ operateHomePage, operateProcessesPage, operateProcessInstancePage, }) => {
47
+ c8Run_8_10_1.test.slow();
48
+ await c8Run_8_10_1.test.step('Navigate to completed process instance', async () => {
49
+ await operateHomePage.clickProcessesTab();
50
+ await operateProcessesPage.clickProcessCompletedCheckbox();
51
+ await operateProcessesPage.clickProcessInstanceLink(processId);
52
+ await operateProcessInstancePage.completedIconAssertion();
53
+ });
54
+ await c8Run_8_10_1.test.step('Verify echoMetaResult reflects the configured _meta', async () => {
55
+ await operateProcessInstancePage.verifyVariableJsonContent('echoMetaResult', (value) => {
56
+ const data = value;
57
+ (0, test_1.expect)(data.name).toBe('echoMeta');
58
+ (0, test_1.expect)(data.isError).toBe(false);
59
+ (0, test_1.expect)(data.content).toBeDefined();
60
+ (0, test_1.expect)(data.content.length).toBeGreaterThan(0);
61
+ (0, test_1.expect)(data.content[0].type).toBe('text');
62
+ // The mcp-test-server echoMeta tool returns the request `_meta`
63
+ // serialized verbatim as a JSON string. The MCP client forwards
64
+ // `data.connectorMode.meta` unmodified as the request `_meta`, so a
65
+ // successful round-trip proves end-to-end `_meta` propagation.
66
+ const echoed = JSON.parse(data.content[0].text);
67
+ (0, test_1.expect)(echoed).toMatchObject({
68
+ exampleMetaKey: 'exampleMetaValue',
69
+ });
70
+ });
71
+ });
72
+ });
73
+ });
@@ -0,0 +1,8 @@
1
+ import { Page } from '@playwright/test';
2
+ export declare const AUTH0_RATE_LIMIT = "AUTH0_RATE_LIMIT";
3
+ export declare const AUTH0_RATE_LIMIT_QUOTA = "AUTH0_RATE_LIMIT_QUOTA";
4
+ export declare const AUTH0_RATE_LIMIT_GENERIC = "AUTH0_RATE_LIMIT_GENERIC";
5
+ export declare const AUTH0_QUOTA_TEXT: RegExp;
6
+ export declare const AUTH0_GENERIC_ERROR_TEXT: RegExp;
7
+ export declare function classifyAuthErrorPage(page: Page): Promise<string | null>;
8
+ export declare function throwIfAuthErrorPage(page: Page): Promise<void>;
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.throwIfAuthErrorPage = exports.classifyAuthErrorPage = exports.AUTH0_GENERIC_ERROR_TEXT = exports.AUTH0_QUOTA_TEXT = exports.AUTH0_RATE_LIMIT_GENERIC = exports.AUTH0_RATE_LIMIT_QUOTA = exports.AUTH0_RATE_LIMIT = void 0;
4
+ // Auth0 renders an error page for two structurally different failures, and
5
+ // until now both were counted as one signal. They need different responses:
6
+ //
7
+ // QUOTA - the tenant's request rate limit ("...rate limit has been
8
+ // reached"). This is what moving the tenant to a Production
9
+ // environment raises.
10
+ // GENERIC - Auth0's catch-all error page. Attack Protection blocks
11
+ // (suspicious IP throttling, brute-force protection) surface here,
12
+ // and those are governed by their own settings rather than the
13
+ // tenant's rate limits, so a Production environment does nothing
14
+ // for them. A CI runner signing in as ~19 freshly created accounts
15
+ // from one egress IP inside a few minutes is precisely the pattern
16
+ // they exist to detect.
17
+ //
18
+ // Telling them apart matters because the remedies do not overlap: one is a
19
+ // limit to raise, the other is a source IP to allow-list. Counting them as one
20
+ // number is why we could not say which we were hitting.
21
+ //
22
+ // Both markers keep the AUTH0_RATE_LIMIT prefix, so the existing
23
+ // String(error).includes(AUTH0_RATE_LIMIT) backoff checks match unchanged.
24
+ exports.AUTH0_RATE_LIMIT = 'AUTH0_RATE_LIMIT';
25
+ exports.AUTH0_RATE_LIMIT_QUOTA = `${exports.AUTH0_RATE_LIMIT}_QUOTA`;
26
+ exports.AUTH0_RATE_LIMIT_GENERIC = `${exports.AUTH0_RATE_LIMIT}_GENERIC`;
27
+ exports.AUTH0_QUOTA_TEXT = /rate limit has been reached/i;
28
+ exports.AUTH0_GENERIC_ERROR_TEXT = /We are sorry, an error occurred/i;
29
+ const MAX_DETAIL_CHARS = 300;
30
+ // The error page's own copy and error code are what separate the two
31
+ // mechanisms, so capture them rather than only the fact that some error
32
+ // rendered. Deliberately narrow: host plus path, and Auth0's two error query
33
+ // params, never the whole URL - on an OAuth redirect that can carry `code` and
34
+ // `state`, which do not belong in a CI log.
35
+ async function readErrorContext(page) {
36
+ const parts = [];
37
+ try {
38
+ const url = new URL(page.url());
39
+ parts.push(`${url.host}${url.pathname}`);
40
+ for (const key of ['error', 'error_description']) {
41
+ const value = url.searchParams.get(key);
42
+ if (value) {
43
+ parts.push(`${key}=${value}`);
44
+ }
45
+ }
46
+ }
47
+ catch {
48
+ // page.url() is not always parsable (about:blank on a torn-down context).
49
+ }
50
+ try {
51
+ const text = await page.locator('body').innerText({ timeout: 5000 });
52
+ const collapsed = text.replace(/\s+/g, ' ').trim();
53
+ if (collapsed) {
54
+ parts.push(`text="${collapsed.slice(0, MAX_DETAIL_CHARS)}"`);
55
+ }
56
+ }
57
+ catch {
58
+ // Best-effort only: this runs on a failure path and must never replace
59
+ // the caller's diagnosis with its own.
60
+ }
61
+ return parts.join(' | ');
62
+ }
63
+ // Returns the message to throw when an Auth0 error page is on screen, or null
64
+ // when it is not. Never throws, for the reason above.
65
+ async function classifyAuthErrorPage(page) {
66
+ const [quota, generic] = await Promise.all([
67
+ page
68
+ .getByText(exports.AUTH0_QUOTA_TEXT)
69
+ .first()
70
+ .isVisible()
71
+ .catch(() => false),
72
+ page
73
+ .getByText(exports.AUTH0_GENERIC_ERROR_TEXT)
74
+ .first()
75
+ .isVisible()
76
+ .catch(() => false),
77
+ ]);
78
+ if (!quota && !generic) {
79
+ return null;
80
+ }
81
+ // Quota wins when both match: its copy is the specific diagnosis, and the
82
+ // generic banner can wrap it.
83
+ const marker = quota ? exports.AUTH0_RATE_LIMIT_QUOTA : exports.AUTH0_RATE_LIMIT_GENERIC;
84
+ const context = await readErrorContext(page);
85
+ const summary = `${marker}: identity provider returned an error page`;
86
+ return context ? `${summary} | ${context}` : summary;
87
+ }
88
+ exports.classifyAuthErrorPage = classifyAuthErrorPage;
89
+ async function throwIfAuthErrorPage(page) {
90
+ const message = await classifyAuthErrorPage(page);
91
+ if (message) {
92
+ throw new Error(message);
93
+ }
94
+ }
95
+ exports.throwIfAuthErrorPage = throwIfAuthErrorPage;
@@ -1,3 +1,4 @@
1
1
  import { Page } from '@playwright/test';
2
- export declare const AUTH0_RATE_LIMIT = "AUTH0_RATE_LIMIT";
2
+ import { AUTH0_RATE_LIMIT } from './authErrorClassification';
3
+ export { AUTH0_RATE_LIMIT };
3
4
  export declare function loginToApp(appName: string, page: Page): Promise<void>;
@@ -3,16 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.loginToApp = exports.AUTH0_RATE_LIMIT = void 0;
4
4
  const test_1 = require("@playwright/test");
5
5
  const LoginPage_1 = require("../pages/8.8/LoginPage");
6
- exports.AUTH0_RATE_LIMIT = 'AUTH0_RATE_LIMIT';
7
- async function throwIfRateLimited(loginPage) {
8
- const rateLimited = await loginPage.rateLimitError
9
- .first()
10
- .isVisible()
11
- .catch(() => false);
12
- if (rateLimited) {
13
- throw new Error(`${exports.AUTH0_RATE_LIMIT}: identity provider returned an error page`);
14
- }
15
- }
6
+ const authErrorClassification_1 = require("./authErrorClassification");
7
+ Object.defineProperty(exports, "AUTH0_RATE_LIMIT", { enumerable: true, get: function () { return authErrorClassification_1.AUTH0_RATE_LIMIT; } });
16
8
  async function loginToApp(appName, page) {
17
9
  await page.goto(appName, { waitUntil: 'domcontentloaded', timeout: 60000 });
18
10
  // 8.8 SaaS occasionally experiences 429s on app.js during navigation rendering,
@@ -25,13 +17,13 @@ async function loginToApp(appName, page) {
25
17
  // caller can tell a rate limit apart from a slow login form, and the caller
26
18
  // needs that distinction to pick a backoff long enough to outlast the window.
27
19
  await (0, test_1.expect)(loginPage.usernameInput.or(loginPage.rateLimitError.first())).toBeVisible({ timeout: 60000 });
28
- await throwIfRateLimited(loginPage);
20
+ await (0, authErrorClassification_1.throwIfAuthErrorPage)(page);
29
21
  await loginPage.fillUsername(process.env.C8_USERNAME);
30
22
  await (0, test_1.expect)(loginPage.usernameInput).toHaveValue(process.env.C8_USERNAME);
31
23
  await (0, test_1.expect)(loginPage.loginMessage).toBeVisible();
32
24
  await loginPage.clickContinueButton();
33
25
  await (0, test_1.expect)(loginPage.passwordInput.or(loginPage.rateLimitError.first())).toBeVisible({ timeout: 90000 });
34
- await throwIfRateLimited(loginPage);
26
+ await (0, authErrorClassification_1.throwIfAuthErrorPage)(page);
35
27
  await loginPage.fillPassword(process.env.C8_PASSWORD);
36
28
  await (0, test_1.expect)(loginPage.passwordHeading).toBeVisible({ timeout: 90000 });
37
29
  await loginPage.clickLoginButton();
@@ -44,7 +36,7 @@ async function loginToApp(appName, page) {
44
36
  catch (error) {
45
37
  // Auth0 can also serve the rate-limit page as the response to the password
46
38
  // submit, in which case the redirect back to appName never happens.
47
- await throwIfRateLimited(loginPage);
39
+ await (0, authErrorClassification_1.throwIfAuthErrorPage)(page);
48
40
  throw error;
49
41
  }
50
42
  await page
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.1190",
3
+ "version": "0.0.1192",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,71 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL"
3
+ xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
4
+ xmlns:dc="http://www.omg.org/spec/DD/20100524/DC"
5
+ xmlns:zeebe="http://camunda.org/schema/zeebe/1.0"
6
+ xmlns:di="http://www.omg.org/spec/DD/20100524/DI"
7
+ xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_mcp_meta"
8
+ targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.42.0"
9
+ modeler:executionPlatform="Camunda Cloud" modeler:executionPlatformVersion="8.9.0">
10
+ <bpmn:process id="mcp_remote_client_meta" name="mcp_remote_client_meta" isExecutable="true">
11
+ <bpmn:startEvent id="StartEvent_1">
12
+ <bpmn:outgoing>Flow_start_to_echo</bpmn:outgoing>
13
+ </bpmn:startEvent>
14
+ <bpmn:serviceTask id="Activity_echo_meta"
15
+ zeebe:modelerTemplate="io.camunda.connectors.agenticai.mcp.remoteclient.v0"
16
+ zeebe:modelerTemplateVersion="3">
17
+ <bpmn:extensionElements>
18
+ <zeebe:taskDefinition type="io.camunda.agenticai:mcpremoteclient:1" retries="3" />
19
+ <zeebe:ioMapping>
20
+ <zeebe:input source="http" target="data.transport.type" />
21
+ <zeebe:input source="http://localhost:12001/mcp"
22
+ target="data.transport.http.url" />
23
+ <zeebe:input source="noAuth" target="data.transport.http.authentication.type" />
24
+ <zeebe:input source="=false" target="data.options.clientCache" />
25
+ <zeebe:input source="standalone" target="data.connectorMode.type" />
26
+ <zeebe:input source="tools/call" target="data.connectorMode.operation.type" />
27
+ <zeebe:input source="echoMeta" target="data.connectorMode.operation.toolName" />
28
+ <zeebe:input source="={exampleMetaKey: &#34;exampleMetaValue&#34;}"
29
+ target="data.connectorMode.meta" />
30
+ </zeebe:ioMapping>
31
+ <zeebe:taskHeaders>
32
+ <zeebe:header key="elementTemplateVersion" value="3" />
33
+ <zeebe:header key="elementTemplateId"
34
+ value="io.camunda.connectors.agenticai.mcp.remoteclient.v0" />
35
+ <zeebe:header key="resultVariable" value="echoMetaResult" />
36
+ <zeebe:header key="retryBackoff" value="PT0S" />
37
+ </zeebe:taskHeaders>
38
+ </bpmn:extensionElements>
39
+ <bpmn:incoming>Flow_start_to_echo</bpmn:incoming>
40
+ <bpmn:outgoing>Flow_echo_to_end</bpmn:outgoing>
41
+ </bpmn:serviceTask>
42
+ <bpmn:endEvent id="Event_end">
43
+ <bpmn:incoming>Flow_echo_to_end</bpmn:incoming>
44
+ </bpmn:endEvent>
45
+ <bpmn:sequenceFlow id="Flow_start_to_echo" sourceRef="StartEvent_1"
46
+ targetRef="Activity_echo_meta" />
47
+ <bpmn:sequenceFlow id="Flow_echo_to_end" sourceRef="Activity_echo_meta"
48
+ targetRef="Event_end" />
49
+ </bpmn:process>
50
+ <bpmndi:BPMNDiagram id="BPMNDiagram_1">
51
+ <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="mcp_remote_client_meta">
52
+ <bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
53
+ <dc:Bounds x="152" y="99" width="36" height="36" />
54
+ </bpmndi:BPMNShape>
55
+ <bpmndi:BPMNShape id="Activity_echo_meta_di" bpmnElement="Activity_echo_meta">
56
+ <dc:Bounds x="250" y="77" width="100" height="80" />
57
+ </bpmndi:BPMNShape>
58
+ <bpmndi:BPMNShape id="Event_end_di" bpmnElement="Event_end">
59
+ <dc:Bounds x="412" y="99" width="36" height="36" />
60
+ </bpmndi:BPMNShape>
61
+ <bpmndi:BPMNEdge id="Flow_start_to_echo_di" bpmnElement="Flow_start_to_echo">
62
+ <di:waypoint x="188" y="117" />
63
+ <di:waypoint x="250" y="117" />
64
+ </bpmndi:BPMNEdge>
65
+ <bpmndi:BPMNEdge id="Flow_echo_to_end_di" bpmnElement="Flow_echo_to_end">
66
+ <di:waypoint x="350" y="117" />
67
+ <di:waypoint x="412" y="117" />
68
+ </bpmndi:BPMNEdge>
69
+ </bpmndi:BPMNPlane>
70
+ </bpmndi:BPMNDiagram>
71
+ </bpmn:definitions>