@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.
- package/dist/fixtures/api.d.ts +15 -0
- package/dist/fixtures/api.js +27 -0
- package/dist/tests/api/v2/authorization/authorization-api-tests.spec.d.ts +1 -0
- package/dist/tests/api/v2/authorization/authorization-api-tests.spec.js +247 -0
- package/dist/tests/api/v2/authorization/authorization-enforcement-api-tests.spec.d.ts +18 -0
- package/dist/tests/api/v2/authorization/authorization-enforcement-api-tests.spec.js +208 -0
- package/dist/tests/api/v2/batch-operation/batch-operation-api-tests.spec.d.ts +1 -0
- package/dist/tests/api/v2/batch-operation/batch-operation-api-tests.spec.js +264 -0
- package/dist/tests/api/v2/cluster/cluster-api-tests.spec.d.ts +1 -0
- package/dist/tests/api/v2/cluster/cluster-api-tests.spec.js +86 -0
- package/dist/tests/api/v2/decision/decision-api-tests.spec.d.ts +1 -0
- package/dist/tests/api/v2/decision/decision-api-tests.spec.js +233 -0
- package/dist/tests/api/v2/deployment/deployment-api-tests.spec.d.ts +1 -0
- package/dist/tests/api/v2/deployment/deployment-api-tests.spec.js +147 -0
- package/dist/tests/api/v2/document/document-api-tests.spec.d.ts +9 -0
- package/dist/tests/api/v2/document/document-api-tests.spec.js +111 -0
- package/dist/tests/api/v2/element-instance/element-instance-api-tests.spec.d.ts +1 -0
- package/dist/tests/api/v2/element-instance/element-instance-api-tests.spec.js +127 -0
- package/dist/tests/api/v2/flows/end-to-end-flows.spec.d.ts +9 -0
- package/dist/tests/api/v2/flows/end-to-end-flows.spec.js +204 -0
- package/dist/tests/api/v2/form/form-api-tests.spec.d.ts +1 -0
- package/dist/tests/api/v2/form/form-api-tests.spec.js +88 -0
- package/dist/tests/api/v2/identity/group-api-tests.spec.d.ts +1 -0
- package/dist/tests/api/v2/identity/group-api-tests.spec.js +172 -0
- package/dist/tests/api/v2/identity/membership-api-tests.spec.d.ts +10 -0
- package/dist/tests/api/v2/identity/membership-api-tests.spec.js +164 -0
- package/dist/tests/api/v2/identity/role-api-tests.spec.d.ts +1 -0
- package/dist/tests/api/v2/identity/role-api-tests.spec.js +212 -0
- package/dist/tests/api/v2/identity/tenant-api-tests.spec.d.ts +1 -0
- package/dist/tests/api/v2/identity/tenant-api-tests.spec.js +319 -0
- package/dist/tests/api/v2/identity/user-api-tests.spec.d.ts +1 -0
- package/dist/tests/api/v2/identity/user-api-tests.spec.js +148 -0
- package/dist/tests/api/v2/job/job-api-tests.spec.d.ts +1 -0
- package/dist/tests/api/v2/job/job-api-tests.spec.js +260 -0
- package/dist/tests/api/v2/message/message-api-tests.spec.d.ts +1 -0
- package/dist/tests/api/v2/message/message-api-tests.spec.js +219 -0
- package/dist/tests/api/v2/process-definition/process-definition-api-tests.spec.d.ts +1 -0
- package/dist/tests/api/v2/process-definition/process-definition-api-tests.spec.js +118 -0
- package/dist/tests/api/v2/process-instance/process-instance-api-tests.spec.d.ts +1 -0
- package/dist/tests/api/v2/process-instance/process-instance-api-tests.spec.js +335 -0
- package/dist/tests/api/v2/runtime/batch-modification-api-tests.spec.d.ts +1 -0
- package/dist/tests/api/v2/runtime/batch-modification-api-tests.spec.js +123 -0
- package/dist/tests/api/v2/runtime/variable-incident-api-tests.spec.d.ts +1 -0
- package/dist/tests/api/v2/runtime/variable-incident-api-tests.spec.js +174 -0
- package/dist/tests/api/v2/system/clock-api-tests.spec.d.ts +19 -0
- package/dist/tests/api/v2/system/clock-api-tests.spec.js +62 -0
- package/dist/tests/api/v2/system/system-api-tests.spec.d.ts +1 -0
- package/dist/tests/api/v2/system/system-api-tests.spec.js +183 -0
- package/dist/tests/api/v2/user-task/user-task-api-tests.spec.d.ts +1 -0
- package/dist/tests/api/v2/user-task/user-task-api-tests.spec.js +258 -0
- package/dist/utils/api/assertions.d.ts +50 -0
- package/dist/utils/api/assertions.js +142 -0
- package/dist/utils/api/auth.d.ts +27 -0
- package/dist/utils/api/auth.js +137 -0
- package/dist/utils/api/beans.d.ts +46 -0
- package/dist/utils/api/beans.js +87 -0
- package/dist/utils/api/bpmn.d.ts +50 -0
- package/dist/utils/api/bpmn.js +217 -0
- package/dist/utils/api/capabilities.d.ts +90 -0
- package/dist/utils/api/capabilities.js +198 -0
- package/dist/utils/api/cleanup.d.ts +24 -0
- package/dist/utils/api/cleanup.js +120 -0
- package/dist/utils/api/config.d.ts +103 -0
- package/dist/utils/api/config.js +62 -0
- package/dist/utils/api/deployments.d.ts +45 -0
- package/dist/utils/api/deployments.js +120 -0
- package/dist/utils/api/eventually.d.ts +36 -0
- package/dist/utils/api/eventually.js +54 -0
- package/dist/utils/api/http.d.ts +56 -0
- package/dist/utils/api/http.js +244 -0
- package/dist/utils/api/identity.d.ts +29 -0
- package/dist/utils/api/identity.js +43 -0
- package/dist/utils/api/keys.d.ts +37 -0
- package/dist/utils/api/keys.js +40 -0
- package/dist/utils/api/paths.d.ts +25 -0
- package/dist/utils/api/paths.js +167 -0
- package/dist/utils/api/tenancy.d.ts +31 -0
- package/dist/utils/api/tenancy.js +65 -0
- package/package.json +1 -1
- package/playwright.config.ts +14 -2
- package/dist/resources/test-api-v2-complete.sh +0 -12246
- 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, '<')
|
|
34
|
+
.replace(/>/g, '>')
|
|
35
|
+
.replace(/"/g, '"');
|
|
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;
|