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