@camunda/e2e-test-suite 0.0.1241 → 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/pages/SM-8.10/NavigationPage.d.ts +3 -2
- package/dist/pages/SM-8.10/NavigationPage.js +24 -34
- package/dist/pages/SM-8.10/ProcessApplicationPage.js +7 -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,208 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Authorization *enforcement* — that the cluster actually refuses an
|
|
4
|
+
* unprivileged principal, not merely that the authorization API accepts writes.
|
|
5
|
+
*
|
|
6
|
+
* This is the coverage a Helm-upgrade run most needs, because role bindings are
|
|
7
|
+
* what an upgrade is most likely to lose. In the shell suite it was 2,700 lines
|
|
8
|
+
* (9506-12224) inside a single `if [[ "$TEST_CLIENT_TOKEN_VALID" ]]` block: if
|
|
9
|
+
* the token fetch failed, the entire section silently did not run and the script
|
|
10
|
+
* still exited 0.
|
|
11
|
+
*
|
|
12
|
+
* Two things change here. The gate is a hard failure rather than a silent skip,
|
|
13
|
+
* so "enforcement was never checked" can no longer look like success. And the
|
|
14
|
+
* unprivileged principal is a user this spec creates, rather than the suite's own
|
|
15
|
+
* admin client with its permissions temporarily stripped and restored — that
|
|
16
|
+
* approach mutated cluster-wide admin state mid-run, so a failure between the
|
|
17
|
+
* strip and the restore left the rest of the suite running without permissions.
|
|
18
|
+
*/
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
const api_1 = require("../../../../fixtures/api");
|
|
21
|
+
const test_1 = require("@playwright/test");
|
|
22
|
+
const assertions_1 = require("../../../../utils/api/assertions");
|
|
23
|
+
const beans_1 = require("../../../../utils/api/beans");
|
|
24
|
+
const config_1 = require("../../../../utils/api/config");
|
|
25
|
+
const auth_1 = require("../../../../utils/api/auth");
|
|
26
|
+
const capabilities_1 = require("../../../../utils/api/capabilities");
|
|
27
|
+
const eventually_1 = require("../../../../utils/api/eventually");
|
|
28
|
+
const bpmn_1 = require("../../../../utils/api/bpmn");
|
|
29
|
+
const deployments_1 = require("../../../../utils/api/deployments");
|
|
30
|
+
/**
|
|
31
|
+
* Obtains a principal that holds no roles, by whichever route the cluster
|
|
32
|
+
* allows.
|
|
33
|
+
*
|
|
34
|
+
* Throws when neither route works. That is deliberate: without a second
|
|
35
|
+
* principal there is no way to observe enforcement, and a suite that quietly
|
|
36
|
+
* reports success in that state is worse than one that fails. The tests are
|
|
37
|
+
* gated on `enforcementPrincipalUnavailable` before they get here, so a throw
|
|
38
|
+
* means a configured route broke rather than that none was configured.
|
|
39
|
+
*/
|
|
40
|
+
async function unprivilegedPrincipal(api, request, resources) {
|
|
41
|
+
// OIDC cluster: act as a second identity-provider client. It must hold no
|
|
42
|
+
// roles for the assertions below to mean anything, which is a property of
|
|
43
|
+
// how it was provisioned rather than something this suite can enforce.
|
|
44
|
+
if (!config_1.apiConfig.usersApiEnabled) {
|
|
45
|
+
const { clientId, clientSecret } = config_1.apiConfig.secondaryClient;
|
|
46
|
+
const header = await (0, auth_1.clientAuthHeader)(request, clientId, clientSecret);
|
|
47
|
+
if (!header) {
|
|
48
|
+
throw new Error(`Could not obtain a token for the secondary client "${clientId}" from ` +
|
|
49
|
+
`${config_1.apiConfig.oauth.tokenUrl}. Enforcement cannot be observed without a ` +
|
|
50
|
+
`second principal, so this is a failure rather than a skip. Check that ` +
|
|
51
|
+
`SECONDARY_CLIENT_SECRET matches the client and that client credentials ` +
|
|
52
|
+
`are enabled for it.`);
|
|
53
|
+
}
|
|
54
|
+
return { ownerId: clientId, ownerType: 'CLIENT', authorization: header };
|
|
55
|
+
}
|
|
56
|
+
const user = (0, beans_1.newUser)();
|
|
57
|
+
await (0, assertions_1.assertCreated)(await api.post('/users', { body: user, tenant: 'none' }));
|
|
58
|
+
resources.track('user', user.username);
|
|
59
|
+
if (config_1.apiConfig.authMethod === 'basic') {
|
|
60
|
+
const encoded = Buffer.from(`${user.username}:${user.password}`).toString('base64');
|
|
61
|
+
return {
|
|
62
|
+
ownerId: user.username,
|
|
63
|
+
ownerType: 'USER',
|
|
64
|
+
authorization: `Basic ${encoded}`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const header = await (0, auth_1.userAuthHeader)(request, user.username, user.password);
|
|
68
|
+
if (!header) {
|
|
69
|
+
throw new Error(`Could not obtain a token for the freshly created user "${user.username}" ` +
|
|
70
|
+
`from ${config_1.apiConfig.oauth.tokenUrl}. Enforcement cannot be observed without a ` +
|
|
71
|
+
`second principal, so this is a failure rather than a skip. Check that the ` +
|
|
72
|
+
`"${config_1.apiConfig.oauth.clientId}" client has direct access grants enabled and that ` +
|
|
73
|
+
`users created through /v2/users are resolvable by the identity provider.`);
|
|
74
|
+
}
|
|
75
|
+
return { ownerId: user.username, ownerType: 'USER', authorization: header };
|
|
76
|
+
}
|
|
77
|
+
api_1.test.describe('Authorization enforcement', () => {
|
|
78
|
+
// Without a second, unprivileged principal there is nothing to enforce
|
|
79
|
+
// against. The reason names what to configure so this reads as a gap to
|
|
80
|
+
// close rather than a test that mysteriously does not run.
|
|
81
|
+
api_1.test.skip(capabilities_1.enforcementPrincipalUnavailable !== false, capabilities_1.enforcementPrincipalUnavailable === false
|
|
82
|
+
? ''
|
|
83
|
+
: capabilities_1.enforcementPrincipalUnavailable);
|
|
84
|
+
(0, api_1.test)('refuses a process-definition search to an unprivileged user', async ({ api, request, resources, }) => {
|
|
85
|
+
const principal = await unprivilegedPrincipal(api, request, resources);
|
|
86
|
+
const response = await api.post('/process-definitions/search', {
|
|
87
|
+
body: { filter: {} },
|
|
88
|
+
authorization: principal.authorization,
|
|
89
|
+
});
|
|
90
|
+
// A cluster that has lost its authorization wiring answers 200 here.
|
|
91
|
+
await (0, assertions_1.assertStatusIn)(response, [200, 403], 'an unprivileged principal is refused outright, or served an empty result set');
|
|
92
|
+
if (response.status() === 200) {
|
|
93
|
+
// Where the API filters rather than refuses, the result must be empty —
|
|
94
|
+
// seeing another principal's definitions is the same defect as a 200 on
|
|
95
|
+
// a forbidden call.
|
|
96
|
+
await (0, assertions_1.assertPaginated)(response, { exactItems: 0 });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
(0, api_1.test)('refuses a user list to an unprivileged user', async ({ api, request, resources, }) => {
|
|
100
|
+
// In OIDC mode /v2/users/** is closed to everyone, so a 403 here would
|
|
101
|
+
// come from the endpoint filter rather than from the authorization check
|
|
102
|
+
// this test is about — it would pass without enforcement working at all.
|
|
103
|
+
api_1.test.skip(capabilities_1.usersApiUnavailable !== false, capabilities_1.usersApiUnavailable === false ? '' : capabilities_1.usersApiUnavailable);
|
|
104
|
+
const principal = await unprivilegedPrincipal(api, request, resources);
|
|
105
|
+
const response = await api.post('/users/search', {
|
|
106
|
+
body: { filter: {} },
|
|
107
|
+
tenant: 'none',
|
|
108
|
+
authorization: principal.authorization,
|
|
109
|
+
});
|
|
110
|
+
await (0, assertions_1.assertStatusIn)(response, [200, 403], 'an unprivileged principal is refused outright, or served an empty result set');
|
|
111
|
+
if (response.status() === 200) {
|
|
112
|
+
await (0, assertions_1.assertPaginated)(response, { exactItems: 0 });
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
(0, api_1.test)('refuses a deployment to an unprivileged user', async ({ api, request, resources, }) => {
|
|
116
|
+
const principal = await unprivilegedPrincipal(api, request, resources);
|
|
117
|
+
const processId = (0, bpmn_1.uniqueId)('enforcement-deploy');
|
|
118
|
+
const response = await api.post('/deployments', {
|
|
119
|
+
tenant: 'none',
|
|
120
|
+
authorization: principal.authorization,
|
|
121
|
+
multipart: {
|
|
122
|
+
resources: {
|
|
123
|
+
name: `${processId}.bpmn`,
|
|
124
|
+
mimeType: 'application/xml',
|
|
125
|
+
buffer: Buffer.from((0, bpmn_1.linearProcess)(processId, bpmn_1.element.serviceTask((0, bpmn_1.uniqueId)('job'))), 'utf-8'),
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
await (0, assertions_1.assertForbidden)(response);
|
|
130
|
+
// Backcheck: the refusal must also mean nothing was created. The shell
|
|
131
|
+
// suite called this step "BACKCHECK" and it is the part worth keeping.
|
|
132
|
+
await (0, eventually_1.eventually)(async () => {
|
|
133
|
+
const search = await api.post('/process-definitions/search', {
|
|
134
|
+
body: { filter: { processDefinitionId: processId } },
|
|
135
|
+
});
|
|
136
|
+
await (0, assertions_1.assertPaginated)(search, { exactItems: 0 });
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
(0, api_1.test)('refuses role creation to an unprivileged user', async ({ api, request, resources, }) => {
|
|
140
|
+
const principal = await unprivilegedPrincipal(api, request, resources);
|
|
141
|
+
const roleId = (0, bpmn_1.uniqueId)('enforcement-role');
|
|
142
|
+
const response = await api.post('/roles', {
|
|
143
|
+
body: { roleId, name: 'should not be created' },
|
|
144
|
+
tenant: 'none',
|
|
145
|
+
authorization: principal.authorization,
|
|
146
|
+
});
|
|
147
|
+
await (0, assertions_1.assertForbidden)(response);
|
|
148
|
+
const lookup = await api.get('/roles/{roleId}', {
|
|
149
|
+
params: { roleId },
|
|
150
|
+
tenant: 'none',
|
|
151
|
+
});
|
|
152
|
+
(0, test_1.expect)(lookup.status(), 'a refused role creation must leave no role behind').toBe(404);
|
|
153
|
+
});
|
|
154
|
+
(0, api_1.test)('refuses starting a process instance to an unprivileged user', async ({ api, request, resources, }) => {
|
|
155
|
+
const principal = await unprivilegedPrincipal(api, request, resources);
|
|
156
|
+
const processId = (0, bpmn_1.uniqueId)('enforcement-start');
|
|
157
|
+
await (0, deployments_1.deployProcess)(api, resources, processId, (0, bpmn_1.linearProcess)(processId, bpmn_1.element.serviceTask((0, bpmn_1.uniqueId)('job'))));
|
|
158
|
+
const response = await api.post('/process-instances', {
|
|
159
|
+
body: { processDefinitionId: processId },
|
|
160
|
+
tenant: 'root',
|
|
161
|
+
authorization: principal.authorization,
|
|
162
|
+
});
|
|
163
|
+
await (0, assertions_1.assertForbidden)(response);
|
|
164
|
+
await (0, eventually_1.eventually)(async () => {
|
|
165
|
+
const search = await api.post('/process-instances/search', {
|
|
166
|
+
body: { filter: { processDefinitionId: processId } },
|
|
167
|
+
});
|
|
168
|
+
await (0, assertions_1.assertPaginated)(search, { exactItems: 0 });
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
(0, api_1.test)('admits a user once granted the matching authorization', async ({ api, request, resources, }) => {
|
|
172
|
+
const principal = await unprivilegedPrincipal(api, request, resources);
|
|
173
|
+
const processId = (0, bpmn_1.uniqueId)('enforcement-grant');
|
|
174
|
+
await (0, deployments_1.deployProcess)(api, resources, processId, (0, bpmn_1.linearProcess)(processId, bpmn_1.element.serviceTask((0, bpmn_1.uniqueId)('job'))));
|
|
175
|
+
// Refused before the grant.
|
|
176
|
+
const before = await api.post('/process-definitions/search', {
|
|
177
|
+
body: { filter: { processDefinitionId: processId } },
|
|
178
|
+
authorization: principal.authorization,
|
|
179
|
+
});
|
|
180
|
+
if (before.status() === 200) {
|
|
181
|
+
await (0, assertions_1.assertPaginated)(before, { exactItems: 0 });
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
await (0, assertions_1.assertForbidden)(before);
|
|
185
|
+
}
|
|
186
|
+
const grant = await api.post('/authorizations', {
|
|
187
|
+
body: {
|
|
188
|
+
ownerType: principal.ownerType,
|
|
189
|
+
ownerId: principal.ownerId,
|
|
190
|
+
resourceType: 'PROCESS_DEFINITION',
|
|
191
|
+
resourceId: processId,
|
|
192
|
+
permissionTypes: ['READ_PROCESS_DEFINITION'],
|
|
193
|
+
},
|
|
194
|
+
tenant: 'none',
|
|
195
|
+
});
|
|
196
|
+
const created = (await (0, assertions_1.assertCreated)(grant));
|
|
197
|
+
resources.track('authorization', String(created.authorizationKey));
|
|
198
|
+
// Admitted after it. This direction is what proves the refusal above came
|
|
199
|
+
// from the authorization check rather than from the resource being absent.
|
|
200
|
+
await (0, eventually_1.eventually)(async () => {
|
|
201
|
+
const after = await api.post('/process-definitions/search', {
|
|
202
|
+
body: { filter: { processDefinitionId: processId } },
|
|
203
|
+
authorization: principal.authorization,
|
|
204
|
+
});
|
|
205
|
+
await (0, assertions_1.assertPaginated)(after, { exactItems: 1 });
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const api_1 = require("../../../../fixtures/api");
|
|
4
|
+
const test_1 = require("@playwright/test");
|
|
5
|
+
const assertions_1 = require("../../../../utils/api/assertions");
|
|
6
|
+
const deployments_1 = require("../../../../utils/api/deployments");
|
|
7
|
+
const bpmn_1 = require("../../../../utils/api/bpmn");
|
|
8
|
+
const eventually_1 = require("../../../../utils/api/eventually");
|
|
9
|
+
const keys_1 = require("../../../../utils/api/keys");
|
|
10
|
+
/**
|
|
11
|
+
* Deploys a process, starts `count` instances, and returns a filter that
|
|
12
|
+
* selects exactly those instances.
|
|
13
|
+
*
|
|
14
|
+
* The shell suite ran its batch operations against `{"filter":{}}` — every
|
|
15
|
+
* instance on the cluster — which is why its batch section had to be serial
|
|
16
|
+
* and why it needed a 900-line concurrency block to cope with the collisions
|
|
17
|
+
* that caused. Scoping each batch to its own process definition removes both.
|
|
18
|
+
*/
|
|
19
|
+
async function seedInstances(api, resources, count) {
|
|
20
|
+
const processId = (0, bpmn_1.uniqueId)('batch-api');
|
|
21
|
+
await (0, deployments_1.deployProcess)(api, resources, processId, (0, bpmn_1.linearProcess)(processId, bpmn_1.element.serviceTask((0, bpmn_1.uniqueId)('job'))));
|
|
22
|
+
const instanceKeys = [];
|
|
23
|
+
for (let i = 0; i < count; i++) {
|
|
24
|
+
const response = await api.post('/process-instances', {
|
|
25
|
+
body: { processDefinitionId: processId },
|
|
26
|
+
tenant: 'root',
|
|
27
|
+
});
|
|
28
|
+
await (0, assertions_1.assertStatusIn)(response, [200], 'instance creation is synchronous');
|
|
29
|
+
const json = (await response.json());
|
|
30
|
+
instanceKeys.push(String(json.processInstanceKey));
|
|
31
|
+
}
|
|
32
|
+
// The batch resolves its scope from the search store, so the instances must
|
|
33
|
+
// be exported before the batch is created or it selects nothing.
|
|
34
|
+
await (0, eventually_1.eventually)(async () => {
|
|
35
|
+
const search = await api.post('/process-instances/search', {
|
|
36
|
+
body: { filter: { processDefinitionId: processId }, page: { limit: 50 } },
|
|
37
|
+
});
|
|
38
|
+
await (0, assertions_1.assertPaginated)(search, { minItems: count });
|
|
39
|
+
});
|
|
40
|
+
return { processId, instanceKeys };
|
|
41
|
+
}
|
|
42
|
+
async function createCancellationBatch(api, processId) {
|
|
43
|
+
const response = await api.post('/process-instances/cancellation', {
|
|
44
|
+
body: { filter: { processDefinitionId: processId } },
|
|
45
|
+
tenant: 'filter',
|
|
46
|
+
});
|
|
47
|
+
await (0, assertions_1.assertStatusIn)(response, [200], 'the batch is accepted and its key returned synchronously');
|
|
48
|
+
const json = (await response.json());
|
|
49
|
+
(0, assertions_1.assertRequiredFields)(json, ['batchOperationKey']);
|
|
50
|
+
return String(json.batchOperationKey);
|
|
51
|
+
}
|
|
52
|
+
api_1.test.describe('Batch operations', () => {
|
|
53
|
+
(0, api_1.test)('creates a cancellation batch and cancels its instances', async ({ api, resources, }) => {
|
|
54
|
+
const { processId, instanceKeys } = await seedInstances(api, resources, 3);
|
|
55
|
+
const batchKey = await createCancellationBatch(api, processId);
|
|
56
|
+
await (0, eventually_1.eventually)(async () => {
|
|
57
|
+
const response = await api.get('/batch-operations/{batchOperationKey}', {
|
|
58
|
+
params: { batchOperationKey: batchKey },
|
|
59
|
+
tenant: 'none',
|
|
60
|
+
});
|
|
61
|
+
const batch = (await (0, assertions_1.assertOk)(response));
|
|
62
|
+
(0, assertions_1.assertRequiredFields)(batch, [
|
|
63
|
+
'batchOperationKey',
|
|
64
|
+
'state',
|
|
65
|
+
'batchOperationType',
|
|
66
|
+
]);
|
|
67
|
+
(0, test_1.expect)(batch.batchOperationType).toBe('CANCEL_PROCESS_INSTANCE');
|
|
68
|
+
(0, test_1.expect)(['COMPLETED', 'PARTIALLY_COMPLETED']).toContain(batch.state);
|
|
69
|
+
});
|
|
70
|
+
// The shell suite asserted only that the batch endpoint answered. Whether
|
|
71
|
+
// the instances were actually cancelled was never checked.
|
|
72
|
+
await (0, eventually_1.eventually)(async () => {
|
|
73
|
+
const search = await api.post('/process-instances/search', {
|
|
74
|
+
body: {
|
|
75
|
+
filter: { processDefinitionId: processId, state: 'ACTIVE' },
|
|
76
|
+
page: { limit: 50 },
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
await (0, assertions_1.assertPaginated)(search, { exactItems: 0 });
|
|
80
|
+
});
|
|
81
|
+
(0, test_1.expect)(instanceKeys.length).toBe(3);
|
|
82
|
+
});
|
|
83
|
+
(0, api_1.test)('finds a created batch through search', async ({ api, resources }) => {
|
|
84
|
+
const { processId } = await seedInstances(api, resources, 1);
|
|
85
|
+
const batchKey = await createCancellationBatch(api, processId);
|
|
86
|
+
await (0, eventually_1.eventually)(async () => {
|
|
87
|
+
const response = await api.post('/batch-operations/search', {
|
|
88
|
+
body: { filter: { batchOperationKey: batchKey }, page: { limit: 10 } },
|
|
89
|
+
tenant: 'none',
|
|
90
|
+
});
|
|
91
|
+
await (0, assertions_1.assertPaginated)(response, { exactItems: 1 });
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
(0, api_1.test)('filters a batch search by state', async ({ api, resources }) => {
|
|
95
|
+
const { processId } = await seedInstances(api, resources, 1);
|
|
96
|
+
const batchKey = await createCancellationBatch(api, processId);
|
|
97
|
+
await (0, eventually_1.eventually)(async () => {
|
|
98
|
+
const lookup = await api.get('/batch-operations/{batchOperationKey}', {
|
|
99
|
+
params: { batchOperationKey: batchKey },
|
|
100
|
+
tenant: 'none',
|
|
101
|
+
});
|
|
102
|
+
const batch = (await (0, assertions_1.assertOk)(lookup));
|
|
103
|
+
const response = await api.post('/batch-operations/search', {
|
|
104
|
+
body: {
|
|
105
|
+
filter: { state: batch.state },
|
|
106
|
+
page: { limit: 20 },
|
|
107
|
+
},
|
|
108
|
+
tenant: 'none',
|
|
109
|
+
});
|
|
110
|
+
const page = await (0, assertions_1.assertPaginated)(response, { minItems: 1 });
|
|
111
|
+
for (const item of page.items) {
|
|
112
|
+
(0, test_1.expect)(item.state).toBe(batch.state);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
(0, api_1.test)('lists the items belonging to a batch', async ({ api, resources }) => {
|
|
117
|
+
const { processId } = await seedInstances(api, resources, 2);
|
|
118
|
+
const batchKey = await createCancellationBatch(api, processId);
|
|
119
|
+
await (0, eventually_1.eventuallyGet)(async () => {
|
|
120
|
+
const response = await api.post('/batch-operation-items/search', {
|
|
121
|
+
body: { filter: { batchOperationKey: batchKey }, page: { limit: 20 } },
|
|
122
|
+
tenant: 'none',
|
|
123
|
+
});
|
|
124
|
+
const page = await (0, assertions_1.assertPaginated)(response);
|
|
125
|
+
return page.items;
|
|
126
|
+
}, (items) => items.length >= 2, { description: `items of batch ${batchKey}` });
|
|
127
|
+
});
|
|
128
|
+
(0, api_1.test)('suspends and resumes a batch', async ({ api, resources }) => {
|
|
129
|
+
// A larger batch keeps the operation in flight long enough to suspend.
|
|
130
|
+
const { processId } = await seedInstances(api, resources, 5);
|
|
131
|
+
const batchKey = await createCancellationBatch(api, processId);
|
|
132
|
+
const suspend = await api.post('/batch-operations/{batchOperationKey}/suspension', { params: { batchOperationKey: batchKey }, body: {}, tenant: 'none' });
|
|
133
|
+
await (0, assertions_1.assertStatusIn)(suspend, [204, 409], 'a batch that has already finished cannot be suspended');
|
|
134
|
+
if (suspend.status() === 204) {
|
|
135
|
+
await (0, eventually_1.eventually)(async () => {
|
|
136
|
+
const lookup = await api.get('/batch-operations/{batchOperationKey}', {
|
|
137
|
+
params: { batchOperationKey: batchKey },
|
|
138
|
+
tenant: 'none',
|
|
139
|
+
});
|
|
140
|
+
const batch = (await (0, assertions_1.assertOk)(lookup));
|
|
141
|
+
(0, test_1.expect)(batch.state).toBe('SUSPENDED');
|
|
142
|
+
});
|
|
143
|
+
const resume = await api.post('/batch-operations/{batchOperationKey}/resumption', { params: { batchOperationKey: batchKey }, body: {}, tenant: 'none' });
|
|
144
|
+
await (0, assertions_1.assertStatusIn)(resume, [204], 'resumption is acknowledged');
|
|
145
|
+
await (0, eventually_1.eventually)(async () => {
|
|
146
|
+
const lookup = await api.get('/batch-operations/{batchOperationKey}', {
|
|
147
|
+
params: { batchOperationKey: batchKey },
|
|
148
|
+
tenant: 'none',
|
|
149
|
+
});
|
|
150
|
+
const batch = (await (0, assertions_1.assertOk)(lookup));
|
|
151
|
+
(0, test_1.expect)(batch.state).not.toBe('SUSPENDED');
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
(0, api_1.test)('cancels a suspended batch', async ({ api, resources }) => {
|
|
156
|
+
const { processId } = await seedInstances(api, resources, 5);
|
|
157
|
+
const batchKey = await createCancellationBatch(api, processId);
|
|
158
|
+
const suspend = await api.post('/batch-operations/{batchOperationKey}/suspension', { params: { batchOperationKey: batchKey }, body: {}, tenant: 'none' });
|
|
159
|
+
await (0, assertions_1.assertStatusIn)(suspend, [204, 409], 'a batch that has already finished cannot be suspended');
|
|
160
|
+
if (suspend.status() === 204) {
|
|
161
|
+
const cancel = await api.post('/batch-operations/{batchOperationKey}/cancellation', { params: { batchOperationKey: batchKey }, body: {}, tenant: 'none' });
|
|
162
|
+
await (0, assertions_1.assertStatusIn)(cancel, [204], 'cancellation is acknowledged');
|
|
163
|
+
await (0, eventually_1.eventually)(async () => {
|
|
164
|
+
const lookup = await api.get('/batch-operations/{batchOperationKey}', {
|
|
165
|
+
params: { batchOperationKey: batchKey },
|
|
166
|
+
tenant: 'none',
|
|
167
|
+
});
|
|
168
|
+
const batch = (await (0, assertions_1.assertOk)(lookup));
|
|
169
|
+
(0, test_1.expect)(batch.state).toBe('CANCELED');
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
(0, api_1.test)('creates a migration batch', async ({ api, resources }) => {
|
|
174
|
+
const processId = (0, bpmn_1.uniqueId)('batch-migration');
|
|
175
|
+
const jobType = (0, bpmn_1.uniqueId)('job');
|
|
176
|
+
const v1 = await (0, deployments_1.deployProcess)(api, resources, processId, (0, bpmn_1.linearProcess)(processId, bpmn_1.element.serviceTask(jobType), { name: 'v1' }));
|
|
177
|
+
const v2 = await (0, deployments_1.deployProcess)(api, resources, processId, (0, bpmn_1.linearProcess)(processId, bpmn_1.element.serviceTask(jobType), { name: 'v2' }));
|
|
178
|
+
(0, test_1.expect)(v2.version).toBe(v1.version + 1);
|
|
179
|
+
const start = await api.post('/process-instances', {
|
|
180
|
+
body: { processDefinitionId: processId },
|
|
181
|
+
tenant: 'root',
|
|
182
|
+
});
|
|
183
|
+
const instance = (await start.json());
|
|
184
|
+
resources.track('processInstance', String(instance.processInstanceKey));
|
|
185
|
+
await (0, eventually_1.eventually)(async () => {
|
|
186
|
+
const search = await api.post('/process-instances/search', {
|
|
187
|
+
body: { filter: { processInstanceKey: instance.processInstanceKey } },
|
|
188
|
+
});
|
|
189
|
+
await (0, assertions_1.assertPaginated)(search, { exactItems: 1 });
|
|
190
|
+
});
|
|
191
|
+
const response = await api.post('/process-instances/migration', {
|
|
192
|
+
body: {
|
|
193
|
+
filter: { processDefinitionKey: v1.processDefinitionKey },
|
|
194
|
+
migrationPlan: {
|
|
195
|
+
targetProcessDefinitionKey: v2.processDefinitionKey,
|
|
196
|
+
mappingInstructions: [
|
|
197
|
+
{ sourceElementId: 'task', targetElementId: 'task' },
|
|
198
|
+
],
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
tenant: 'filter',
|
|
202
|
+
});
|
|
203
|
+
await (0, assertions_1.assertStatusIn)(response, [200], 'the batch key is returned');
|
|
204
|
+
const json = (await response.json());
|
|
205
|
+
(0, assertions_1.assertRequiredFields)(json, ['batchOperationKey']);
|
|
206
|
+
});
|
|
207
|
+
(0, api_1.test)('rejects a lookup for an absent batch', async ({ api }) => {
|
|
208
|
+
const response = await api.get('/batch-operations/{batchOperationKey}', {
|
|
209
|
+
params: { batchOperationKey: keys_1.ABSENT_KEY },
|
|
210
|
+
tenant: 'none',
|
|
211
|
+
});
|
|
212
|
+
await (0, assertions_1.assertNotFound)(response);
|
|
213
|
+
});
|
|
214
|
+
(0, api_1.test)('rejects suspending an absent batch', async ({ api }) => {
|
|
215
|
+
const response = await api.post('/batch-operations/{batchOperationKey}/suspension', {
|
|
216
|
+
params: { batchOperationKey: keys_1.ABSENT_ROUTABLE_KEY },
|
|
217
|
+
body: {},
|
|
218
|
+
tenant: 'none',
|
|
219
|
+
});
|
|
220
|
+
await (0, assertions_1.assertNotFound)(response);
|
|
221
|
+
});
|
|
222
|
+
(0, api_1.test)('rejects resuming an absent batch', async ({ api }) => {
|
|
223
|
+
const response = await api.post('/batch-operations/{batchOperationKey}/resumption', {
|
|
224
|
+
params: { batchOperationKey: keys_1.ABSENT_ROUTABLE_KEY },
|
|
225
|
+
body: {},
|
|
226
|
+
tenant: 'none',
|
|
227
|
+
});
|
|
228
|
+
await (0, assertions_1.assertNotFound)(response);
|
|
229
|
+
});
|
|
230
|
+
(0, api_1.test)('rejects cancelling an absent batch', async ({ api }) => {
|
|
231
|
+
const response = await api.post('/batch-operations/{batchOperationKey}/cancellation', {
|
|
232
|
+
params: { batchOperationKey: keys_1.ABSENT_ROUTABLE_KEY },
|
|
233
|
+
body: {},
|
|
234
|
+
tenant: 'none',
|
|
235
|
+
});
|
|
236
|
+
await (0, assertions_1.assertNotFound)(response);
|
|
237
|
+
});
|
|
238
|
+
(0, api_1.test)('reports an unknown batch operation id as not found', async ({ api }) => {
|
|
239
|
+
// A batch operation is addressed by a string id, not a numeric key, so
|
|
240
|
+
// there is no "malformed" shape to reject here — any string the cluster
|
|
241
|
+
// does not hold is simply absent.
|
|
242
|
+
const response = await api.get('/batch-operations/{batchOperationKey}', {
|
|
243
|
+
params: { batchOperationKey: 'not-a-key' },
|
|
244
|
+
tenant: 'none',
|
|
245
|
+
});
|
|
246
|
+
await (0, assertions_1.assertNotFound)(response, /not found/i);
|
|
247
|
+
});
|
|
248
|
+
(0, api_1.test)('rejects a cancellation batch with an empty filter', async ({ api }) => {
|
|
249
|
+
const response = await api.post('/process-instances/cancellation', {
|
|
250
|
+
body: { filter: {} },
|
|
251
|
+
tenant: 'none',
|
|
252
|
+
});
|
|
253
|
+
// An unscoped batch would cancel every instance on the cluster. The shell
|
|
254
|
+
// suite asserted `200 202 400` here, so it accepted that outcome.
|
|
255
|
+
await (0, assertions_1.assertBadRequest)(response);
|
|
256
|
+
});
|
|
257
|
+
(0, api_1.test)('rejects a batch search with an unknown state filter', async ({ api }) => {
|
|
258
|
+
const response = await api.post('/batch-operations/search', {
|
|
259
|
+
body: { filter: { state: 'NOT_A_STATE' } },
|
|
260
|
+
tenant: 'none',
|
|
261
|
+
});
|
|
262
|
+
await (0, assertions_1.assertBadRequest)(response);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const api_1 = require("../../../../fixtures/api");
|
|
4
|
+
const assertions_1 = require("../../../../utils/api/assertions");
|
|
5
|
+
const test_1 = require("@playwright/test");
|
|
6
|
+
const config_1 = require("../../../../utils/api/config");
|
|
7
|
+
const capabilities_1 = require("../../../../utils/api/capabilities");
|
|
8
|
+
api_1.test.describe('Cluster and authentication', () => {
|
|
9
|
+
(0, api_1.test)('returns the authenticated principal', async ({ api }) => {
|
|
10
|
+
api_1.test.skip(capabilities_1.unauthenticatedAccessAllowed !== false, capabilities_1.unauthenticatedAccessAllowed === false
|
|
11
|
+
? ''
|
|
12
|
+
: capabilities_1.unauthenticatedAccessAllowed);
|
|
13
|
+
const response = await api.get('/authentication/me', { tenant: 'none' });
|
|
14
|
+
const me = (await (0, assertions_1.assertOk)(response));
|
|
15
|
+
// The shell suite accepted any 200 here and read the user id with a
|
|
16
|
+
// `// empty` fallback, so an empty body passed. What the endpoint
|
|
17
|
+
// guarantees is `CamundaUserResult`, whose required fields describe what
|
|
18
|
+
// the caller may do — assert those.
|
|
19
|
+
(0, assertions_1.assertRequiredFields)(me, [
|
|
20
|
+
'authorizedComponents',
|
|
21
|
+
'tenants',
|
|
22
|
+
'groups',
|
|
23
|
+
'roles',
|
|
24
|
+
'canLogout',
|
|
25
|
+
]);
|
|
26
|
+
(0, test_1.expect)(Array.isArray(me.authorizedComponents)).toBe(true);
|
|
27
|
+
(0, test_1.expect)(Array.isArray(me.tenants)).toBe(true);
|
|
28
|
+
// `username` names a *user*. Authenticating with client credentials —
|
|
29
|
+
// which is how every OIDC cluster runs this suite — means the principal
|
|
30
|
+
// is a client and there is no username to return, so requiring one here
|
|
31
|
+
// asserted a property of the auth method rather than of the endpoint.
|
|
32
|
+
if (config_1.apiConfig.usersApiEnabled) {
|
|
33
|
+
(0, test_1.expect)(me.username, 'a user principal is identified by name').toBeTruthy();
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
// Not gated on `unauthenticatedAccessAllowed`: /v2/authentication/me
|
|
37
|
+
// refuses an anonymous caller even where the rest of the tree does not,
|
|
38
|
+
// so this one holds on an open cluster too and passed on c8Run.
|
|
39
|
+
(0, api_1.test)('rejects an unauthenticated principal lookup', async ({ api }) => {
|
|
40
|
+
const response = await api.get('/authentication/me', {
|
|
41
|
+
tenant: 'none',
|
|
42
|
+
authorization: null,
|
|
43
|
+
});
|
|
44
|
+
await (0, assertions_1.assertUnauthorized)(response);
|
|
45
|
+
});
|
|
46
|
+
(0, api_1.test)('reports cluster topology with at least one broker', async ({ api }) => {
|
|
47
|
+
const response = await api.get('/topology', { tenant: 'none' });
|
|
48
|
+
const topology = (await (0, assertions_1.assertOk)(response));
|
|
49
|
+
(0, assertions_1.assertRequiredFields)(topology, ['brokers', 'clusterSize']);
|
|
50
|
+
(0, test_1.expect)(topology.brokers?.length ?? 0).toBeGreaterThan(0);
|
|
51
|
+
(0, test_1.expect)(topology.clusterSize).toBeGreaterThan(0);
|
|
52
|
+
for (const broker of topology.brokers ?? []) {
|
|
53
|
+
(0, assertions_1.assertRequiredFields)(broker, ['nodeId', 'host', 'port', 'partitions']);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
(0, api_1.test)('reports the cluster licence state', async ({ api }) => {
|
|
57
|
+
const response = await api.get('/license', { tenant: 'none' });
|
|
58
|
+
const license = (await (0, assertions_1.assertOk)(response));
|
|
59
|
+
(0, assertions_1.assertRequiredFields)(license, ['validLicense', 'licenseType']);
|
|
60
|
+
(0, test_1.expect)(typeof license.validLicense).toBe('boolean');
|
|
61
|
+
});
|
|
62
|
+
(0, api_1.test)('reports readiness on the status endpoint', async ({ api }) => {
|
|
63
|
+
const response = await api.get('/status', { tenant: 'none' });
|
|
64
|
+
await (0, assertions_1.assertNoContent)(response);
|
|
65
|
+
});
|
|
66
|
+
(0, api_1.test)('rejects an unauthenticated topology request', async ({ api }) => {
|
|
67
|
+
api_1.test.skip(capabilities_1.unauthenticatedAccessAllowed !== false, capabilities_1.unauthenticatedAccessAllowed === false
|
|
68
|
+
? ''
|
|
69
|
+
: capabilities_1.unauthenticatedAccessAllowed);
|
|
70
|
+
const response = await api.get('/topology', {
|
|
71
|
+
tenant: 'none',
|
|
72
|
+
authorization: null,
|
|
73
|
+
});
|
|
74
|
+
await (0, assertions_1.assertUnauthorized)(response);
|
|
75
|
+
});
|
|
76
|
+
(0, api_1.test)('rejects a request bearing a malformed token', async ({ api }) => {
|
|
77
|
+
api_1.test.skip(capabilities_1.unauthenticatedAccessAllowed !== false, capabilities_1.unauthenticatedAccessAllowed === false
|
|
78
|
+
? ''
|
|
79
|
+
: capabilities_1.unauthenticatedAccessAllowed);
|
|
80
|
+
const response = await api.get('/topology', {
|
|
81
|
+
tenant: 'none',
|
|
82
|
+
authorization: 'Bearer not-a-real-token',
|
|
83
|
+
});
|
|
84
|
+
await (0, assertions_1.assertStatus)(response, 401);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|