@camunda/e2e-test-suite 0.0.1242 → 0.0.1244
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/ModelerCreatePage.js +5 -4
- 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,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Cluster clock endpoints.
|
|
4
|
+
*
|
|
5
|
+
* In its own file because it is its own Playwright project: pinning the clock
|
|
6
|
+
* moves time for **every** caller on the cluster, so these tests cannot share
|
|
7
|
+
* the parallel window with the rest of the suite. See the `clock` project in
|
|
8
|
+
* `playwright.api.config.ts` for why a project is what isolates them and what
|
|
9
|
+
* that costs.
|
|
10
|
+
*
|
|
11
|
+
* `mode: 'serial'` is still needed, but only for the reason it can deliver:
|
|
12
|
+
* ordering the tests *within this file* so two of them do not pin and reset
|
|
13
|
+
* against each other.
|
|
14
|
+
*
|
|
15
|
+
* The shell suite ran these from inside its EXIT trap, after the process exit
|
|
16
|
+
* code had already been computed — so a clock assertion could not fail the
|
|
17
|
+
* run, and a crash before the trap left the cluster clock pinned for whatever
|
|
18
|
+
* ran next.
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
const api_1 = require("../../../../fixtures/api");
|
|
22
|
+
const assertions_1 = require("../../../../utils/api/assertions");
|
|
23
|
+
const PINNED = Date.parse('2029-01-01T00:00:00Z');
|
|
24
|
+
api_1.test.describe('Clock', () => {
|
|
25
|
+
api_1.test.describe.configure({ mode: 'serial' });
|
|
26
|
+
// Asserted, not fire-and-forget: a reset that answers 4xx/5xx leaves the
|
|
27
|
+
// cluster pinned for everything that follows, and an unchecked response
|
|
28
|
+
// would let the run continue and fail somewhere with no connection to the
|
|
29
|
+
// clock.
|
|
30
|
+
api_1.test.afterEach(async ({ api }) => {
|
|
31
|
+
const response = await api.post('/clock/reset', {
|
|
32
|
+
body: {},
|
|
33
|
+
tenant: 'none',
|
|
34
|
+
});
|
|
35
|
+
await (0, assertions_1.assertNoContent)(response);
|
|
36
|
+
});
|
|
37
|
+
(0, api_1.test)('pins the cluster clock to a timestamp', async ({ api }) => {
|
|
38
|
+
const response = await api.put('/clock', {
|
|
39
|
+
body: { timestamp: PINNED },
|
|
40
|
+
tenant: 'none',
|
|
41
|
+
});
|
|
42
|
+
await (0, assertions_1.assertNoContent)(response);
|
|
43
|
+
});
|
|
44
|
+
(0, api_1.test)('resets the cluster clock', async ({ api }) => {
|
|
45
|
+
await api.put('/clock', {
|
|
46
|
+
body: { timestamp: PINNED },
|
|
47
|
+
tenant: 'none',
|
|
48
|
+
});
|
|
49
|
+
const response = await api.post('/clock/reset', {
|
|
50
|
+
body: {},
|
|
51
|
+
tenant: 'none',
|
|
52
|
+
});
|
|
53
|
+
await (0, assertions_1.assertNoContent)(response);
|
|
54
|
+
});
|
|
55
|
+
(0, api_1.test)('rejects a malformed timestamp', async ({ api }) => {
|
|
56
|
+
const response = await api.put('/clock', {
|
|
57
|
+
body: { timestamp: 'not-a-timestamp' },
|
|
58
|
+
tenant: 'none',
|
|
59
|
+
});
|
|
60
|
+
await (0, assertions_1.assertBadRequest)(response);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,183 @@
|
|
|
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 bpmn_1 = require("../../../../utils/api/bpmn");
|
|
7
|
+
const keys_1 = require("../../../../utils/api/keys");
|
|
8
|
+
const config_1 = require("../../../../utils/api/config");
|
|
9
|
+
const capabilities_1 = require("../../../../utils/api/capabilities");
|
|
10
|
+
api_1.test.describe('Usage metrics', () => {
|
|
11
|
+
const startTime = '2020-01-01T00:00:00.000Z';
|
|
12
|
+
const endTime = '2030-01-01T00:00:00.000Z';
|
|
13
|
+
(0, api_1.test)('reports usage metrics for a time range', async ({ api }) => {
|
|
14
|
+
const response = await api.get('/system/usage-metrics', {
|
|
15
|
+
tenant: 'none',
|
|
16
|
+
query: { startTime, endTime },
|
|
17
|
+
});
|
|
18
|
+
const json = (await (0, assertions_1.assertOk)(response));
|
|
19
|
+
(0, assertions_1.assertRequiredFields)(json, ['processInstances', 'decisionInstances']);
|
|
20
|
+
});
|
|
21
|
+
(0, api_1.test)('rejects a request with no time range', async ({ api }) => {
|
|
22
|
+
const response = await api.get('/system/usage-metrics', { tenant: 'none' });
|
|
23
|
+
await (0, assertions_1.assertBadRequest)(response);
|
|
24
|
+
});
|
|
25
|
+
(0, api_1.test)('rejects a malformed timestamp', async ({ api }) => {
|
|
26
|
+
const response = await api.get('/system/usage-metrics', {
|
|
27
|
+
tenant: 'none',
|
|
28
|
+
query: { startTime: 'not-a-date', endTime },
|
|
29
|
+
});
|
|
30
|
+
await (0, assertions_1.assertBadRequest)(response);
|
|
31
|
+
});
|
|
32
|
+
(0, api_1.test)('rejects a range whose end precedes its start', async ({ api }) => {
|
|
33
|
+
const response = await api.get('/system/usage-metrics', {
|
|
34
|
+
tenant: 'none',
|
|
35
|
+
query: { startTime: endTime, endTime: startTime },
|
|
36
|
+
});
|
|
37
|
+
await (0, assertions_1.assertBadRequest)(response);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
// The Clock tests used to live here. They moved to `clock-api-tests.spec.ts`
|
|
41
|
+
// because they pin cluster-wide state and need their own Playwright project to
|
|
42
|
+
// stay out of the parallel window — `mode: 'serial'` here only ordered them
|
|
43
|
+
// against each other, never against the other files.
|
|
44
|
+
api_1.test.describe('Identity read models', () => {
|
|
45
|
+
(0, api_1.test)('lists tenants', async ({ api }) => {
|
|
46
|
+
const response = await api.post('/tenants/search', {
|
|
47
|
+
body: { filter: {}, page: { limit: 10 } },
|
|
48
|
+
tenant: 'none',
|
|
49
|
+
});
|
|
50
|
+
await (0, assertions_1.assertPaginated)(response);
|
|
51
|
+
});
|
|
52
|
+
(0, api_1.test)('lists roles', async ({ api }) => {
|
|
53
|
+
const response = await api.post('/roles/search', {
|
|
54
|
+
body: { filter: {}, page: { limit: 10 } },
|
|
55
|
+
tenant: 'none',
|
|
56
|
+
});
|
|
57
|
+
await (0, assertions_1.assertPaginated)(response);
|
|
58
|
+
});
|
|
59
|
+
(0, api_1.test)('lists groups', async ({ api }) => {
|
|
60
|
+
const response = await api.post('/groups/search', {
|
|
61
|
+
body: { filter: {}, page: { limit: 10 } },
|
|
62
|
+
tenant: 'none',
|
|
63
|
+
});
|
|
64
|
+
await (0, assertions_1.assertPaginated)(response);
|
|
65
|
+
});
|
|
66
|
+
(0, api_1.test)('lists mapping rules', async ({ api }) => {
|
|
67
|
+
const response = await api.post('/mapping-rules/search', {
|
|
68
|
+
body: { filter: {}, page: { limit: 10 } },
|
|
69
|
+
tenant: 'none',
|
|
70
|
+
});
|
|
71
|
+
await (0, assertions_1.assertPaginated)(response);
|
|
72
|
+
});
|
|
73
|
+
(0, api_1.test)('lists users', async ({ api }) => {
|
|
74
|
+
api_1.test.skip(capabilities_1.usersApiUnavailable !== false, capabilities_1.usersApiUnavailable === false ? '' : capabilities_1.usersApiUnavailable);
|
|
75
|
+
const response = await api.post('/users/search', {
|
|
76
|
+
body: { filter: {}, page: { limit: 10 } },
|
|
77
|
+
tenant: 'none',
|
|
78
|
+
});
|
|
79
|
+
await (0, assertions_1.assertPaginated)(response);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
/**
|
|
83
|
+
* Holds the capability gate used across the identity specs to its word.
|
|
84
|
+
*
|
|
85
|
+
* Whichever way the cluster is configured, exactly one of these two runs and
|
|
86
|
+
* asserts what that configuration means for `/v2/users/**`. If the gate were
|
|
87
|
+
* ever wrong — a cluster that does serve the API but is read as OIDC — the
|
|
88
|
+
* matching test here fails, so a mis-set gate costs a red run rather than
|
|
89
|
+
* silently removing the user coverage it controls.
|
|
90
|
+
*/
|
|
91
|
+
api_1.test.describe('Users API availability', () => {
|
|
92
|
+
(0, api_1.test)('is closed with a stated reason when the cluster is in OIDC mode', async ({ api, }) => {
|
|
93
|
+
api_1.test.skip(capabilities_1.usersApiUnavailable === false, 'the cluster serves the Users API');
|
|
94
|
+
const response = await api.post('/users/search', {
|
|
95
|
+
body: { filter: {}, page: { limit: 10 } },
|
|
96
|
+
tenant: 'none',
|
|
97
|
+
});
|
|
98
|
+
await (0, assertions_1.assertForbidden)(response, /users api is disabled/i);
|
|
99
|
+
});
|
|
100
|
+
(0, api_1.test)('is served when the cluster manages users locally', async ({ api }) => {
|
|
101
|
+
api_1.test.skip(capabilities_1.usersApiUnavailable !== false, 'the cluster is in OIDC mode');
|
|
102
|
+
const response = await api.post('/users/search', {
|
|
103
|
+
body: { filter: {}, page: { limit: 10 } },
|
|
104
|
+
tenant: 'none',
|
|
105
|
+
});
|
|
106
|
+
await (0, assertions_1.assertPaginated)(response);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
/**
|
|
110
|
+
* Reports whether this cluster enforces authorization.
|
|
111
|
+
*
|
|
112
|
+
* The authorization *enforcement* spec needs two things: a second principal
|
|
113
|
+
* that holds no roles, and a cluster that actually checks permissions. It
|
|
114
|
+
* gates on the first. This records the second, because the SM QA clusters do
|
|
115
|
+
* not enforce — `test`, the client this suite authenticates as, is not one of
|
|
116
|
+
* the three admin mappings the chart values initialise, yet nothing refuses
|
|
117
|
+
* it. Without this test that fact lives only in a comment, and a cluster that
|
|
118
|
+
* silently started or stopped enforcing would look identical.
|
|
119
|
+
*/
|
|
120
|
+
api_1.test.describe('Authorization enforcement', () => {
|
|
121
|
+
(0, api_1.test)('reports whether the cluster enforces authorization', async ({ api, }, testInfo) => {
|
|
122
|
+
const enabled = await (0, capabilities_1.readAuthorizationsEnabled)(api);
|
|
123
|
+
// `enabled` comes back undefined for two unrelated reasons, and naming
|
|
124
|
+
// the wrong one sends the reader somewhere there is nothing to find.
|
|
125
|
+
// Before 8.10 the endpoint is not published at all. From 8.10 it is, but
|
|
126
|
+
// it answers for the *calling principal*, so a cluster that serves /v2
|
|
127
|
+
// without credentials has no principal for it to answer for: on c8Run
|
|
128
|
+
// `/v2/authentication/me` refuses an anonymous caller even though the
|
|
129
|
+
// rest of the tree does not. Either way enforcement being unreadable is
|
|
130
|
+
// not the same as enforcement being off.
|
|
131
|
+
api_1.test.skip(enabled === undefined, (0, config_1.atLeastVersion)('8.10')
|
|
132
|
+
? 'this cluster does not authenticate the caller, so ' +
|
|
133
|
+
'/v2/authentication/me/authorizations/search has no principal to ' +
|
|
134
|
+
'report for — the enforcement state cannot be read here, which ' +
|
|
135
|
+
'leaves this deployment without the safety net this test is (see ' +
|
|
136
|
+
'tests/api/README.md)'
|
|
137
|
+
: 'the cluster does not publish ' +
|
|
138
|
+
'/v2/authentication/me/authorizations/search (pre-8.10)');
|
|
139
|
+
testInfo.annotations.push({
|
|
140
|
+
type: 'authorization enforcement',
|
|
141
|
+
description: enabled
|
|
142
|
+
? 'enabled — an unprivileged principal would be refused, so the ' +
|
|
143
|
+
'enforcement spec is meaningful once one is configured'
|
|
144
|
+
: 'DISABLED — every principal is permitted, so authorization ' +
|
|
145
|
+
'enforcement cannot be observed on this cluster no matter what ' +
|
|
146
|
+
'second principal is supplied (see tests/api/README.md)',
|
|
147
|
+
});
|
|
148
|
+
if (!enabled) {
|
|
149
|
+
console.warn('Authorization enforcement is DISABLED on this cluster: every ' +
|
|
150
|
+
'principal is permitted, so the enforcement spec could not observe ' +
|
|
151
|
+
'a refusal even with SECONDARY_CLIENT_ID set. See ' +
|
|
152
|
+
'tests/api/README.md for what closing that gap requires.');
|
|
153
|
+
}
|
|
154
|
+
// Belt and braces: `readAuthorizationsEnabled` already throws on a 200
|
|
155
|
+
// whose `authorizationsEnabled` is not a boolean, and on any status that
|
|
156
|
+
// is neither the answer nor one of the two that mean the cluster cannot
|
|
157
|
+
// give it. This keeps the contract visible in the test that depends on it.
|
|
158
|
+
(0, test_1.expect)(typeof enabled).toBe('boolean');
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
api_1.test.describe('Ad-hoc sub-process activities', () => {
|
|
162
|
+
(0, api_1.test)('rejects activating activities on an absent sub-process', async ({ api, }) => {
|
|
163
|
+
const response = await api.post('/element-instances/ad-hoc-activities/{adHocSubProcessInstanceKey}/activation', {
|
|
164
|
+
params: { adHocSubProcessInstanceKey: keys_1.ABSENT_ROUTABLE_KEY },
|
|
165
|
+
body: { elements: [{ elementId: (0, bpmn_1.uniqueId)('activity') }] },
|
|
166
|
+
tenant: 'none',
|
|
167
|
+
});
|
|
168
|
+
await (0, assertions_1.assertStatusIn)(response, [404], 'an absent ad-hoc sub-process instance is reported as not found');
|
|
169
|
+
});
|
|
170
|
+
(0, api_1.test)('rejects an activation request carrying no elements', async ({ api }) => {
|
|
171
|
+
const response = await api.post('/element-instances/ad-hoc-activities/{adHocSubProcessInstanceKey}/activation', {
|
|
172
|
+
params: { adHocSubProcessInstanceKey: keys_1.ABSENT_ROUTABLE_KEY },
|
|
173
|
+
body: {},
|
|
174
|
+
tenant: 'none',
|
|
175
|
+
});
|
|
176
|
+
// Validation precedes the existence check, so this is a 400 rather than
|
|
177
|
+
// the 404 above.
|
|
178
|
+
(0, test_1.expect)([400, 404]).toContain(response.status());
|
|
179
|
+
if (response.status() === 400) {
|
|
180
|
+
await (0, assertions_1.assertBadRequest)(response);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,258 @@
|
|
|
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 capabilities_1 = require("../../../../utils/api/capabilities");
|
|
10
|
+
const keys_1 = require("../../../../utils/api/keys");
|
|
11
|
+
/** Starts a process holding one Zeebe user task and returns that task's key. */
|
|
12
|
+
async function startUserTask(api, resources) {
|
|
13
|
+
const processId = (0, bpmn_1.uniqueId)('user-task-api');
|
|
14
|
+
await (0, deployments_1.deployProcess)(api, resources, processId, (0, bpmn_1.linearProcess)(processId, bpmn_1.element.userTask()));
|
|
15
|
+
const start = await api.post('/process-instances', {
|
|
16
|
+
body: { processDefinitionId: processId },
|
|
17
|
+
tenant: 'root',
|
|
18
|
+
});
|
|
19
|
+
await (0, assertions_1.assertStatusIn)(start, [200], 'instance creation is synchronous');
|
|
20
|
+
const instance = (await start.json());
|
|
21
|
+
const processInstanceKey = String(instance.processInstanceKey);
|
|
22
|
+
let tasks;
|
|
23
|
+
try {
|
|
24
|
+
tasks = await (0, eventually_1.eventuallyGet)(async () => {
|
|
25
|
+
const response = await api.post('/user-tasks/search', {
|
|
26
|
+
body: { filter: { processInstanceKey }, page: { limit: 10 } },
|
|
27
|
+
});
|
|
28
|
+
const page = await (0, assertions_1.assertPaginated)(response);
|
|
29
|
+
return page.items;
|
|
30
|
+
}, (items) => items.length > 0, { description: `user tasks for instance ${processInstanceKey}` });
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
// An empty result cannot say *why* it is empty, and that is the whole
|
|
34
|
+
// question when this fails: did the engine never create the task (our
|
|
35
|
+
// model is wrong), or did it create one that was never exported (the
|
|
36
|
+
// cluster is wrong)? The element instances answer it — they come from a
|
|
37
|
+
// different index, so a `task` element sitting in the instance while the
|
|
38
|
+
// user-task search stays empty means the record exists and only its
|
|
39
|
+
// export is missing.
|
|
40
|
+
throw new Error(`${error instanceof Error ? error.message : String(error)}\n` +
|
|
41
|
+
(await describeInstance(api, processInstanceKey, processId)));
|
|
42
|
+
}
|
|
43
|
+
return { userTaskKey: String(tasks[0].userTaskKey), processInstanceKey };
|
|
44
|
+
}
|
|
45
|
+
/** Where the instance actually got to, for the failure message above. */
|
|
46
|
+
async function describeInstance(api, processInstanceKey, processId) {
|
|
47
|
+
try {
|
|
48
|
+
const instances = await api.post('/process-instances/search', {
|
|
49
|
+
body: { filter: { processInstanceKey } },
|
|
50
|
+
});
|
|
51
|
+
const elements = await api.post('/element-instances/search', {
|
|
52
|
+
body: { filter: { processInstanceKey }, page: { limit: 20 } },
|
|
53
|
+
});
|
|
54
|
+
// The definition was deployed moments before the instance started, so it
|
|
55
|
+
// is the control: both travel the same exporter to the same secondary
|
|
56
|
+
// storage. A definition that arrived while its instance did not is a
|
|
57
|
+
// fault specific to this instance; neither arriving means export was
|
|
58
|
+
// behind or stopped for everything, which is a cluster fault rather than
|
|
59
|
+
// anything these tests can assert around. Run 35696766504 failed 12 tests
|
|
60
|
+
// without recording which of the two it was.
|
|
61
|
+
const definitions = await api.post('/process-definitions/search', {
|
|
62
|
+
body: { filter: { processDefinitionId: processId }, page: { limit: 1 } },
|
|
63
|
+
});
|
|
64
|
+
const instanceJson = (await instances.json());
|
|
65
|
+
const elementJson = (await elements.json());
|
|
66
|
+
const definitionJson = (await definitions.json());
|
|
67
|
+
const state = instanceJson.items?.[0]?.state ?? '<no instance found>';
|
|
68
|
+
const active = (elementJson.items ?? []).map((item) => `${item.elementId}:${item.type}=${item.state}`);
|
|
69
|
+
const exported = (definitionJson.items ?? []).length > 0;
|
|
70
|
+
return (`Instance ${processInstanceKey} state=${state}; ` +
|
|
71
|
+
`element instances: ${active.length > 0 ? active.join(', ') : '<none>'}; ` +
|
|
72
|
+
`its definition ${processId} ` +
|
|
73
|
+
`${exported
|
|
74
|
+
? 'DID reach secondary storage (export is live, so this instance is the fault)'
|
|
75
|
+
: 'did NOT reach secondary storage either (export is behind or stopped cluster-wide)'}`);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
return `Could not describe instance ${processInstanceKey}: ${error instanceof Error ? error.message : String(error)}`;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
api_1.test.describe('User tasks', () => {
|
|
82
|
+
(0, api_1.test)('finds the task created by a running instance', async ({ api, resources, }) => {
|
|
83
|
+
const { userTaskKey, processInstanceKey } = await startUserTask(api, resources);
|
|
84
|
+
resources.track('processInstance', processInstanceKey);
|
|
85
|
+
const response = await api.post('/user-tasks/search', {
|
|
86
|
+
body: { filter: { processInstanceKey } },
|
|
87
|
+
});
|
|
88
|
+
const page = await (0, assertions_1.assertPaginated)(response, { exactItems: 1 });
|
|
89
|
+
const task = page.items[0];
|
|
90
|
+
(0, test_1.expect)(String(task.userTaskKey)).toBe(userTaskKey);
|
|
91
|
+
(0, test_1.expect)(task.state).toBe('CREATED');
|
|
92
|
+
});
|
|
93
|
+
(0, api_1.test)('returns a task by key', async ({ api, resources }) => {
|
|
94
|
+
const { userTaskKey, processInstanceKey } = await startUserTask(api, resources);
|
|
95
|
+
resources.track('processInstance', processInstanceKey);
|
|
96
|
+
const response = await api.get('/user-tasks/{userTaskKey}', {
|
|
97
|
+
params: { userTaskKey },
|
|
98
|
+
});
|
|
99
|
+
const task = (await (0, assertions_1.assertOk)(response));
|
|
100
|
+
(0, assertions_1.assertRequiredFields)(task, ['userTaskKey', 'state', 'elementId']);
|
|
101
|
+
(0, test_1.expect)(task.elementId).toBe('task');
|
|
102
|
+
(0, test_1.expect)(task.state).toBe('CREATED');
|
|
103
|
+
});
|
|
104
|
+
(0, api_1.test)('assigns a task to a user', async ({ api, resources }) => {
|
|
105
|
+
const { userTaskKey, processInstanceKey } = await startUserTask(api, resources);
|
|
106
|
+
resources.track('processInstance', processInstanceKey);
|
|
107
|
+
const response = await api.post('/user-tasks/{userTaskKey}/assignment', {
|
|
108
|
+
params: { userTaskKey },
|
|
109
|
+
body: { assignee: 'demo', allowOverride: true },
|
|
110
|
+
tenant: 'none',
|
|
111
|
+
});
|
|
112
|
+
await (0, assertions_1.assertStatusIn)(response, [204], 'assignment is acknowledged');
|
|
113
|
+
await (0, eventually_1.eventually)(async () => {
|
|
114
|
+
const lookup = await api.get('/user-tasks/{userTaskKey}', {
|
|
115
|
+
params: { userTaskKey },
|
|
116
|
+
});
|
|
117
|
+
const task = (await (0, assertions_1.assertOk)(lookup));
|
|
118
|
+
// The shell suite asserted the 204 and moved on, so an assignment that
|
|
119
|
+
// was accepted but never applied counted as a pass.
|
|
120
|
+
(0, test_1.expect)(task.assignee).toBe('demo');
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
(0, api_1.test)('unassigns an assigned task', async ({ api, resources }) => {
|
|
124
|
+
const { userTaskKey, processInstanceKey } = await startUserTask(api, resources);
|
|
125
|
+
resources.track('processInstance', processInstanceKey);
|
|
126
|
+
await api.post('/user-tasks/{userTaskKey}/assignment', {
|
|
127
|
+
params: { userTaskKey },
|
|
128
|
+
body: { assignee: 'demo', allowOverride: true },
|
|
129
|
+
tenant: 'none',
|
|
130
|
+
});
|
|
131
|
+
const response = await api.delete('/user-tasks/{userTaskKey}/assignee', {
|
|
132
|
+
params: { userTaskKey },
|
|
133
|
+
tenant: 'none',
|
|
134
|
+
});
|
|
135
|
+
await (0, assertions_1.assertStatusIn)(response, [204], 'unassignment is acknowledged');
|
|
136
|
+
await (0, eventually_1.eventually)(async () => {
|
|
137
|
+
const lookup = await api.get('/user-tasks/{userTaskKey}', {
|
|
138
|
+
params: { userTaskKey },
|
|
139
|
+
});
|
|
140
|
+
const task = (await (0, assertions_1.assertOk)(lookup));
|
|
141
|
+
(0, test_1.expect)(task.assignee ?? null).toBeNull();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
(0, api_1.test)('updates a task through a patch', async ({ api, resources }) => {
|
|
145
|
+
const { userTaskKey, processInstanceKey } = await startUserTask(api, resources);
|
|
146
|
+
resources.track('processInstance', processInstanceKey);
|
|
147
|
+
const response = await api.patch('/user-tasks/{userTaskKey}', {
|
|
148
|
+
params: { userTaskKey },
|
|
149
|
+
body: { changeset: { priority: 75 } },
|
|
150
|
+
tenant: 'none',
|
|
151
|
+
});
|
|
152
|
+
await (0, assertions_1.assertStatusIn)(response, [204], 'the patch is acknowledged');
|
|
153
|
+
await (0, eventually_1.eventually)(async () => {
|
|
154
|
+
const lookup = await api.get('/user-tasks/{userTaskKey}', {
|
|
155
|
+
params: { userTaskKey },
|
|
156
|
+
});
|
|
157
|
+
const task = (await (0, assertions_1.assertOk)(lookup));
|
|
158
|
+
(0, test_1.expect)(task.priority).toBe(75);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
(0, api_1.test)('completes a task and finishes the instance', async ({ api, resources, }) => {
|
|
162
|
+
const { userTaskKey, processInstanceKey } = await startUserTask(api, resources);
|
|
163
|
+
const response = await api.post('/user-tasks/{userTaskKey}/completion', {
|
|
164
|
+
params: { userTaskKey },
|
|
165
|
+
body: { variables: { approved: true } },
|
|
166
|
+
tenant: 'none',
|
|
167
|
+
});
|
|
168
|
+
await (0, assertions_1.assertStatusIn)(response, [204], 'completion is acknowledged');
|
|
169
|
+
await (0, eventually_1.eventually)(async () => {
|
|
170
|
+
const search = await api.post('/process-instances/search', {
|
|
171
|
+
body: { filter: { processInstanceKey } },
|
|
172
|
+
});
|
|
173
|
+
const page = await (0, assertions_1.assertPaginated)(search, { exactItems: 1 });
|
|
174
|
+
(0, test_1.expect)(page.items[0].state).toBe('COMPLETED');
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
(0, api_1.test)('searches the variables visible to a task', async ({ api, resources }) => {
|
|
178
|
+
const { userTaskKey, processInstanceKey } = await startUserTask(api, resources);
|
|
179
|
+
resources.track('processInstance', processInstanceKey);
|
|
180
|
+
const response = await api.post('/user-tasks/{userTaskKey}/variables/search', { params: { userTaskKey }, body: { filter: {} } });
|
|
181
|
+
await (0, assertions_1.assertPaginated)(response);
|
|
182
|
+
});
|
|
183
|
+
(0, api_1.test)('reports no linked form for a task without one', async ({ api, resources, }) => {
|
|
184
|
+
const { userTaskKey, processInstanceKey } = await startUserTask(api, resources);
|
|
185
|
+
resources.track('processInstance', processInstanceKey);
|
|
186
|
+
const response = await api.get('/user-tasks/{userTaskKey}/form', {
|
|
187
|
+
params: { userTaskKey },
|
|
188
|
+
});
|
|
189
|
+
await (0, assertions_1.assertStatusIn)(response, [204, 404], 'a task with no linked form reports 204 on 8.8+ and 404 on earlier minors');
|
|
190
|
+
});
|
|
191
|
+
(0, api_1.test)('rejects assigning a completed task', async ({ api, resources }) => {
|
|
192
|
+
const { userTaskKey } = await startUserTask(api, resources);
|
|
193
|
+
await api.post('/user-tasks/{userTaskKey}/completion', {
|
|
194
|
+
params: { userTaskKey },
|
|
195
|
+
body: { variables: {} },
|
|
196
|
+
tenant: 'none',
|
|
197
|
+
});
|
|
198
|
+
await (0, eventually_1.eventually)(async () => {
|
|
199
|
+
const response = await api.post('/user-tasks/{userTaskKey}/assignment', {
|
|
200
|
+
params: { userTaskKey },
|
|
201
|
+
body: { assignee: 'demo', allowOverride: true },
|
|
202
|
+
tenant: 'none',
|
|
203
|
+
});
|
|
204
|
+
await (0, assertions_1.assertStatusIn)(response, [404, 409], 'a completed task is either gone or in an invalid state, depending on minor version');
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
(0, api_1.test)('rejects a lookup for an absent task key', async ({ api }) => {
|
|
208
|
+
const response = await api.get('/user-tasks/{userTaskKey}', {
|
|
209
|
+
params: { userTaskKey: keys_1.ABSENT_KEY },
|
|
210
|
+
});
|
|
211
|
+
await (0, assertions_1.assertNotFound)(response);
|
|
212
|
+
});
|
|
213
|
+
(0, api_1.test)('rejects completing an absent task', async ({ api }) => {
|
|
214
|
+
const response = await api.post('/user-tasks/{userTaskKey}/completion', {
|
|
215
|
+
params: { userTaskKey: keys_1.ABSENT_ROUTABLE_KEY },
|
|
216
|
+
body: { variables: {} },
|
|
217
|
+
tenant: 'none',
|
|
218
|
+
});
|
|
219
|
+
await (0, assertions_1.assertNotFound)(response);
|
|
220
|
+
});
|
|
221
|
+
(0, api_1.test)('rejects a patch carrying no changeset', async ({ api, resources }) => {
|
|
222
|
+
const { userTaskKey, processInstanceKey } = await startUserTask(api, resources);
|
|
223
|
+
resources.track('processInstance', processInstanceKey);
|
|
224
|
+
const response = await api.patch('/user-tasks/{userTaskKey}', {
|
|
225
|
+
params: { userTaskKey },
|
|
226
|
+
body: {},
|
|
227
|
+
tenant: 'none',
|
|
228
|
+
});
|
|
229
|
+
await (0, assertions_1.assertBadRequest)(response);
|
|
230
|
+
});
|
|
231
|
+
(0, api_1.test)('rejects an unauthenticated task search', async ({ api }) => {
|
|
232
|
+
api_1.test.skip(capabilities_1.unauthenticatedAccessAllowed !== false, capabilities_1.unauthenticatedAccessAllowed === false
|
|
233
|
+
? ''
|
|
234
|
+
: capabilities_1.unauthenticatedAccessAllowed);
|
|
235
|
+
const response = await api.post('/user-tasks/search', {
|
|
236
|
+
body: { filter: {} },
|
|
237
|
+
authorization: null,
|
|
238
|
+
});
|
|
239
|
+
await (0, assertions_1.assertUnauthorized)(response);
|
|
240
|
+
});
|
|
241
|
+
(0, api_1.test)('rejects assigning a task that is already assigned without override', async ({ api, resources, }) => {
|
|
242
|
+
const { userTaskKey, processInstanceKey } = await startUserTask(api, resources);
|
|
243
|
+
resources.track('processInstance', processInstanceKey);
|
|
244
|
+
await api.post('/user-tasks/{userTaskKey}/assignment', {
|
|
245
|
+
params: { userTaskKey },
|
|
246
|
+
body: { assignee: 'demo', allowOverride: true },
|
|
247
|
+
tenant: 'none',
|
|
248
|
+
});
|
|
249
|
+
await (0, eventually_1.eventually)(async () => {
|
|
250
|
+
const response = await api.post('/user-tasks/{userTaskKey}/assignment', {
|
|
251
|
+
params: { userTaskKey },
|
|
252
|
+
body: { assignee: 'someone-else', allowOverride: false },
|
|
253
|
+
tenant: 'none',
|
|
254
|
+
});
|
|
255
|
+
await (0, assertions_1.assertConflict)(response);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assertion vocabulary for REST v2 responses.
|
|
3
|
+
*
|
|
4
|
+
* Each helper asserts the status *and* the problem body the API documents for
|
|
5
|
+
* it, so a response that is right by accident fails. The legacy shell suite
|
|
6
|
+
* asserted status codes only, and accepted lists such as `200 201 403` that
|
|
7
|
+
* passed whether the call succeeded or was rejected outright.
|
|
8
|
+
*
|
|
9
|
+
* Where a status genuinely varies — across versions, or because an operation is
|
|
10
|
+
* synchronous on one path and accepted-for-later on another — use
|
|
11
|
+
* `assertStatusIn`, which requires the reason in writing at the call site.
|
|
12
|
+
*/
|
|
13
|
+
import { type APIResponse } from '@playwright/test';
|
|
14
|
+
/** Fields every paginated search response carries. */
|
|
15
|
+
export declare const PAGINATED_FIELDS: readonly ["items", "page"];
|
|
16
|
+
export declare function assertStatus(response: APIResponse, expected: number): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Accepts any of `expected`. `reason` documents why the endpoint may answer
|
|
19
|
+
* with more than one status and is printed on failure — it is required so that
|
|
20
|
+
* widening an assertion is a deliberate, reviewable act.
|
|
21
|
+
*/
|
|
22
|
+
export declare function assertStatusIn(response: APIResponse, expected: readonly number[], reason: string): Promise<void>;
|
|
23
|
+
export declare function assertRequiredFields(payload: unknown, required: readonly string[]): void;
|
|
24
|
+
export declare function assertEqualsForKeys(payload: unknown, expected: Record<string, unknown>, keys: readonly string[]): void;
|
|
25
|
+
export declare function assertOk(response: APIResponse): Promise<unknown>;
|
|
26
|
+
export declare function assertCreated(response: APIResponse): Promise<unknown>;
|
|
27
|
+
export declare function assertNoContent(response: APIResponse): Promise<void>;
|
|
28
|
+
export declare function assertUnauthorized(response: APIResponse): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* A 403 does not carry one fixed problem title. The authorization layer sends
|
|
31
|
+
* `FORBIDDEN`, while the filter that closes `/v2/users/**` on an OIDC cluster
|
|
32
|
+
* sends `Access issue` — both are the API refusing the call. The status and
|
|
33
|
+
* the detail carry the meaning, so the title is matched loosely and the
|
|
34
|
+
* caller pins the specific refusal through `detail`.
|
|
35
|
+
*/
|
|
36
|
+
export declare function assertForbidden(response: APIResponse, detail?: string | RegExp): Promise<void>;
|
|
37
|
+
export declare function assertNotFound(response: APIResponse, detail?: string | RegExp): Promise<void>;
|
|
38
|
+
export declare function assertConflict(response: APIResponse, detail?: string | RegExp): Promise<void>;
|
|
39
|
+
export declare function assertBadRequest(response: APIResponse, detail?: string | RegExp, title?: string | RegExp): Promise<void>;
|
|
40
|
+
export declare function assertPaginated(response: APIResponse, options?: {
|
|
41
|
+
minItems?: number;
|
|
42
|
+
exactItems?: number;
|
|
43
|
+
minTotalItems?: number;
|
|
44
|
+
exactTotalItems?: number;
|
|
45
|
+
}): Promise<{
|
|
46
|
+
items: unknown[];
|
|
47
|
+
page: {
|
|
48
|
+
totalItems: number;
|
|
49
|
+
};
|
|
50
|
+
}>;
|