@camunda/e2e-test-suite 0.0.1242 → 0.0.1243

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 (82) hide show
  1. package/dist/fixtures/api.d.ts +15 -0
  2. package/dist/fixtures/api.js +27 -0
  3. package/dist/tests/api/v2/authorization/authorization-api-tests.spec.d.ts +1 -0
  4. package/dist/tests/api/v2/authorization/authorization-api-tests.spec.js +247 -0
  5. package/dist/tests/api/v2/authorization/authorization-enforcement-api-tests.spec.d.ts +18 -0
  6. package/dist/tests/api/v2/authorization/authorization-enforcement-api-tests.spec.js +208 -0
  7. package/dist/tests/api/v2/batch-operation/batch-operation-api-tests.spec.d.ts +1 -0
  8. package/dist/tests/api/v2/batch-operation/batch-operation-api-tests.spec.js +264 -0
  9. package/dist/tests/api/v2/cluster/cluster-api-tests.spec.d.ts +1 -0
  10. package/dist/tests/api/v2/cluster/cluster-api-tests.spec.js +86 -0
  11. package/dist/tests/api/v2/decision/decision-api-tests.spec.d.ts +1 -0
  12. package/dist/tests/api/v2/decision/decision-api-tests.spec.js +233 -0
  13. package/dist/tests/api/v2/deployment/deployment-api-tests.spec.d.ts +1 -0
  14. package/dist/tests/api/v2/deployment/deployment-api-tests.spec.js +147 -0
  15. package/dist/tests/api/v2/document/document-api-tests.spec.d.ts +9 -0
  16. package/dist/tests/api/v2/document/document-api-tests.spec.js +111 -0
  17. package/dist/tests/api/v2/element-instance/element-instance-api-tests.spec.d.ts +1 -0
  18. package/dist/tests/api/v2/element-instance/element-instance-api-tests.spec.js +127 -0
  19. package/dist/tests/api/v2/flows/end-to-end-flows.spec.d.ts +9 -0
  20. package/dist/tests/api/v2/flows/end-to-end-flows.spec.js +204 -0
  21. package/dist/tests/api/v2/form/form-api-tests.spec.d.ts +1 -0
  22. package/dist/tests/api/v2/form/form-api-tests.spec.js +88 -0
  23. package/dist/tests/api/v2/identity/group-api-tests.spec.d.ts +1 -0
  24. package/dist/tests/api/v2/identity/group-api-tests.spec.js +172 -0
  25. package/dist/tests/api/v2/identity/membership-api-tests.spec.d.ts +10 -0
  26. package/dist/tests/api/v2/identity/membership-api-tests.spec.js +164 -0
  27. package/dist/tests/api/v2/identity/role-api-tests.spec.d.ts +1 -0
  28. package/dist/tests/api/v2/identity/role-api-tests.spec.js +212 -0
  29. package/dist/tests/api/v2/identity/tenant-api-tests.spec.d.ts +1 -0
  30. package/dist/tests/api/v2/identity/tenant-api-tests.spec.js +319 -0
  31. package/dist/tests/api/v2/identity/user-api-tests.spec.d.ts +1 -0
  32. package/dist/tests/api/v2/identity/user-api-tests.spec.js +148 -0
  33. package/dist/tests/api/v2/job/job-api-tests.spec.d.ts +1 -0
  34. package/dist/tests/api/v2/job/job-api-tests.spec.js +260 -0
  35. package/dist/tests/api/v2/message/message-api-tests.spec.d.ts +1 -0
  36. package/dist/tests/api/v2/message/message-api-tests.spec.js +219 -0
  37. package/dist/tests/api/v2/process-definition/process-definition-api-tests.spec.d.ts +1 -0
  38. package/dist/tests/api/v2/process-definition/process-definition-api-tests.spec.js +118 -0
  39. package/dist/tests/api/v2/process-instance/process-instance-api-tests.spec.d.ts +1 -0
  40. package/dist/tests/api/v2/process-instance/process-instance-api-tests.spec.js +335 -0
  41. package/dist/tests/api/v2/runtime/batch-modification-api-tests.spec.d.ts +1 -0
  42. package/dist/tests/api/v2/runtime/batch-modification-api-tests.spec.js +123 -0
  43. package/dist/tests/api/v2/runtime/variable-incident-api-tests.spec.d.ts +1 -0
  44. package/dist/tests/api/v2/runtime/variable-incident-api-tests.spec.js +174 -0
  45. package/dist/tests/api/v2/system/clock-api-tests.spec.d.ts +19 -0
  46. package/dist/tests/api/v2/system/clock-api-tests.spec.js +62 -0
  47. package/dist/tests/api/v2/system/system-api-tests.spec.d.ts +1 -0
  48. package/dist/tests/api/v2/system/system-api-tests.spec.js +183 -0
  49. package/dist/tests/api/v2/user-task/user-task-api-tests.spec.d.ts +1 -0
  50. package/dist/tests/api/v2/user-task/user-task-api-tests.spec.js +258 -0
  51. package/dist/utils/api/assertions.d.ts +50 -0
  52. package/dist/utils/api/assertions.js +142 -0
  53. package/dist/utils/api/auth.d.ts +27 -0
  54. package/dist/utils/api/auth.js +137 -0
  55. package/dist/utils/api/beans.d.ts +46 -0
  56. package/dist/utils/api/beans.js +87 -0
  57. package/dist/utils/api/bpmn.d.ts +50 -0
  58. package/dist/utils/api/bpmn.js +217 -0
  59. package/dist/utils/api/capabilities.d.ts +90 -0
  60. package/dist/utils/api/capabilities.js +198 -0
  61. package/dist/utils/api/cleanup.d.ts +24 -0
  62. package/dist/utils/api/cleanup.js +120 -0
  63. package/dist/utils/api/config.d.ts +103 -0
  64. package/dist/utils/api/config.js +62 -0
  65. package/dist/utils/api/deployments.d.ts +45 -0
  66. package/dist/utils/api/deployments.js +120 -0
  67. package/dist/utils/api/eventually.d.ts +36 -0
  68. package/dist/utils/api/eventually.js +54 -0
  69. package/dist/utils/api/http.d.ts +56 -0
  70. package/dist/utils/api/http.js +244 -0
  71. package/dist/utils/api/identity.d.ts +29 -0
  72. package/dist/utils/api/identity.js +43 -0
  73. package/dist/utils/api/keys.d.ts +37 -0
  74. package/dist/utils/api/keys.js +40 -0
  75. package/dist/utils/api/paths.d.ts +25 -0
  76. package/dist/utils/api/paths.js +167 -0
  77. package/dist/utils/api/tenancy.d.ts +31 -0
  78. package/dist/utils/api/tenancy.js +65 -0
  79. package/package.json +1 -1
  80. package/playwright.config.ts +14 -2
  81. package/dist/resources/test-api-v2-complete.sh +0 -12246
  82. package/resources/test-api-v2-complete.sh +0 -12246
@@ -0,0 +1,244 @@
1
+ "use strict";
2
+ /**
3
+ * HTTP client for the Orchestration Cluster REST v2 API.
4
+ *
5
+ * Wraps Playwright's `APIRequestContext` with the three things a Helm-deployed
6
+ * cluster needs and a localhost one does not: pluggable auth (see `auth.ts`),
7
+ * tenant propagation (see `tenancy.ts`), and tolerance of the ingress dropping
8
+ * requests while orchestration pods roll during an upgrade.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.ApiClient = void 0;
12
+ const config_1 = require("./config");
13
+ const auth_1 = require("./auth");
14
+ const tenancy_1 = require("./tenancy");
15
+ /**
16
+ * Two different layers answer 5xx while a pod rolls during an upgrade-minor
17
+ * run, and both are transient rather than a real answer to any assertion here.
18
+ *
19
+ * The ingress emits an nginx error page when it cannot reach an orchestration
20
+ * pod: 502/504 when an upstream is reachable but not answering, 503 when no
21
+ * endpoint is ready at all.
22
+ *
23
+ * The orchestration REST API itself emits 503 UNAVAILABLE ("The search client
24
+ * could not connect to the search server") while its search client briefly
25
+ * cannot reach Elasticsearch/OpenSearch — the same restart window, one layer
26
+ * down. That one carries a real JSON problem body, so a content-type check
27
+ * alone reads it as a final answer and skips the retry entirely. This is the
28
+ * bug 2f00674 fixed in the shell suite, reintroduced here by porting the
29
+ * nginx-only check without its follow-up.
30
+ *
31
+ * Anything else — including a JSON 5xx that is not the search-backend message
32
+ * — is returned as-is, and a sustained outage still surfaces once the retry
33
+ * budget is spent.
34
+ */
35
+ const GATEWAY_STATUSES = new Set([502, 503, 504]);
36
+ const GATEWAY_MAX_ATTEMPTS = 12;
37
+ const GATEWAY_MAX_BACKOFF_MS = 10000;
38
+ const SEARCH_BACKEND_UNAVAILABLE = 'could not connect to the search server';
39
+ /**
40
+ * The same pod roll also drops connections outright, which never reaches the
41
+ * status check above: `fetch` rejects instead of resolving, so the retry loop
42
+ * was skipped entirely and the test failed with a bare "socket hang up".
43
+ *
44
+ * These are transport-level failures with no HTTP answer behind them, so they
45
+ * carry no information about the assertion under test and are retried on the
46
+ * same budget as a gateway 5xx. A name not in this list propagates unchanged —
47
+ * a genuine client bug must not be retried into a timeout.
48
+ */
49
+ const TRANSIENT_CONNECTION_ERRORS = [
50
+ 'socket hang up',
51
+ 'ECONNRESET',
52
+ 'ECONNREFUSED',
53
+ 'EPIPE',
54
+ 'ETIMEDOUT',
55
+ 'EAI_AGAIN',
56
+ 'other side closed',
57
+ 'network socket disconnected',
58
+ ];
59
+ /**
60
+ * The XML endpoints declare `produces = {text/xml, application/problem+json}`
61
+ * (see `ProcessDefinitionController` / `DecisionDefinitionController` in
62
+ * camunda/camunda), so a blanket `Accept: application/json` makes Spring answer
63
+ * 406 before the handler runs — which reads as a failed assertion on a
64
+ * perfectly healthy cluster, and also masks the 404 an absent key should give.
65
+ *
66
+ * Deriving this from the path rather than the call site keeps a new `/xml`
67
+ * endpoint correct by default.
68
+ */
69
+ function acceptFor(path) {
70
+ return path?.endsWith('/xml')
71
+ ? 'text/xml, application/problem+json'
72
+ : 'application/json';
73
+ }
74
+ function isTransientConnectionError(error) {
75
+ const message = error instanceof Error ? error.message : String(error);
76
+ return TRANSIENT_CONNECTION_ERRORS.some((needle) => message.toLowerCase().includes(needle.toLowerCase()));
77
+ }
78
+ function backoffMs(attempt) {
79
+ return Math.min(attempt * 2000, GATEWAY_MAX_BACKOFF_MS);
80
+ }
81
+ async function isGatewayError(response) {
82
+ if (!GATEWAY_STATUSES.has(response.status()))
83
+ return false;
84
+ const body = await response.text().catch(() => '');
85
+ return (body.includes('nginx') ||
86
+ body.trim() === '' ||
87
+ body.includes(SEARCH_BACKEND_UNAVAILABLE));
88
+ }
89
+ function sleep(ms) {
90
+ return new Promise((resolve) => setTimeout(resolve, ms));
91
+ }
92
+ /**
93
+ * Announces a replayed request that could have had an effect already.
94
+ *
95
+ * A retry can duplicate a side effect: the connection can drop *after* the
96
+ * engine accepted the command, so a replayed `POST /process-instances` is two
97
+ * instances and a replayed deployment is a second version.
98
+ *
99
+ * Not retrying commands is not the safer default here. The retry exists
100
+ * because an upgrade-minor run rolls the orchestration pods under a live
101
+ * suite, and its absence produced 12 `socket hang up` failures in a single
102
+ * run. Neither Playwright nor Node tells us whether the request reached a
103
+ * server — the case where a replay is provably safe — or whether only the
104
+ * response was lost, so restricting retries by method would trade a frequent
105
+ * observed failure for a rare silent one.
106
+ *
107
+ * What is not acceptable is the duplicate being invisible, so every replay of
108
+ * a mutating request says so. A duplicated definition or instance is then one
109
+ * grep away instead of an unexplained extra row. Whether to narrow the retry
110
+ * further is a call for the suite's owners; `tests/api/README.md` records the
111
+ * trade-off.
112
+ *
113
+ * Two things are excluded to keep the warning worth reading. `/search` POSTs
114
+ * are queries, so a replay has no effect to duplicate. And a bare 503 is the
115
+ * ingress saying no endpoint was ready, which means the request never reached
116
+ * the application — unlike 502/504, where an upstream was reached and only the
117
+ * answer went missing, and unlike a dropped socket.
118
+ */
119
+ function warnIfMutationReplayed(method, path, attempt, reason) {
120
+ const mutates = (method === 'post' || method === 'patch') && !path.endsWith('/search');
121
+ if (!mutates || reason === 'HTTP 503')
122
+ return;
123
+ console.warn(`Replaying ${method.toUpperCase()} ${path} after ${reason} (attempt ` +
124
+ `${attempt + 2}). If the first attempt was already accepted, its ` +
125
+ `effect now exists twice.`);
126
+ }
127
+ class ApiClient {
128
+ request;
129
+ config;
130
+ constructor(request, config = config_1.apiConfig) {
131
+ this.request = request;
132
+ this.config = config;
133
+ }
134
+ /**
135
+ * Expands a path template against the shared `/v2` prefix.
136
+ *
137
+ * Throws on a missing placeholder rather than emitting a literal `{key}` in
138
+ * the URL: the gateway answers such a path with a plausible 404, so a test
139
+ * asserting 404 would pass without exercising anything.
140
+ */
141
+ buildUrl(path, params, query) {
142
+ const expanded = path.replace(/\{(\w+)\}/g, (_match, key) => {
143
+ const value = params?.[key];
144
+ if (value === undefined || value === null || value === '') {
145
+ throw new Error(`buildUrl: missing path parameter "${key}" for "${path}". ` +
146
+ `Received: ${JSON.stringify(params ?? {})}`);
147
+ }
148
+ return encodeURIComponent(String(value));
149
+ });
150
+ let url = `${this.config.baseUrl}/v2${expanded}`;
151
+ const pairs = Object.entries(query ?? {})
152
+ .filter(([, value]) => value !== undefined)
153
+ .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
154
+ if (pairs.length > 0) {
155
+ url += (url.includes('?') ? '&' : '?') + pairs.join('&');
156
+ }
157
+ return url;
158
+ }
159
+ async headersFor(options, contentType, path) {
160
+ const headers = {
161
+ Accept: acceptFor(path),
162
+ ...options.headers,
163
+ };
164
+ if (contentType)
165
+ headers['Content-Type'] = contentType;
166
+ if (options.authorization === null) {
167
+ return headers;
168
+ }
169
+ headers.Authorization =
170
+ options.authorization ?? (await (0, auth_1.authHeader)(this.request, this.config));
171
+ return headers;
172
+ }
173
+ async send(method, path, options = {}) {
174
+ const tenantMode = options.tenant ?? 'auto';
175
+ const hasBody = options.body !== undefined;
176
+ const isMultipart = options.multipart !== undefined;
177
+ const body = hasBody
178
+ ? (0, tenancy_1.applyTenantToBody)(options.body, tenantMode, this.config)
179
+ : undefined;
180
+ // Tenant-aware endpoints that carry no body take the tenant on the query
181
+ // string instead.
182
+ const query = {
183
+ ...(hasBody || isMultipart
184
+ ? {}
185
+ : (0, tenancy_1.tenantQueryParam)(tenantMode, this.config)),
186
+ ...options.query,
187
+ };
188
+ const url = this.buildUrl(path, options.params, query);
189
+ const headers = await this.headersFor(options, hasBody ? 'application/json' : undefined, path);
190
+ for (let attempt = 0;; attempt++) {
191
+ let response;
192
+ try {
193
+ response = await this.request.fetch(url, {
194
+ method: method.toUpperCase(),
195
+ headers,
196
+ ...(hasBody ? { data: body } : {}),
197
+ ...(isMultipart
198
+ ? {
199
+ multipart: options.multipart,
200
+ }
201
+ : {}),
202
+ // Assertions own the status; never let Playwright throw on 4xx/5xx.
203
+ failOnStatusCode: false,
204
+ timeout: 30000,
205
+ });
206
+ }
207
+ catch (error) {
208
+ if (attempt >= GATEWAY_MAX_ATTEMPTS ||
209
+ !isTransientConnectionError(error)) {
210
+ throw error;
211
+ }
212
+ warnIfMutationReplayed(method, path, attempt, error instanceof Error ? error.message : String(error));
213
+ await sleep(backoffMs(attempt + 1));
214
+ continue;
215
+ }
216
+ if (attempt >= GATEWAY_MAX_ATTEMPTS ||
217
+ !(await isGatewayError(response))) {
218
+ return response;
219
+ }
220
+ warnIfMutationReplayed(method, path, attempt, `HTTP ${response.status()}`);
221
+ await sleep(backoffMs(attempt + 1));
222
+ }
223
+ }
224
+ get(path, options = {}) {
225
+ return this.send('get', path, options);
226
+ }
227
+ post(path, options = {}) {
228
+ return this.send('post', path, options);
229
+ }
230
+ put(path, options = {}) {
231
+ return this.send('put', path, options);
232
+ }
233
+ patch(path, options = {}) {
234
+ return this.send('patch', path, options);
235
+ }
236
+ delete(path, options = {}) {
237
+ return this.send('delete', path, options);
238
+ }
239
+ /** Raw request for the handful of cases needing a non-JSON content type. */
240
+ raw(method, path, options = {}) {
241
+ return this.send(method, path, options);
242
+ }
243
+ }
244
+ exports.ApiClient = ApiClient;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Identity helpers for the specs that need a username to hang something off.
3
+ *
4
+ * Membership and authorization take a username in the path or the body, and
5
+ * the endpoint that *creates* a username is the only part an OIDC cluster
6
+ * withholds. Going through `provisionUsername` rather than posting to
7
+ * `/v2/users` directly keeps those specs running in both modes instead of
8
+ * skipping them wherever the Users API is closed — which is every SM Helm
9
+ * run, and role, group and tenant membership is exactly the surface an
10
+ * upgrade is most likely to break.
11
+ */
12
+ import type { ApiClient } from './http';
13
+ import type { ResourceTracker } from './cleanup';
14
+ /**
15
+ * A username the membership and authorization endpoints will accept.
16
+ *
17
+ * Where the cluster keeps its own user store the user is really created and
18
+ * tracked for cleanup, so the assertions run against a principal that exists.
19
+ *
20
+ * Under OIDC there is nothing to create: users belong to the identity
21
+ * provider. The engine guards membership with
22
+ * `!localUserEnabled || userState.getUser(entityId).isPresent()`, so with
23
+ * local users off the existence check short-circuits and a bare username is
24
+ * accepted — which is also why assigning an *unknown* user is a 204 there and
25
+ * a 404 with a local store (see `absentUserMembership` in `capabilities.ts`).
26
+ * Returning a synthetic name is therefore not a stub: it is the same thing an
27
+ * identity-provider user looks like to these endpoints before first sign-in.
28
+ */
29
+ export declare function provisionUsername(api: ApiClient, resources: ResourceTracker): Promise<string>;
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ /**
3
+ * Identity helpers for the specs that need a username to hang something off.
4
+ *
5
+ * Membership and authorization take a username in the path or the body, and
6
+ * the endpoint that *creates* a username is the only part an OIDC cluster
7
+ * withholds. Going through `provisionUsername` rather than posting to
8
+ * `/v2/users` directly keeps those specs running in both modes instead of
9
+ * skipping them wherever the Users API is closed — which is every SM Helm
10
+ * run, and role, group and tenant membership is exactly the surface an
11
+ * upgrade is most likely to break.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.provisionUsername = void 0;
15
+ const config_1 = require("./config");
16
+ const beans_1 = require("./beans");
17
+ const assertions_1 = require("./assertions");
18
+ const bpmn_1 = require("./bpmn");
19
+ /**
20
+ * A username the membership and authorization endpoints will accept.
21
+ *
22
+ * Where the cluster keeps its own user store the user is really created and
23
+ * tracked for cleanup, so the assertions run against a principal that exists.
24
+ *
25
+ * Under OIDC there is nothing to create: users belong to the identity
26
+ * provider. The engine guards membership with
27
+ * `!localUserEnabled || userState.getUser(entityId).isPresent()`, so with
28
+ * local users off the existence check short-circuits and a bare username is
29
+ * accepted — which is also why assigning an *unknown* user is a 204 there and
30
+ * a 404 with a local store (see `absentUserMembership` in `capabilities.ts`).
31
+ * Returning a synthetic name is therefore not a stub: it is the same thing an
32
+ * identity-provider user looks like to these endpoints before first sign-in.
33
+ */
34
+ async function provisionUsername(api, resources) {
35
+ if (!config_1.apiConfig.usersApiEnabled) {
36
+ return (0, bpmn_1.uniqueId)('oidc-user');
37
+ }
38
+ const user = (0, beans_1.newUser)();
39
+ await (0, assertions_1.assertCreated)(await api.post('/users', { body: user, tenant: 'none' }));
40
+ resources.track('user', user.username);
41
+ return user.username;
42
+ }
43
+ exports.provisionUsername = provisionUsername;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Keys that no record holds, for the "absent resource" assertions.
3
+ *
4
+ * Zeebe encodes the partition in the high bits of a key —
5
+ * `(partitionId << 51) + counter`, see `Protocol.java` in camunda/camunda —
6
+ * so which absent key to use depends on how the endpoint is served:
7
+ *
8
+ * - A **query** (`GET /v2/...`, a `/search`) reads secondary storage and
9
+ * looks the key up as a value. Any number that is not present answers 404,
10
+ * partition or no partition.
11
+ * - A **command** (`PUT .../variables`, a cancellation, a completion) may be
12
+ * routed to the partition its key names before anything looks the record
13
+ * up. A key below `1 << 51` decodes to partition 0, which no cluster has,
14
+ * so the gateway cannot deliver it and answers `503 UNAVAILABLE` —
15
+ * "Expected to handle request, but request could not be delivered" —
16
+ * rather than the 404 the test means to assert.
17
+ *
18
+ * How much of that is observable varies by endpoint, so treat it as a rule to
19
+ * follow rather than a claim about every path. `PUT
20
+ * /element-instances/{key}/variables` is where the 503 was actually seen, and
21
+ * it is what this constant was added for. Other commands currently answer 404
22
+ * for a partition-0 key, presumably because the REST layer resolves the record
23
+ * before dispatching — but that is an implementation detail no test should
24
+ * depend on. Every command assertion therefore uses the routable key, so none
25
+ * of them can start testing the router if that resolution order changes.
26
+ *
27
+ * Import these rather than redeclaring the literal. Eight specs used to carry
28
+ * their own `const ABSENT_KEY`, which shadowed this module and is why the
29
+ * command sites drifted onto the query-side key in the first place.
30
+ */
31
+ /** Partition 0 — fine for queries, unroutable for commands. */
32
+ export declare const ABSENT_KEY = "999999999999";
33
+ /**
34
+ * Partition 1, counter far beyond anything a test cluster allocates: routable,
35
+ * so a command reaches a partition and is rejected as not found.
36
+ */
37
+ export declare const ABSENT_ROUTABLE_KEY: string;
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ /**
3
+ * Keys that no record holds, for the "absent resource" assertions.
4
+ *
5
+ * Zeebe encodes the partition in the high bits of a key —
6
+ * `(partitionId << 51) + counter`, see `Protocol.java` in camunda/camunda —
7
+ * so which absent key to use depends on how the endpoint is served:
8
+ *
9
+ * - A **query** (`GET /v2/...`, a `/search`) reads secondary storage and
10
+ * looks the key up as a value. Any number that is not present answers 404,
11
+ * partition or no partition.
12
+ * - A **command** (`PUT .../variables`, a cancellation, a completion) may be
13
+ * routed to the partition its key names before anything looks the record
14
+ * up. A key below `1 << 51` decodes to partition 0, which no cluster has,
15
+ * so the gateway cannot deliver it and answers `503 UNAVAILABLE` —
16
+ * "Expected to handle request, but request could not be delivered" —
17
+ * rather than the 404 the test means to assert.
18
+ *
19
+ * How much of that is observable varies by endpoint, so treat it as a rule to
20
+ * follow rather than a claim about every path. `PUT
21
+ * /element-instances/{key}/variables` is where the 503 was actually seen, and
22
+ * it is what this constant was added for. Other commands currently answer 404
23
+ * for a partition-0 key, presumably because the REST layer resolves the record
24
+ * before dispatching — but that is an implementation detail no test should
25
+ * depend on. Every command assertion therefore uses the routable key, so none
26
+ * of them can start testing the router if that resolution order changes.
27
+ *
28
+ * Import these rather than redeclaring the literal. Eight specs used to carry
29
+ * their own `const ABSENT_KEY`, which shadowed this module and is why the
30
+ * command sites drifted onto the query-side key in the first place.
31
+ */
32
+ Object.defineProperty(exports, "__esModule", { value: true });
33
+ exports.ABSENT_ROUTABLE_KEY = exports.ABSENT_KEY = void 0;
34
+ /** Partition 0 — fine for queries, unroutable for commands. */
35
+ exports.ABSENT_KEY = '999999999999';
36
+ /**
37
+ * Partition 1, counter far beyond anything a test cluster allocates: routable,
38
+ * so a command reaches a partition and is rejected as not found.
39
+ */
40
+ exports.ABSENT_ROUTABLE_KEY = String(2251799813685248n + 999999999n);
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Every Orchestration Cluster REST v2 path this suite exercises.
3
+ *
4
+ * `buildUrl` accepts only these literals, so an endpoint that is renamed or
5
+ * mistyped fails to compile. The legacy shell suite concatenated path strings,
6
+ * where a typo produced a plausible 404 that an assertion expecting 404 then
7
+ * reported as a pass.
8
+ *
9
+ * The union is only as good as the literals in it: the first cluster run
10
+ * rejected paths that had been written from assumption rather than read off
11
+ * the controllers, and the compiler had happily accepted all of them. Every
12
+ * entry here is checked against the v2 OpenAPI spec on stable/8.8, stable/8.9
13
+ * and stable/8.10 in camunda/camunda
14
+ * (`zeebe/gateway-protocol/src/main/proto/v2/`), and a new one should be added
15
+ * the same way — the type gives no protection against a path that never
16
+ * existed.
17
+ *
18
+ * Eleven entries failed that check. Four announced themselves as failures;
19
+ * the rest did not, because the test using each one expected a status list
20
+ * that the 404 from an unrouted path already satisfied. Checking the whole
21
+ * union against the spec, rather than only the paths that went red, is what
22
+ * found them.
23
+ */
24
+ export declare const API_PATHS: readonly ["/authentication/me", "/authentication/me/authorizations/search", "/topology", "/license", "/status", "/system/usage-metrics", "/clock", "/clock/reset", "/setup/user", "/process-definitions/search", "/process-definitions/{processDefinitionKey}", "/process-definitions/{processDefinitionKey}/xml", "/process-definitions/{processDefinitionKey}/form", "/process-definitions/{processDefinitionKey}/statistics/element-instances", "/process-instances", "/process-instances/search", "/process-instances/cancellation", "/process-instances/migration", "/process-instances/modification", "/process-instances/incident-resolution", "/process-instances/{processInstanceKey}", "/process-instances/{processInstanceKey}/cancellation", "/process-instances/{processInstanceKey}/migration", "/process-instances/{processInstanceKey}/modification", "/process-instances/{processInstanceKey}/sequence-flows", "/process-instances/{processInstanceKey}/call-hierarchy", "/process-instances/{processInstanceKey}/incidents/search", "/process-instances/{processInstanceKey}/statistics/element-instances", "/deployments", "/resources/{resourceKey}", "/resources/{resourceKey}/deletion", "/decision-definitions/search", "/decision-definitions/{decisionDefinitionKey}", "/decision-definitions/{decisionDefinitionKey}/xml", "/decision-definitions/evaluation", "/decision-instances/search", "/decision-instances/{decisionEvaluationInstanceKey}", "/decision-requirements/search", "/decision-requirements/{decisionRequirementsKey}", "/decision-requirements/{decisionRequirementsKey}/xml", "/user-tasks/search", "/user-tasks/{userTaskKey}", "/user-tasks/{userTaskKey}/form", "/user-tasks/{userTaskKey}/assignment", "/user-tasks/{userTaskKey}/assignee", "/user-tasks/{userTaskKey}/completion", "/user-tasks/{userTaskKey}/variables/search", "/jobs/search", "/jobs/activation", "/jobs/{jobKey}", "/jobs/{jobKey}/completion", "/jobs/{jobKey}/failure", "/jobs/{jobKey}/error", "/messages/publication", "/messages/correlation", "/message-subscriptions/search", "/correlated-message-subscriptions/search", "/signals/broadcast", "/element-instances/search", "/element-instances/{elementInstanceKey}", "/element-instances/{elementInstanceKey}/variables", "/element-instances/ad-hoc-activities/{adHocSubProcessInstanceKey}/activation", "/variables/search", "/variables/{variableKey}", "/incidents/search", "/incidents/{incidentKey}", "/incidents/{incidentKey}/resolution", "/forms/{formKey}", "/documents", "/documents/{documentId}", "/batch-operations/search", "/batch-operations/{batchOperationKey}", "/batch-operations/{batchOperationKey}/suspension", "/batch-operations/{batchOperationKey}/resumption", "/batch-operations/{batchOperationKey}/cancellation", "/batch-operation-items/search", "/authorizations", "/authorizations/search", "/authorizations/{authorizationKey}", "/roles", "/roles/search", "/roles/{roleId}", "/roles/{roleId}/users/search", "/roles/{roleId}/users/{username}", "/roles/{roleId}/groups/search", "/roles/{roleId}/groups/{groupId}", "/roles/{roleId}/clients/search", "/roles/{roleId}/clients/{clientId}", "/roles/{roleId}/mapping-rules/search", "/roles/{roleId}/mapping-rules/{mappingRuleId}", "/groups", "/groups/search", "/groups/{groupId}", "/groups/{groupId}/users/search", "/groups/{groupId}/users/{username}", "/groups/{groupId}/roles/search", "/groups/{groupId}/clients/search", "/groups/{groupId}/clients/{clientId}", "/groups/{groupId}/mapping-rules/search", "/groups/{groupId}/mapping-rules/{mappingRuleId}", "/users", "/users/search", "/users/{username}", "/mapping-rules", "/mapping-rules/search", "/mapping-rules/{mappingRuleId}", "/tenants", "/tenants/search", "/tenants/{tenantId}", "/tenants/{tenantId}/users/search", "/tenants/{tenantId}/users/{username}", "/tenants/{tenantId}/groups/search", "/tenants/{tenantId}/groups/{groupId}", "/tenants/{tenantId}/roles/search", "/tenants/{tenantId}/roles/{roleId}", "/tenants/{tenantId}/clients/search", "/tenants/{tenantId}/clients/{clientId}", "/tenants/{tenantId}/mapping-rules/search", "/tenants/{tenantId}/mapping-rules/{mappingRuleId}"];
25
+ export type ApiPath = (typeof API_PATHS)[number];
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.API_PATHS = void 0;
4
+ /**
5
+ * Every Orchestration Cluster REST v2 path this suite exercises.
6
+ *
7
+ * `buildUrl` accepts only these literals, so an endpoint that is renamed or
8
+ * mistyped fails to compile. The legacy shell suite concatenated path strings,
9
+ * where a typo produced a plausible 404 that an assertion expecting 404 then
10
+ * reported as a pass.
11
+ *
12
+ * The union is only as good as the literals in it: the first cluster run
13
+ * rejected paths that had been written from assumption rather than read off
14
+ * the controllers, and the compiler had happily accepted all of them. Every
15
+ * entry here is checked against the v2 OpenAPI spec on stable/8.8, stable/8.9
16
+ * and stable/8.10 in camunda/camunda
17
+ * (`zeebe/gateway-protocol/src/main/proto/v2/`), and a new one should be added
18
+ * the same way — the type gives no protection against a path that never
19
+ * existed.
20
+ *
21
+ * Eleven entries failed that check. Four announced themselves as failures;
22
+ * the rest did not, because the test using each one expected a status list
23
+ * that the 404 from an unrouted path already satisfied. Checking the whole
24
+ * union against the spec, rather than only the paths that went red, is what
25
+ * found them.
26
+ */
27
+ exports.API_PATHS = [
28
+ // Authentication, cluster and system
29
+ '/authentication/me',
30
+ '/authentication/me/authorizations/search',
31
+ '/topology',
32
+ '/license',
33
+ '/status',
34
+ '/system/usage-metrics',
35
+ '/clock',
36
+ '/clock/reset',
37
+ '/setup/user',
38
+ // Process definitions
39
+ '/process-definitions/search',
40
+ '/process-definitions/{processDefinitionKey}',
41
+ '/process-definitions/{processDefinitionKey}/xml',
42
+ '/process-definitions/{processDefinitionKey}/form',
43
+ '/process-definitions/{processDefinitionKey}/statistics/element-instances',
44
+ // Process instances
45
+ '/process-instances',
46
+ '/process-instances/search',
47
+ '/process-instances/cancellation',
48
+ '/process-instances/migration',
49
+ '/process-instances/modification',
50
+ '/process-instances/incident-resolution',
51
+ '/process-instances/{processInstanceKey}',
52
+ '/process-instances/{processInstanceKey}/cancellation',
53
+ '/process-instances/{processInstanceKey}/migration',
54
+ '/process-instances/{processInstanceKey}/modification',
55
+ '/process-instances/{processInstanceKey}/sequence-flows',
56
+ '/process-instances/{processInstanceKey}/call-hierarchy',
57
+ '/process-instances/{processInstanceKey}/incidents/search',
58
+ '/process-instances/{processInstanceKey}/statistics/element-instances',
59
+ // Deployments and resources
60
+ '/deployments',
61
+ '/resources/{resourceKey}',
62
+ '/resources/{resourceKey}/deletion',
63
+ // Decisions
64
+ '/decision-definitions/search',
65
+ '/decision-definitions/{decisionDefinitionKey}',
66
+ '/decision-definitions/{decisionDefinitionKey}/xml',
67
+ // Evaluation is a collection-level POST carrying the decision id or key in
68
+ // the body — there is no per-key `/evaluate` sub-resource.
69
+ '/decision-definitions/evaluation',
70
+ '/decision-instances/search',
71
+ // Addressed by the compound `<decisionEvaluationKey>-<index>`.
72
+ '/decision-instances/{decisionEvaluationInstanceKey}',
73
+ '/decision-requirements/search',
74
+ '/decision-requirements/{decisionRequirementsKey}',
75
+ '/decision-requirements/{decisionRequirementsKey}/xml',
76
+ // User tasks
77
+ '/user-tasks/search',
78
+ '/user-tasks/{userTaskKey}',
79
+ '/user-tasks/{userTaskKey}/form',
80
+ '/user-tasks/{userTaskKey}/assignment',
81
+ '/user-tasks/{userTaskKey}/assignee',
82
+ '/user-tasks/{userTaskKey}/completion',
83
+ '/user-tasks/{userTaskKey}/variables/search',
84
+ // Jobs
85
+ '/jobs/search',
86
+ '/jobs/activation',
87
+ '/jobs/{jobKey}',
88
+ '/jobs/{jobKey}/completion',
89
+ '/jobs/{jobKey}/failure',
90
+ '/jobs/{jobKey}/error',
91
+ // Messages and subscriptions
92
+ '/messages/publication',
93
+ '/messages/correlation',
94
+ '/message-subscriptions/search',
95
+ '/correlated-message-subscriptions/search',
96
+ '/signals/broadcast',
97
+ // Element instances
98
+ '/element-instances/search',
99
+ '/element-instances/{elementInstanceKey}',
100
+ '/element-instances/{elementInstanceKey}/variables',
101
+ '/element-instances/ad-hoc-activities/{adHocSubProcessInstanceKey}/activation',
102
+ // Variables, incidents, forms, documents
103
+ '/variables/search',
104
+ '/variables/{variableKey}',
105
+ '/incidents/search',
106
+ '/incidents/{incidentKey}',
107
+ '/incidents/{incidentKey}/resolution',
108
+ '/forms/{formKey}',
109
+ '/documents',
110
+ '/documents/{documentId}',
111
+ // Batch operations
112
+ '/batch-operations/search',
113
+ '/batch-operations/{batchOperationKey}',
114
+ '/batch-operations/{batchOperationKey}/suspension',
115
+ '/batch-operations/{batchOperationKey}/resumption',
116
+ '/batch-operations/{batchOperationKey}/cancellation',
117
+ '/batch-operation-items/search',
118
+ // Authorizations
119
+ '/authorizations',
120
+ '/authorizations/search',
121
+ '/authorizations/{authorizationKey}',
122
+ // Roles
123
+ '/roles',
124
+ '/roles/search',
125
+ '/roles/{roleId}',
126
+ '/roles/{roleId}/users/search',
127
+ '/roles/{roleId}/users/{username}',
128
+ '/roles/{roleId}/groups/search',
129
+ '/roles/{roleId}/groups/{groupId}',
130
+ '/roles/{roleId}/clients/search',
131
+ '/roles/{roleId}/clients/{clientId}',
132
+ '/roles/{roleId}/mapping-rules/search',
133
+ '/roles/{roleId}/mapping-rules/{mappingRuleId}',
134
+ // Groups
135
+ '/groups',
136
+ '/groups/search',
137
+ '/groups/{groupId}',
138
+ '/groups/{groupId}/users/search',
139
+ '/groups/{groupId}/users/{username}',
140
+ '/groups/{groupId}/roles/search',
141
+ '/groups/{groupId}/clients/search',
142
+ '/groups/{groupId}/clients/{clientId}',
143
+ '/groups/{groupId}/mapping-rules/search',
144
+ '/groups/{groupId}/mapping-rules/{mappingRuleId}',
145
+ // Users and clients
146
+ '/users',
147
+ '/users/search',
148
+ '/users/{username}',
149
+ // Mapping rules
150
+ '/mapping-rules',
151
+ '/mapping-rules/search',
152
+ '/mapping-rules/{mappingRuleId}',
153
+ // Tenants
154
+ '/tenants',
155
+ '/tenants/search',
156
+ '/tenants/{tenantId}',
157
+ '/tenants/{tenantId}/users/search',
158
+ '/tenants/{tenantId}/users/{username}',
159
+ '/tenants/{tenantId}/groups/search',
160
+ '/tenants/{tenantId}/groups/{groupId}',
161
+ '/tenants/{tenantId}/roles/search',
162
+ '/tenants/{tenantId}/roles/{roleId}',
163
+ '/tenants/{tenantId}/clients/search',
164
+ '/tenants/{tenantId}/clients/{clientId}',
165
+ '/tenants/{tenantId}/mapping-rules/search',
166
+ '/tenants/{tenantId}/mapping-rules/{mappingRuleId}',
167
+ ];
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Multi-tenancy handling for REST v2 requests.
3
+ *
4
+ * On a multi-tenant cluster every tenant-aware endpoint needs the tenant
5
+ * supplied, but the endpoints disagree on where: search endpoints take it
6
+ * inside `filter`, mutations take it at the body root, job activation takes an
7
+ * array under `tenantIds`, and GETs take it as a query parameter. When
8
+ * multi-tenancy is off the cluster rejects the field entirely, so nothing is
9
+ * injected.
10
+ */
11
+ import { type ApiConfig } from './config';
12
+ export type TenantMode =
13
+ /** Pick `filter` or root by body shape — the default for most endpoints. */
14
+ 'auto'
15
+ /** Always inside `filter` (search endpoints). */
16
+ | 'filter'
17
+ /** Always at the body root (creation and mutation endpoints). */
18
+ | 'root'
19
+ /** `tenantIds: [tenant]` — job activation only. */
20
+ | 'tenantIds'
21
+ /** Endpoint is not tenant-aware (clock, identity, setup). */
22
+ | 'none';
23
+ /**
24
+ * Applies the tenant to a request body. `auto` mirrors what the endpoints
25
+ * expect: a body that already filters gets the tenant added to that filter, a
26
+ * body that paginates or sorts without filtering gains one, and anything else
27
+ * is treated as a mutation and takes the tenant at the root.
28
+ */
29
+ export declare function applyTenantToBody(body: unknown, mode: TenantMode, config?: ApiConfig): unknown;
30
+ /** The `tenantId` query parameter for tenant-aware GET and DELETE requests. */
31
+ export declare function tenantQueryParam(mode: TenantMode, config?: ApiConfig): Record<string, string>;