@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,217 @@
1
+ "use strict";
2
+ /**
3
+ * Minimal BPMN/DMN builders for API-level tests.
4
+ *
5
+ * The legacy shell suite pasted 18 near-identical BPMN documents inline as
6
+ * heredocs, several of them interpolating a process id. Building the XML gives
7
+ * every test a unique process id without a new file per shape, which is what
8
+ * the suite's isolation rules require — reused ids collide across parallel
9
+ * workers and produce cross-test flakiness.
10
+ *
11
+ * Only the shapes the API tests need are modelled. Anything requiring real
12
+ * diagram semantics belongs in `resources/` as a checked-in file.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.simpleForm = exports.simpleDecision = exports.startFormProcess = exports.emptyProcess = exports.linearProcess = exports.element = exports.uniqueId = void 0;
16
+ const node_crypto_1 = require("node:crypto");
17
+ /** A process/decision id that cannot collide with another worker's. */
18
+ function uniqueId(prefix) {
19
+ return `${prefix}-${(0, node_crypto_1.randomUUID)().slice(0, 8)}`;
20
+ }
21
+ exports.uniqueId = uniqueId;
22
+ const NS = [
23
+ 'xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL"',
24
+ 'xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"',
25
+ 'xmlns:dc="http://www.omg.org/spec/DD/20100524/DC"',
26
+ 'xmlns:di="http://www.omg.org/spec/DD/20100524/DI"',
27
+ 'xmlns:zeebe="http://camunda.org/schema/zeebe/1.0"',
28
+ 'xmlns:modeler="http://camunda.org/schema/modeler/1.0"',
29
+ ].join(' ');
30
+ function escapeXml(value) {
31
+ return value
32
+ .replace(/&/g, '&')
33
+ .replace(/</g, '&lt;')
34
+ .replace(/>/g, '&gt;')
35
+ .replace(/"/g, '&quot;');
36
+ }
37
+ exports.element = {
38
+ serviceTask(jobType, id = 'task') {
39
+ return {
40
+ id,
41
+ xml: `<bpmn:serviceTask id="${id}" name="Service Task">
42
+ <bpmn:extensionElements>
43
+ <zeebe:taskDefinition type="${escapeXml(jobType)}" />
44
+ </bpmn:extensionElements>
45
+ <bpmn:incoming>flow_in</bpmn:incoming>
46
+ <bpmn:outgoing>flow_out</bpmn:outgoing>
47
+ </bpmn:serviceTask>`,
48
+ };
49
+ },
50
+ userTask(id = 'task', formId) {
51
+ const form = formId
52
+ ? `<zeebe:formDefinition formId="${escapeXml(formId)}" />`
53
+ : '';
54
+ return {
55
+ id,
56
+ xml: `<bpmn:userTask id="${id}" name="User Task">
57
+ <bpmn:extensionElements>
58
+ <zeebe:userTask />
59
+ ${form}
60
+ </bpmn:extensionElements>
61
+ <bpmn:incoming>flow_in</bpmn:incoming>
62
+ <bpmn:outgoing>flow_out</bpmn:outgoing>
63
+ </bpmn:userTask>`,
64
+ };
65
+ },
66
+ messageCatch(messageName, correlationKey = '=msgCorrelationKey', id = 'task') {
67
+ return {
68
+ id,
69
+ xml: `<bpmn:intermediateCatchEvent id="${id}" name="Wait for message">
70
+ <bpmn:incoming>flow_in</bpmn:incoming>
71
+ <bpmn:outgoing>flow_out</bpmn:outgoing>
72
+ <bpmn:messageEventDefinition messageRef="message_def" />
73
+ </bpmn:intermediateCatchEvent>`,
74
+ definitionsFragment: `<bpmn:message id="message_def" name="${escapeXml(messageName)}">
75
+ <bpmn:extensionElements>
76
+ <zeebe:subscription correlationKey="${escapeXml(correlationKey)}" />
77
+ </bpmn:extensionElements>
78
+ </bpmn:message>`,
79
+ };
80
+ },
81
+ businessRuleTask(decisionId, id = 'task') {
82
+ return {
83
+ id,
84
+ xml: `<bpmn:businessRuleTask id="${id}" name="Business Rule Task">
85
+ <bpmn:extensionElements>
86
+ <zeebe:calledDecision decisionId="${escapeXml(decisionId)}" resultVariable="decisionResult" />
87
+ </bpmn:extensionElements>
88
+ <bpmn:incoming>flow_in</bpmn:incoming>
89
+ <bpmn:outgoing>flow_out</bpmn:outgoing>
90
+ </bpmn:businessRuleTask>`,
91
+ };
92
+ },
93
+ callActivity(calledProcessId, id = 'task') {
94
+ return {
95
+ id,
96
+ xml: `<bpmn:callActivity id="${id}" name="Call Activity">
97
+ <bpmn:extensionElements>
98
+ <zeebe:calledElement processId="${escapeXml(calledProcessId)}" propagateAllChildVariables="false" />
99
+ </bpmn:extensionElements>
100
+ <bpmn:incoming>flow_in</bpmn:incoming>
101
+ <bpmn:outgoing>flow_out</bpmn:outgoing>
102
+ </bpmn:callActivity>`,
103
+ };
104
+ },
105
+ };
106
+ /**
107
+ * A single-path process: start event -> the given element -> end event.
108
+ * No DI is emitted; the engine does not require it and no test reads it.
109
+ */
110
+ function linearProcess(processId, body, options = {}) {
111
+ return `<?xml version="1.0" encoding="UTF-8"?>
112
+ <bpmn:definitions ${NS} id="definitions_${escapeXml(processId)}" targetNamespace="http://camunda.org/schema/1.0/bpmn" modeler:executionPlatform="Camunda Cloud" modeler:executionPlatformVersion="8.8.0">
113
+ <bpmn:process id="${escapeXml(processId)}" name="${escapeXml(options.name ?? processId)}" isExecutable="true">
114
+ <bpmn:startEvent id="start">
115
+ <bpmn:outgoing>flow_in</bpmn:outgoing>
116
+ </bpmn:startEvent>
117
+ <bpmn:sequenceFlow id="flow_in" sourceRef="start" targetRef="${body.id}" />
118
+ ${body.xml}
119
+ <bpmn:sequenceFlow id="flow_out" sourceRef="${body.id}" targetRef="end" />
120
+ <bpmn:endEvent id="end">
121
+ <bpmn:incoming>flow_out</bpmn:incoming>
122
+ </bpmn:endEvent>
123
+ </bpmn:process>
124
+ ${body.definitionsFragment ?? ''}
125
+ </bpmn:definitions>`;
126
+ }
127
+ exports.linearProcess = linearProcess;
128
+ /** A process whose only content is a start and end event. */
129
+ function emptyProcess(processId) {
130
+ return `<?xml version="1.0" encoding="UTF-8"?>
131
+ <bpmn:definitions ${NS} id="definitions_${escapeXml(processId)}" targetNamespace="http://camunda.org/schema/1.0/bpmn">
132
+ <bpmn:process id="${escapeXml(processId)}" name="${escapeXml(processId)}" isExecutable="true">
133
+ <bpmn:startEvent id="start">
134
+ <bpmn:outgoing>flow_out</bpmn:outgoing>
135
+ </bpmn:startEvent>
136
+ <bpmn:sequenceFlow id="flow_out" sourceRef="start" targetRef="end" />
137
+ <bpmn:endEvent id="end">
138
+ <bpmn:incoming>flow_out</bpmn:incoming>
139
+ </bpmn:endEvent>
140
+ </bpmn:process>
141
+ </bpmn:definitions>`;
142
+ }
143
+ exports.emptyProcess = emptyProcess;
144
+ /**
145
+ * A process whose start event links a deployed form, so that
146
+ * `GET /v2/process-definitions/{key}/form` has something to return. The form
147
+ * must already be deployed (or ride in the same deployment), otherwise the
148
+ * broker rejects the reference.
149
+ */
150
+ function startFormProcess(processId, formId) {
151
+ return `<?xml version="1.0" encoding="UTF-8"?>
152
+ <bpmn:definitions ${NS} id="definitions_${escapeXml(processId)}" targetNamespace="http://camunda.org/schema/1.0/bpmn">
153
+ <bpmn:process id="${escapeXml(processId)}" name="${escapeXml(processId)}" isExecutable="true">
154
+ <bpmn:startEvent id="start">
155
+ <bpmn:extensionElements>
156
+ <zeebe:formDefinition formId="${escapeXml(formId)}" />
157
+ </bpmn:extensionElements>
158
+ <bpmn:outgoing>flow_out</bpmn:outgoing>
159
+ </bpmn:startEvent>
160
+ <bpmn:sequenceFlow id="flow_out" sourceRef="start" targetRef="end" />
161
+ <bpmn:endEvent id="end">
162
+ <bpmn:incoming>flow_out</bpmn:incoming>
163
+ </bpmn:endEvent>
164
+ </bpmn:process>
165
+ </bpmn:definitions>`;
166
+ }
167
+ exports.startFormProcess = startFormProcess;
168
+ /** A one-rule DMN decision that echoes its input, for evaluation tests. */
169
+ function simpleDecision(decisionId, definitionsId = `definitions_${decisionId}`) {
170
+ return `<?xml version="1.0" encoding="UTF-8"?>
171
+ <definitions xmlns="https://www.omg.org/spec/DMN/20191111/MODEL/" xmlns:camunda="http://camunda.org/schema/1.0/dmn" id="${escapeXml(definitionsId)}" name="${escapeXml(decisionId)}" namespace="http://camunda.org/schema/1.0/dmn">
172
+ <decision id="${escapeXml(decisionId)}" name="${escapeXml(decisionId)}">
173
+ <decisionTable id="decisionTable_${escapeXml(decisionId)}" hitPolicy="FIRST">
174
+ <input id="input_1" label="Input">
175
+ <inputExpression id="inputExpression_1" typeRef="string">
176
+ <text>inputVariable</text>
177
+ </inputExpression>
178
+ </input>
179
+ <output id="output_1" label="Output" name="result" typeRef="string" />
180
+ <rule id="rule_match">
181
+ <inputEntry id="inputEntry_match"><text>"match"</text></inputEntry>
182
+ <outputEntry id="outputEntry_match"><text>"matched"</text></outputEntry>
183
+ </rule>
184
+ <rule id="rule_default">
185
+ <inputEntry id="inputEntry_default"><text></text></inputEntry>
186
+ <outputEntry id="outputEntry_default"><text>"default"</text></outputEntry>
187
+ </rule>
188
+ </decisionTable>
189
+ </decision>
190
+ </definitions>`;
191
+ }
192
+ exports.simpleDecision = simpleDecision;
193
+ /** A minimal Camunda form with two text fields, for form submission tests. */
194
+ function simpleForm(formId) {
195
+ return JSON.stringify({
196
+ components: [
197
+ {
198
+ label: 'Field one',
199
+ type: 'textfield',
200
+ id: 'field_one',
201
+ key: 'fieldOne',
202
+ },
203
+ {
204
+ label: 'Field two',
205
+ type: 'textfield',
206
+ id: 'field_two',
207
+ key: 'fieldTwo',
208
+ },
209
+ ],
210
+ type: 'default',
211
+ id: formId,
212
+ schemaVersion: 16,
213
+ executionPlatform: 'Camunda Cloud',
214
+ executionPlatformVersion: '8.8.0',
215
+ });
216
+ }
217
+ exports.simpleForm = simpleForm;
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Cluster capabilities that decide whether a spec *can* run at all.
3
+ *
4
+ * A capability gate is not a way to quieten a failing test. It marks the one
5
+ * case the suite cannot otherwise express: the endpoint under test is absent
6
+ * by configuration, so there is nothing to assert and a red result would say
7
+ * nothing about the cluster's health.
8
+ *
9
+ * Two rules keep that distinction honest:
10
+ *
11
+ * - The gate reads *configuration*, never a response. Deciding from a live
12
+ * 403 would also absorb a genuine permission regression, which is the
13
+ * failure this suite exists to catch.
14
+ * - Every gate is paired with a test that asserts the endpoint really is
15
+ * absent for that configuration (see `system-api-tests.spec.ts`). Without
16
+ * it, a gate that silently became wrong would remove coverage and nothing
17
+ * would report it.
18
+ *
19
+ * Anything else — a token that could not be minted, a fixture that failed, a
20
+ * role that was not granted — is a failure, not a capability, and must stay
21
+ * red.
22
+ */
23
+ import type { ApiClient } from './http';
24
+ /**
25
+ * Reason text for the `/v2/users/**` gate, or `false` when the tree is served.
26
+ *
27
+ * Playwright prints this next to the skipped test, so a reader of the report
28
+ * sees why the tests did not run without opening the source.
29
+ */
30
+ export declare const usersApiUnavailable: string | false;
31
+ /**
32
+ * Reason text for the unauthenticated-rejection gate, or `false` where a
33
+ * missing `Authorization` header really is refused.
34
+ *
35
+ * Every `assertUnauthorized` test sends a request with no credentials and
36
+ * expects 401. That asserts a property of the *deployment*, not of the
37
+ * endpoint: c8Run answers its whole `/v2` tree without credentials — which
38
+ * `tests/c8Run-8.10/api-tests-v2.spec.ts` depends on, calling
39
+ * `/v2/process-definitions/search` unauthenticated and asserting 200 — so
40
+ * there the same requests return the normal response and the tests report
41
+ * "expected 401, got 200" against a cluster that never authenticated.
42
+ *
43
+ * It also takes `/v2/authentication/me` with it: an open cluster has no
44
+ * authenticated principal to describe, and it answers 401 even when the
45
+ * suite does send credentials.
46
+ *
47
+ * This is the third capability of the same shape as the two around it, and
48
+ * the distinction between them is worth keeping straight — whether a caller
49
+ * must be a principal at all (here), what a known principal may do
50
+ * (`authorizationsEnabled`), and whether the cluster keeps its own user
51
+ * registry (`usersApiEnabled`). A deployment can differ on each
52
+ * independently, and c8Run differs on all three.
53
+ */
54
+ export declare const unauthenticatedAccessAllowed: string | false;
55
+ export declare const enforcementPrincipalUnavailable: string | false;
56
+ /**
57
+ * What `PUT /{roles,groups,tenants}/{id}/users/{username}` does when the
58
+ * username belongs to nobody.
59
+ *
60
+ * The engine guards membership with
61
+ * `case USER -> !localUserEnabled || userState.getUser(entityId).isPresent()`
62
+ * (`RoleAddEntityProcessor`, `GroupAddEntityProcessor`,
63
+ * `TenantAddEntityProcessor` in camunda/camunda). With local users disabled
64
+ * the left side short-circuits, so the membership is written against the bare
65
+ * username and the command is accepted.
66
+ *
67
+ * That is deliberate — in OIDC mode the cluster has no registry to check a
68
+ * username against, and refusing would make it impossible to grant a role to
69
+ * an identity-provider user before their first sign-in. So the two modes
70
+ * genuinely differ, and the assertion has to follow the mode rather than pick
71
+ * one and call the other a bug.
72
+ */
73
+ export declare const absentUserMembership: {
74
+ status: number;
75
+ reason: string;
76
+ };
77
+ /**
78
+ * Whether the cluster enforces authorization, straight from the cluster.
79
+ *
80
+ * `POST /v2/authentication/me/authorizations/search` carries
81
+ * `authorizationsEnabled` as a required field, so this is the cluster's own
82
+ * answer rather than an inference from the chart values. Added in 8.10.
83
+ *
84
+ * `undefined` means only the two cases above. Every other status throws
85
+ * instead: this reading is the safety net under the enforcement gap recorded
86
+ * in `tests/api/README.md`, and treating a 403, a 500 or a 503 as "unknown"
87
+ * would let the very regression it guards remove it. A 500 from this endpoint
88
+ * is a finding, not a reason to stop looking.
89
+ */
90
+ export declare function readAuthorizationsEnabled(api: ApiClient): Promise<boolean | undefined>;
@@ -0,0 +1,198 @@
1
+ "use strict";
2
+ /**
3
+ * Cluster capabilities that decide whether a spec *can* run at all.
4
+ *
5
+ * A capability gate is not a way to quieten a failing test. It marks the one
6
+ * case the suite cannot otherwise express: the endpoint under test is absent
7
+ * by configuration, so there is nothing to assert and a red result would say
8
+ * nothing about the cluster's health.
9
+ *
10
+ * Two rules keep that distinction honest:
11
+ *
12
+ * - The gate reads *configuration*, never a response. Deciding from a live
13
+ * 403 would also absorb a genuine permission regression, which is the
14
+ * failure this suite exists to catch.
15
+ * - Every gate is paired with a test that asserts the endpoint really is
16
+ * absent for that configuration (see `system-api-tests.spec.ts`). Without
17
+ * it, a gate that silently became wrong would remove coverage and nothing
18
+ * would report it.
19
+ *
20
+ * Anything else — a token that could not be minted, a fixture that failed, a
21
+ * role that was not granted — is a failure, not a capability, and must stay
22
+ * red.
23
+ */
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.readAuthorizationsEnabled = exports.absentUserMembership = exports.enforcementPrincipalUnavailable = exports.unauthenticatedAccessAllowed = exports.usersApiUnavailable = void 0;
26
+ const config_1 = require("./config");
27
+ /**
28
+ * Reason text for the `/v2/users/**` gate, or `false` when the tree is served.
29
+ *
30
+ * Playwright prints this next to the skipped test, so a reader of the report
31
+ * sees why the tests did not run without opening the source.
32
+ */
33
+ exports.usersApiUnavailable = config_1.apiConfig.usersApiEnabled
34
+ ? false
35
+ : 'the cluster is in OIDC mode, where the identity provider owns the user ' +
36
+ 'registry and /v2/users/** answers 403 — there is no local user to create';
37
+ /**
38
+ * Reason text for the unauthenticated-rejection gate, or `false` where a
39
+ * missing `Authorization` header really is refused.
40
+ *
41
+ * Every `assertUnauthorized` test sends a request with no credentials and
42
+ * expects 401. That asserts a property of the *deployment*, not of the
43
+ * endpoint: c8Run answers its whole `/v2` tree without credentials — which
44
+ * `tests/c8Run-8.10/api-tests-v2.spec.ts` depends on, calling
45
+ * `/v2/process-definitions/search` unauthenticated and asserting 200 — so
46
+ * there the same requests return the normal response and the tests report
47
+ * "expected 401, got 200" against a cluster that never authenticated.
48
+ *
49
+ * It also takes `/v2/authentication/me` with it: an open cluster has no
50
+ * authenticated principal to describe, and it answers 401 even when the
51
+ * suite does send credentials.
52
+ *
53
+ * This is the third capability of the same shape as the two around it, and
54
+ * the distinction between them is worth keeping straight — whether a caller
55
+ * must be a principal at all (here), what a known principal may do
56
+ * (`authorizationsEnabled`), and whether the cluster keeps its own user
57
+ * registry (`usersApiEnabled`). A deployment can differ on each
58
+ * independently, and c8Run differs on all three.
59
+ */
60
+ exports.unauthenticatedAccessAllowed = config_1.apiConfig.authenticationRequired
61
+ ? false
62
+ : 'this cluster serves /v2 without authentication (AUTHENTICATION_REQUIRED ' +
63
+ 'is false), so a request carrying no credentials is answered normally ' +
64
+ 'and the 401 this test asserts cannot occur — see tests/api/README.md';
65
+ /**
66
+ * Whether the enforcement tests can obtain a second, unprivileged principal.
67
+ *
68
+ * Enforcement is only observable against a principal that is *not* the
69
+ * suite's admin. Two ways exist and neither is universal:
70
+ *
71
+ * - Where local users are served, the spec creates one. Simple and
72
+ * self-contained, but impossible in OIDC mode.
73
+ * - In OIDC mode the cluster's principals are identity-provider clients,
74
+ * which this suite cannot provision. `SECONDARY_CLIENT_ID` and
75
+ * `SECONDARY_CLIENT_SECRET` name one that already exists.
76
+ *
77
+ * With neither available the tests cannot run, and the reason below says
78
+ * exactly what to configure. This is a real coverage gap on OIDC clusters
79
+ * until a second client is wired into the SM workflows — not a defect in the
80
+ * tests, but not something to leave unstated either.
81
+ *
82
+ * A second principal is necessary but NOT sufficient. The QA clusters do not
83
+ * enforce authorization at all: `.github/actions/template-helm-chart-values`
84
+ * initialises the admin role from three mappings only — `demo`
85
+ * (preferred_username), `venom` and `connectors` (client_id) — and the client
86
+ * this suite authenticates as, `test`, is not among them, yet it creates
87
+ * roles, tenants and authorizations without being refused. Nothing sets
88
+ * `CAMUNDA_SECURITY_AUTHORIZATIONS_ENABLED`.
89
+ *
90
+ * So wiring a client alone would not restore this coverage: the tests would
91
+ * fail with "expected 403, got 200", which is exactly the regression they
92
+ * exist to detect. Enabling enforcement on those clusters is a deliberate
93
+ * environment change with a blast radius beyond this suite, so it is not done
94
+ * here — `readAuthorizationsEnabled` reports the cluster's actual state
95
+ * instead, and `tests/api/README.md` records what closing the gap requires.
96
+ */
97
+ function enforcementUnavailableReason() {
98
+ // Necessary condition one: a principal that is not the suite's admin.
99
+ const havePrincipal = config_1.apiConfig.usersApiEnabled ||
100
+ (config_1.apiConfig.secondaryClient.clientId !== '' &&
101
+ config_1.apiConfig.secondaryClient.clientSecret !== '');
102
+ if (!havePrincipal) {
103
+ return ('this cluster serves no local users, so an unprivileged principal must ' +
104
+ 'be an existing identity-provider client — set SECONDARY_CLIENT_ID and ' +
105
+ 'SECONDARY_CLIENT_SECRET to one that holds no roles');
106
+ }
107
+ // Necessary condition two, and the one this file used to state in prose
108
+ // without encoding: the cluster has to actually check permissions. Where it
109
+ // does not, every principal is permitted and these tests fail with
110
+ // "expected 403, got 200" — the regression they exist to catch, reported
111
+ // against a cluster that never had enforcement rather than against a
112
+ // product that lost it. c8Run is exactly that case: it serves its whole v2
113
+ // API unauthenticated, so a freshly created local user satisfies the
114
+ // principal condition while proving nothing.
115
+ if (!config_1.apiConfig.authorizationsEnabled) {
116
+ return ('this cluster does not check permissions (AUTHORIZATIONS_ENABLED is ' +
117
+ 'false), so an unprivileged principal is permitted like any other and ' +
118
+ 'a refusal cannot be observed — see tests/api/README.md');
119
+ }
120
+ return false;
121
+ }
122
+ exports.enforcementPrincipalUnavailable = enforcementUnavailableReason();
123
+ /**
124
+ * What `PUT /{roles,groups,tenants}/{id}/users/{username}` does when the
125
+ * username belongs to nobody.
126
+ *
127
+ * The engine guards membership with
128
+ * `case USER -> !localUserEnabled || userState.getUser(entityId).isPresent()`
129
+ * (`RoleAddEntityProcessor`, `GroupAddEntityProcessor`,
130
+ * `TenantAddEntityProcessor` in camunda/camunda). With local users disabled
131
+ * the left side short-circuits, so the membership is written against the bare
132
+ * username and the command is accepted.
133
+ *
134
+ * That is deliberate — in OIDC mode the cluster has no registry to check a
135
+ * username against, and refusing would make it impossible to grant a role to
136
+ * an identity-provider user before their first sign-in. So the two modes
137
+ * genuinely differ, and the assertion has to follow the mode rather than pick
138
+ * one and call the other a bug.
139
+ */
140
+ exports.absentUserMembership = config_1.apiConfig.usersApiEnabled
141
+ ? {
142
+ status: 404,
143
+ reason: 'local users are enabled, so the engine checks the username against ' +
144
+ 'its user state and rejects an unknown one',
145
+ }
146
+ : {
147
+ status: 204,
148
+ reason: 'the cluster is in OIDC mode: with local users disabled the engine ' +
149
+ 'skips the existence check and stores the membership by username, so ' +
150
+ 'that a role can be granted before the user first signs in',
151
+ };
152
+ /**
153
+ * Statuses that mean "this cluster cannot answer the question", as opposed to
154
+ * "something is wrong".
155
+ *
156
+ * - 404: the endpoint is not published (before 8.10 the path is unrouted).
157
+ * - 401: it answers for the *calling principal*, so a deployment that serves
158
+ * /v2 without credentials has no principal for it to answer for.
159
+ */
160
+ const ENFORCEMENT_UNREADABLE = new Set([401, 404]);
161
+ /**
162
+ * Whether the cluster enforces authorization, straight from the cluster.
163
+ *
164
+ * `POST /v2/authentication/me/authorizations/search` carries
165
+ * `authorizationsEnabled` as a required field, so this is the cluster's own
166
+ * answer rather than an inference from the chart values. Added in 8.10.
167
+ *
168
+ * `undefined` means only the two cases above. Every other status throws
169
+ * instead: this reading is the safety net under the enforcement gap recorded
170
+ * in `tests/api/README.md`, and treating a 403, a 500 or a 503 as "unknown"
171
+ * would let the very regression it guards remove it. A 500 from this endpoint
172
+ * is a finding, not a reason to stop looking.
173
+ */
174
+ async function readAuthorizationsEnabled(api) {
175
+ const response = await api.post('/authentication/me/authorizations/search', {
176
+ body: { page: { limit: 1 } },
177
+ tenant: 'none',
178
+ });
179
+ if (ENFORCEMENT_UNREADABLE.has(response.status()))
180
+ return undefined;
181
+ if (response.status() !== 200) {
182
+ throw new Error(`/v2/authentication/me/authorizations/search answered ` +
183
+ `${response.status()}, which is neither the enforcement state nor one ` +
184
+ `of the two statuses that mean this cluster cannot report it ` +
185
+ `(401, 404). Body: ${await response.text()}`);
186
+ }
187
+ const json = (await response.json());
188
+ // The field is required by the schema, so a 200 without a boolean is a
189
+ // contract break rather than an unreadable cluster. The caller asserts the
190
+ // type as well; failing here names the endpoint that broke it.
191
+ if (typeof json.authorizationsEnabled !== 'boolean') {
192
+ throw new Error(`/v2/authentication/me/authorizations/search answered 200 without a ` +
193
+ `boolean authorizationsEnabled, which the v2 schema marks required: ` +
194
+ `${JSON.stringify(json)}`);
195
+ }
196
+ return json.authorizationsEnabled;
197
+ }
198
+ exports.readAuthorizationsEnabled = readAuthorizationsEnabled;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Tracked-resource cleanup.
3
+ *
4
+ * The legacy shell suite cleaned up from a single EXIT trap that also computed
5
+ * the process exit code and ran a final batch of clock assertions — so those
6
+ * assertions could not fail the run, and one slow delete delayed every other.
7
+ * Here each test tracks what it created and the `resources` fixture deletes it
8
+ * when that test ends, in parallel, with failures reported but never masking a
9
+ * test result.
10
+ */
11
+ import type { ApiClient } from './http';
12
+ export type ResourceKind = 'user' | 'role' | 'group' | 'tenant' | 'mappingRule' | 'authorization' | 'document' | 'processInstance' | 'resource';
13
+ export declare class ResourceTracker {
14
+ private readonly tracked;
15
+ track(kind: ResourceKind, id: string | number | undefined | null): void;
16
+ forget(kind: ResourceKind, id: string | number): void;
17
+ /**
18
+ * Deletes everything tracked, most-dependent kinds first so a membership does
19
+ * not outlive its owner. Never throws: a cleanup failure is logged, because
20
+ * failing here would mask the result of the test that just ran.
21
+ */
22
+ cleanup(api: ApiClient): Promise<void>;
23
+ private remove;
24
+ }
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ /**
3
+ * Tracked-resource cleanup.
4
+ *
5
+ * The legacy shell suite cleaned up from a single EXIT trap that also computed
6
+ * the process exit code and ran a final batch of clock assertions — so those
7
+ * assertions could not fail the run, and one slow delete delayed every other.
8
+ * Here each test tracks what it created and the `resources` fixture deletes it
9
+ * when that test ends, in parallel, with failures reported but never masking a
10
+ * test result.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.ResourceTracker = void 0;
14
+ const DELETE_PATHS = {
15
+ user: '/users/{username}',
16
+ role: '/roles/{roleId}',
17
+ group: '/groups/{groupId}',
18
+ tenant: '/tenants/{tenantId}',
19
+ mappingRule: '/mapping-rules/{mappingRuleId}',
20
+ authorization: '/authorizations/{authorizationKey}',
21
+ document: '/documents/{documentId}',
22
+ };
23
+ const PARAM_NAMES = {
24
+ user: 'username',
25
+ role: 'roleId',
26
+ group: 'groupId',
27
+ tenant: 'tenantId',
28
+ mappingRule: 'mappingRuleId',
29
+ authorization: 'authorizationKey',
30
+ document: 'documentId',
31
+ };
32
+ /** Statuses that mean "the resource is gone", which is all cleanup needs. */
33
+ const GONE = new Set([200, 202, 204, 404]);
34
+ class ResourceTracker {
35
+ tracked = new Map();
36
+ track(kind, id) {
37
+ if (id === undefined || id === null || id === '')
38
+ return;
39
+ const set = this.tracked.get(kind) ?? new Set();
40
+ set.add(String(id));
41
+ this.tracked.set(kind, set);
42
+ }
43
+ forget(kind, id) {
44
+ this.tracked.get(kind)?.delete(String(id));
45
+ }
46
+ /**
47
+ * Deletes everything tracked, most-dependent kinds first so a membership does
48
+ * not outlive its owner. Never throws: a cleanup failure is logged, because
49
+ * failing here would mask the result of the test that just ran.
50
+ */
51
+ async cleanup(api) {
52
+ const order = [
53
+ 'processInstance',
54
+ 'authorization',
55
+ 'document',
56
+ 'mappingRule',
57
+ 'user',
58
+ 'group',
59
+ 'role',
60
+ 'tenant',
61
+ // Last, and after `processInstance` deliberately — but the engine does
62
+ // not need it to be, and cleanup does not wait for the cancellations to
63
+ // land before getting here.
64
+ //
65
+ // `ResourceDeletionDeleteProcessor.deleteProcess` accepts the deletion
66
+ // either way. It writes `ProcessIntent.DRAINING`, then finalises at once
67
+ // when `hasActiveProcessInstances` is false and otherwise leaves the
68
+ // definition draining until its instances end. Identical on stable/8.8,
69
+ // 8.9 and 8.10.
70
+ //
71
+ // So cancelling first is an optimisation, not a precondition: it makes
72
+ // the common case finalise immediately instead of draining. Cancellation
73
+ // is asynchronous and a 204 only means the command was written, so a
74
+ // definition can still reach deletion with a live instance — that costs
75
+ // a short DRAINING window which resolves itself when the cancellation
76
+ // takes effect, and nothing is left behind either way. Waiting for
77
+ // TERMINATED here would buy that window back at the price of an
78
+ // exporter-lag poll in every test's teardown.
79
+ 'resource',
80
+ ];
81
+ for (const kind of order) {
82
+ const ids = this.tracked.get(kind);
83
+ if (!ids || ids.size === 0)
84
+ continue;
85
+ const results = await Promise.allSettled([...ids].map((id) => this.remove(api, kind, id)));
86
+ const failed = results.filter((r) => r.status === 'rejected').length;
87
+ if (failed > 0) {
88
+ console.warn(`Cleanup: ${failed}/${ids.size} ${kind}(s) not removed`);
89
+ }
90
+ ids.clear();
91
+ }
92
+ }
93
+ async remove(api, kind, id) {
94
+ try {
95
+ const response = kind === 'processInstance'
96
+ ? await api.post('/process-instances/{processInstanceKey}/cancellation', {
97
+ params: { processInstanceKey: id },
98
+ body: {},
99
+ tenant: 'none',
100
+ })
101
+ : kind === 'resource'
102
+ ? await api.post('/resources/{resourceKey}/deletion', {
103
+ params: { resourceKey: id },
104
+ body: {},
105
+ tenant: 'none',
106
+ })
107
+ : await api.delete(DELETE_PATHS[kind], {
108
+ params: { [PARAM_NAMES[kind]]: id },
109
+ tenant: 'none',
110
+ });
111
+ if (!GONE.has(response.status())) {
112
+ console.warn(`Cleanup: ${kind} ${id} returned ${response.status()}`);
113
+ }
114
+ }
115
+ catch (error) {
116
+ console.warn(`Cleanup: ${kind} ${id} threw`, error);
117
+ }
118
+ }
119
+ }
120
+ exports.ResourceTracker = ResourceTracker;