c8y-nitro 0.10.3 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +2 -0
  2. package/dist/{apiClient-kxv5KHYJ.mjs → apiClient-GMQqcgQe.mjs} +2 -3
  3. package/dist/{autoBootstrap-DUjHuHe_.mjs → autoBootstrap-D4vEytWW.mjs} +6 -8
  4. package/dist/{bootstrap-DrMjty-k.mjs → bootstrap-DpYAhM7C.mjs} +6 -7
  5. package/dist/cli/index.mjs +6 -7
  6. package/dist/{config-BVQHqlgV.mjs → config-Dozrdw3A.mjs} +11 -8
  7. package/dist/{credentials-eKnJ7m9Y.d.mts → credentials-C_3m4LVX.d.mts} +0 -2
  8. package/dist/defaults-CrrCinRJ.mjs +20 -0
  9. package/dist/{c8y-api-DSs1xIIh.mjs → env-fUAhIWsI.mjs} +76 -3
  10. package/dist/{env-file-BrOTmVjA.mjs → env-file-DWbaFlIp.mjs} +1 -2
  11. package/dist/{index-BEFoDy-y.d.mts → index-CmIQfBpj.d.mts} +56 -22
  12. package/dist/index.d.mts +4 -6
  13. package/dist/index.mjs +20 -21
  14. package/dist/{logging-BUYT8Z2V.mjs → logging-B1PuWdiF.mjs} +1 -2
  15. package/dist/{manifest-DV3UW9Dl.mjs → manifest-BhrHDm8Y.mjs} +4 -6
  16. package/dist/{options-54uJmuUV.mjs → options-CgTptIpI.mjs} +5 -6
  17. package/dist/{package-Dqlz6AoZ.mjs → package-y3p_br3i.mjs} +2 -3
  18. package/dist/{roles-CNCObfa3.mjs → roles-DEIHe2Xt.mjs} +4 -5
  19. package/dist/runtime/handlers/invalidateTenantOptions.mjs +2 -408
  20. package/dist/runtime/handlers/liveness-readiness.mjs +1 -2
  21. package/dist/runtime/handlers/openapi.mjs +4 -5
  22. package/dist/runtime/handlers/rewriteRequestUrl.mjs +3 -4
  23. package/dist/runtime/middlewares/c8y-client.mjs +3 -4
  24. package/dist/runtime/middlewares/dev-user.dev.mjs +2 -3
  25. package/dist/runtime/plugins/c8y-variables.dev.mjs +2 -3
  26. package/dist/runtime/plugins/enrich-logs.mjs +2 -3
  27. package/dist/scope-CxEPZ74p.mjs +2763 -0
  28. package/dist/{client-BmSKIcuq.mjs → tenant-0MBgBY_o.mjs} +115 -139
  29. package/dist/testing/setup.d.mts +1 -0
  30. package/dist/testing/setup.mjs +62 -0
  31. package/dist/testing/vite.d.mts +26 -0
  32. package/dist/testing/vite.mjs +58 -0
  33. package/dist/testing-D5VjeD-F.mjs +14 -0
  34. package/dist/testing.d.mts +521 -0
  35. package/dist/testing.mjs +494 -0
  36. package/dist/{typegen-BO_AkyNc.mjs → typegen-Bw4VxFmA.mjs} +4 -5
  37. package/dist/types-D663gk80.d.mts +140 -0
  38. package/dist/types.d.mts +3 -3
  39. package/dist/utils.d.mts +48 -66
  40. package/dist/utils.mjs +16 -27
  41. package/package.json +36 -19
@@ -0,0 +1,494 @@
1
+ import { S as store, c as splitPath, d as RESOURCES, f as createDocument, g as renderManagedObject, h as createManagedObject, i as platform, l as createBinary, m as updateDocument, n as assertActiveTest, o as compileRoute, p as renderDocument, s as matchRoute, t as arrange, u as addExternalId, v as realtime, x as addChild } from "./scope-CxEPZ74p.mjs";
2
+ import { t as DEFAULTS } from "./defaults-CrrCinRJ.mjs";
3
+ import { Buffer } from "node:buffer";
4
+ import { serverFetch as serverFetch$1 } from "nitro";
5
+ // src/testing/data.ts
6
+ var Ref = class {
7
+ label;
8
+ constructor(label) {
9
+ this.label = label;
10
+ }
11
+ get id() {
12
+ const id = store.refs.get(this);
13
+ if (id === void 0) throw new Error(`${this.label} does not exist yet: managed objects created with c8y.device()/c8y.managedObject() exist while a test runs, in the scope they were created in.`);
14
+ return id;
15
+ }
16
+ toJSON() {
17
+ return { id: this.id };
18
+ }
19
+ };
20
+ function resolveId(input) {
21
+ return typeof input === "string" ? input : input.id;
22
+ }
23
+ const RESERVED = [
24
+ "children",
25
+ "assets",
26
+ "additions",
27
+ "externalIds",
28
+ "tenant"
29
+ ];
30
+ function newRef(label) {
31
+ return new Ref(label);
32
+ }
33
+ /**
34
+ * Creates a managed object from factory input and binds it to its ref.
35
+ * @param ref - The ref returned to the test
36
+ * @param input - Factory input
37
+ */
38
+ function createFromInput(ref, input) {
39
+ const data = store.tenant(input.tenant ?? platform.tenant);
40
+ const fragments = Object.fromEntries(Object.entries(input).filter(([key]) => !RESERVED.includes(key)));
41
+ const document = createManagedObject(data, fragments, input.owner ?? platform.user?.userName ?? "test-user");
42
+ store.refs.set(ref, document.id);
43
+ for (const [kind, children] of [
44
+ ["childDevices", input.children],
45
+ ["childAssets", input.assets],
46
+ ["childAdditions", input.additions]
47
+ ]) for (const child of children ?? []) addChild(data, document.id, kind, child.id);
48
+ for (const [type, externalId] of Object.entries(input.externalIds ?? {})) if (addExternalId(data, document.id, type, externalId) instanceof Response) throw new TypeError(`c8y: external id ${type}/${externalId} already exists`);
49
+ }
50
+ /**
51
+ * Creates an inventory binary from seed input and binds it to its ref.
52
+ * @param ref - The ref returned to the test
53
+ * @param input - Binary input
54
+ */
55
+ function createBinaryFromInput(ref, input) {
56
+ const { name, content, contentType, tenant, ...fragments } = input;
57
+ const data = store.tenant(tenant ?? platform.tenant);
58
+ const bytes = typeof content === "string" ? new TextEncoder().encode(content) : content;
59
+ const document = createBinary(data, {
60
+ bytes,
61
+ contentType: contentType ?? (typeof content === "string" ? "text/plain" : "application/octet-stream"),
62
+ name
63
+ }, {
64
+ name,
65
+ ...fragments
66
+ }, platform.user?.userName ?? "test-user");
67
+ store.refs.set(ref, document.id);
68
+ }
69
+ function toContent(content) {
70
+ return content && {
71
+ ...content,
72
+ text: () => new TextDecoder().decode(content.bytes)
73
+ };
74
+ }
75
+ function seedInto(data, collection, input) {
76
+ const resource = Object.values(RESOURCES).find((item) => item.collection === collection);
77
+ const body = {
78
+ time: store.now(),
79
+ ...input
80
+ };
81
+ if ("source" in input) body.source = { id: resolveId(input.source) };
82
+ if (collection === "events" || collection === "alarms") body.text ??= body.type;
83
+ if (collection === "alarms") body.severity ??= "MAJOR";
84
+ if (collection === "operations") {
85
+ delete body.time;
86
+ delete body.status;
87
+ delete body.failureReason;
88
+ body.deviceId = resolveId(input.deviceId);
89
+ }
90
+ const result = createDocument(data, resource, body);
91
+ if (result instanceof Response) throw new TypeError(`c8y.seed(): invalid ${collection.slice(0, -1)}: ${result.message ?? result.status}`);
92
+ if (collection === "operations" && (input.status !== void 0 || input.failureReason !== void 0)) updateDocument(data, resource, result, {
93
+ status: input.status ?? "PENDING",
94
+ failureReason: input.failureReason
95
+ });
96
+ }
97
+ /**
98
+ * Seeds measurements, events, alarms, and operations (validated like API writes).
99
+ * @param seed - Data to seed
100
+ */
101
+ function applySeed(seed) {
102
+ const data = store.tenant(seed.tenant ?? platform.tenant);
103
+ for (const measurement of seed.measurements ?? []) seedInto(data, "measurements", measurement);
104
+ for (const event of seed.events ?? []) seedInto(data, "events", event);
105
+ for (const alarm of seed.alarms ?? []) seedInto(data, "alarms", alarm);
106
+ for (const operation of seed.operations ?? []) seedInto(data, "operations", operation);
107
+ }
108
+ function matches(value, expected) {
109
+ if (expected instanceof Ref) return value?.id === expected.id || value === expected.id;
110
+ if (expected && typeof expected === "object" && !Array.isArray(expected)) return Object.entries(expected).every(([key, item]) => matches(value?.[key], item));
111
+ return value === expected;
112
+ }
113
+ function filterDocuments(documents, filter) {
114
+ if (!filter) return documents;
115
+ if (typeof filter === "function") return documents.filter(filter);
116
+ return documents.filter((document) => Object.entries(filter).every(([key, expected]) => {
117
+ if ((key === "source" || key === "deviceId") && (typeof expected === "string" || expected instanceof Ref)) {
118
+ const id = typeof expected === "string" ? expected : expected.id;
119
+ return (key === "source" ? document.source?.id : document.deviceId) === id;
120
+ }
121
+ return matches(document[key], expected);
122
+ }));
123
+ }
124
+ function collectionView(tenant, collection) {
125
+ const rendered = () => {
126
+ const data = store.tenant(tenant());
127
+ return [...data[collection].values()].map((document) => collection === "managedObjects" ? renderManagedObject(data, document, { withParents: true }) : renderDocument(data, Object.values(RESOURCES).find((item) => item.collection === collection), document));
128
+ };
129
+ return {
130
+ get: (id) => rendered().find((document) => document.id === resolveId(id)),
131
+ find: (filter) => filterDocuments(rendered(), filter)
132
+ };
133
+ }
134
+ function createDb(tenant) {
135
+ return {
136
+ managedObjects: collectionView(tenant, "managedObjects"),
137
+ measurements: collectionView(tenant, "measurements"),
138
+ events: {
139
+ ...collectionView(tenant, "events"),
140
+ attachment: (id) => toContent(store.tenant(tenant()).eventBinaries.get(resolveId(id)))
141
+ },
142
+ binaries: {
143
+ get: (id) => {
144
+ const document = collectionView(tenant, "managedObjects").get(id);
145
+ return document?.c8y_IsBinary !== void 0 ? document : void 0;
146
+ },
147
+ find: (filter) => collectionView(tenant, "managedObjects").find(filter).filter((document) => document.c8y_IsBinary !== void 0),
148
+ content: (id) => toContent(store.tenant(tenant()).binaries.get(resolveId(id)))
149
+ },
150
+ alarms: collectionView(tenant, "alarms"),
151
+ operations: {
152
+ ...collectionView(tenant, "operations"),
153
+ update(id, update) {
154
+ const operation = store.tenant(tenant()).operations.get(resolveId(id));
155
+ if (!operation) throw new TypeError(`c8y.db.operations.update(): no operation ${resolveId(id)}`);
156
+ updateDocument(store.tenant(tenant()), RESOURCES.devicecontrol, operation, update);
157
+ }
158
+ },
159
+ externalIds: { find: (managedObject) => store.tenant(tenant()).externalIds.filter((entry) => managedObject === void 0 || entry.managedObjectId === resolveId(managedObject)).map((entry) => ({ ...entry })) }
160
+ };
161
+ }
162
+ // src/testing/index.ts
163
+ /**
164
+ * The fake Cumulocity tenant your microservice talks to in tests. Requires the
165
+ * `c8yTest()` plugin from `c8y-nitro/testing/vite`.
166
+ *
167
+ * Arrange calls are **scope-aware**: at the top of a file they apply to every
168
+ * test in the file, inside `describe` to the tests of that `describe` (inner
169
+ * scopes override outer ones), and inside a test only to that test. Before
170
+ * every test the tenant is rebuilt from the plugin baseline, the call log is
171
+ * cleared, and c8y-nitro's caches are invalidated, so tests are independent.
172
+ *
173
+ * Arrange the platform as data: users, subscriptions, tenant options,
174
+ * managed objects (`device()`), and measurements, events, alarms, and
175
+ * operations (`seed()`). Your service reads and writes them through the real
176
+ * `@c8y/client`; assert on its responses and on `c8y.db`.
177
+ *
178
+ * @example
179
+ * ```ts
180
+ * import { c8y, definePersona, serverFetch } from 'c8y-nitro/testing'
181
+ *
182
+ * const pump = c8y.device({ name: 'Pump', externalIds: { c8y_Serial: 'SN-1' } }) // seed the platform
183
+ * const visitor = definePersona({ userName: 'visitor', roles: [] })
184
+ *
185
+ * it('returns the pump', async () => {
186
+ * expect((await serverFetch('/pumps/SN-1')).status).toBe(200) // as the default user
187
+ * expect((await serverFetch('/pumps/SN-1', { as: visitor })).status).toBe(403) // as another user
188
+ * })
189
+ * ```
190
+ */
191
+ const c8y = {
192
+ /**
193
+ * Tenant the microservice is deployed in (the bootstrap tenant).
194
+ */
195
+ get tenant() {
196
+ return platform.tenant;
197
+ },
198
+ /**
199
+ * Every request the microservice sent to the platform in this test, in order.
200
+ */
201
+ get calls() {
202
+ return platform.calls;
203
+ },
204
+ /**
205
+ * Requests in this test the fake tenant could not answer (not emulated, and
206
+ * no `override()`). With the default `onUnhandledRequest: 'error'`, the test
207
+ * fails after it ran if this is not empty.
208
+ */
209
+ get unhandled() {
210
+ return platform.unhandled;
211
+ },
212
+ /**
213
+ * The fake tenant's data, to assert on what your service wrote (and to
214
+ * drive operation status like a device would).
215
+ *
216
+ * @example
217
+ * ```ts
218
+ * expect(c8y.db.alarms.find({ source: pump, type: 'overdue' })).toMatchObject([{ severity: 'MAJOR' }])
219
+ * c8y.db.operations.update(operationId, { status: 'SUCCESSFUL' })
220
+ * ```
221
+ */
222
+ db: {
223
+ ...createDb(() => platform.tenant),
224
+ tenant: (tenant) => createDb(() => tenant)
225
+ },
226
+ /**
227
+ * Creates a managed object in the fake tenant (in the current scope).
228
+ * Returns a ref whose `id` is available while a test runs.
229
+ *
230
+ * @example
231
+ * ```ts
232
+ * const gateway = c8y.managedObject({ name: 'Gateway', type: 'c8y_Gateway', children: [c8y.device({ name: 'Pump' })] })
233
+ * ```
234
+ * @param input - Fragments, plus `children`/`assets`/`additions`, `externalIds`, and `tenant`
235
+ */
236
+ managedObject(input = {}) {
237
+ const ref = newRef(`c8y.managedObject(${JSON.stringify(input.name ?? input.type ?? "")})`);
238
+ arrange("c8y.managedObject()", () => createFromInput(ref, input));
239
+ return ref;
240
+ },
241
+ /**
242
+ * Creates a device (a managed object with `c8y_IsDevice`) in the fake tenant.
243
+ *
244
+ * @example
245
+ * ```ts
246
+ * const pump = c8y.device({ name: 'Pump', externalIds: { c8y_Serial: 'SN-1' } })
247
+ * it('…', async () => {
248
+ * await serverFetch(`/devices/${pump.id}`)
249
+ * })
250
+ * ```
251
+ * @param input - Fragments, plus `children`/`assets`/`additions`, `externalIds`, and `tenant`
252
+ */
253
+ device(input = {}) {
254
+ const ref = newRef(`c8y.device(${JSON.stringify(input.name ?? "")})`);
255
+ arrange("c8y.device()", () => createFromInput(ref, {
256
+ c8y_IsDevice: {},
257
+ ...input
258
+ }));
259
+ return ref;
260
+ },
261
+ /**
262
+ * Creates an inventory binary (a file) in the fake tenant (in the current
263
+ * scope). Returns a ref whose `id` is available while a test runs.
264
+ *
265
+ * @example
266
+ * ```ts
267
+ * const manual = c8y.binary({ name: 'manual.txt', content: 'Turn it off and on again.' })
268
+ * ```
269
+ * @param input - Name, content, optional content type, tenant, and fragments
270
+ */
271
+ binary(input) {
272
+ const ref = newRef(`c8y.binary(${JSON.stringify(input.name)})`);
273
+ arrange("c8y.binary()", () => createBinaryFromInput(ref, input));
274
+ return ref;
275
+ },
276
+ /**
277
+ * Seeds measurements, events, alarms, and operations (in the current scope).
278
+ * They are validated like API writes (required fields, enums, existing
279
+ * source), and alarms are deduplicated like on the platform.
280
+ *
281
+ * @example
282
+ * ```ts
283
+ * c8y.seed({
284
+ * measurements: [{ source: pump, type: 'c8y_Temperature', c8y_Temperature: { T: { value: 21.5, unit: 'C' } } }],
285
+ * alarms: [{ source: pump, type: 'c8y_Overheating', severity: 'CRITICAL' }],
286
+ * })
287
+ * ```
288
+ * @param seed - Data to seed
289
+ */
290
+ seed(seed) {
291
+ arrange("c8y.seed()", () => applySeed(seed));
292
+ },
293
+ /**
294
+ * Tenants subscribed to the microservice and their service users.
295
+ */
296
+ subscriptions: {
297
+ /**
298
+ * Current subscriptions.
299
+ */
300
+ list() {
301
+ return platform.subscriptions;
302
+ },
303
+ /**
304
+ * Adds a subscription, or replaces the one of the same tenant.
305
+ * @param subscription - Tenant and service user credentials
306
+ */
307
+ add(subscription) {
308
+ arrange("c8y.subscriptions.add()", () => {
309
+ platform.subscriptions = [...platform.subscriptions.filter((item) => item.tenant !== subscription.tenant), { ...subscription }];
310
+ });
311
+ },
312
+ /**
313
+ * Removes the subscription of a tenant.
314
+ * @param tenant - Tenant id
315
+ */
316
+ remove(tenant) {
317
+ arrange("c8y.subscriptions.remove()", () => {
318
+ platform.subscriptions = platform.subscriptions.filter((item) => item.tenant !== tenant);
319
+ });
320
+ },
321
+ /**
322
+ * Replaces all subscriptions.
323
+ * @param subscriptions - The new subscriptions
324
+ */
325
+ set(subscriptions) {
326
+ arrange("c8y.subscriptions.set()", () => {
327
+ platform.subscriptions = subscriptions.map((subscription) => ({ ...subscription }));
328
+ });
329
+ }
330
+ },
331
+ /**
332
+ * Tenant options stored in the fake platform. These are platform values, not
333
+ * caches: a cached read keeps its value until it expires or is invalidated.
334
+ */
335
+ tenantOptions: {
336
+ /**
337
+ * Sets an option value (merged with the outer scope's options).
338
+ * @param key - Option key
339
+ * @param value - Option value
340
+ * @param scope - Category and tenant, default own category in the deployed tenant
341
+ */
342
+ set(key, value, scope) {
343
+ arrange("c8y.tenantOptions.set()", () => platform.setOption(key, value, scope));
344
+ },
345
+ /**
346
+ * Deletes an option in this scope.
347
+ * @param key - Option key
348
+ * @param scope - Category and tenant
349
+ */
350
+ delete(key, scope) {
351
+ arrange("c8y.tenantOptions.delete()", () => {
352
+ platform.deleteOption(key, scope);
353
+ });
354
+ },
355
+ /**
356
+ * Reads an option value from the platform.
357
+ * @param key - Option key
358
+ * @param scope - Category and tenant
359
+ */
360
+ get(key, scope) {
361
+ return platform.getOption(key, scope);
362
+ }
363
+ },
364
+ /**
365
+ * Overrides the platform's answer for requests matching `pattern`. Use it
366
+ * for failures (errors, timeouts) and for APIs the fake tenant does not
367
+ * emulate; arrange normal data with `device()`/`seed()` instead, so tests do
368
+ * not depend on which endpoints your service calls. Overrides stack: inner
369
+ * scopes and newer overrides win over everything else.
370
+ *
371
+ * Patterns are `"[METHOD ]/path"` with `:param`, `*` (one segment), and `**`
372
+ * (rest of the path).
373
+ *
374
+ * @example
375
+ * ```ts
376
+ * c8y.override('GET /inventory/**', () => Response.json({ message: 'Unavailable' }, { status: 503 }))
377
+ * c8y.override('GET /tenant/statistics/summary', () => ({ deviceCount: 3 }))
378
+ * ```
379
+ * @param pattern - Route pattern
380
+ * @param handler - Returns a `Response`, a JSON value (`200`), or nothing (`204`)
381
+ * @param options - Handler options
382
+ * @param options.once - Answer only the first matching request
383
+ */
384
+ override(pattern, handler, options = {}) {
385
+ arrange("c8y.override()", () => {
386
+ platform.override(pattern, handler, options.once ?? false);
387
+ });
388
+ },
389
+ /**
390
+ * Applies several arrange settings at once, in the current scope. Callers
391
+ * are not part of the state: use `serverFetch(path, { as })`.
392
+ * @param state - Partial tenant state
393
+ */
394
+ apply(state) {
395
+ arrange("c8y.apply()", () => platform.apply(state));
396
+ },
397
+ /**
398
+ * Notification 2.0 (realtime) in the fake tenant. Writes from your service,
399
+ * `c8y.seed()`/`c8y.device()` inside a test, and `c8y.db.operations.update()`
400
+ * are published to the subscriptions your service created.
401
+ */
402
+ notifications: {
403
+ /**
404
+ * Waits until every notification delivered to your service's realtime
405
+ * handlers has been processed (acknowledged), including notifications
406
+ * caused by the handlers' own writes.
407
+ *
408
+ * @example
409
+ * ```ts
410
+ * c8y.seed({ measurements: [{ source: pump, type: 'c8y_Temperature', c8y_Temperature: { T: { value: 95, unit: 'C' } } }] })
411
+ * await c8y.notifications.settled()
412
+ * expect(c8y.db.alarms.find({ source: pump })).toHaveLength(1)
413
+ * ```
414
+ * @param options - Options
415
+ * @param options.timeout - Milliseconds to wait before failing (default 5000)
416
+ */
417
+ async settled(options = {}) {
418
+ const timeout = options.timeout ?? 5e3;
419
+ const start = Date.now();
420
+ const tick = () => new Promise((resolve) => {
421
+ setTimeout(resolve, 1);
422
+ });
423
+ while (Date.now() - start < 500 && realtime.subscriptions.some((item) => ![...realtime.tokens.values()].some((token) => token.tenant === item.tenant && token.subscription === item.subscription))) await tick();
424
+ let quietTicks = 0;
425
+ while (quietTicks < 3) {
426
+ await tick();
427
+ quietTicks = realtime.pending() === 0 ? quietTicks + 1 : 0;
428
+ if (Date.now() - start > timeout) throw new Error(`c8y.notifications.settled(): ${realtime.pending()} notification(s) still not processed after ${timeout} ms`);
429
+ }
430
+ },
431
+ /**
432
+ * Notification 2.0 subscriptions your service created (all tenants).
433
+ */
434
+ subscriptions() {
435
+ return realtime.subscriptions;
436
+ }
437
+ },
438
+ /**
439
+ * Recorded requests matching a route pattern (same syntax as `override()`).
440
+ * @param pattern - Route pattern, e.g. `'GET /tenant/options/**'`
441
+ */
442
+ callsTo(pattern) {
443
+ const route = compileRoute(pattern);
444
+ return platform.calls.filter((call) => matchRoute(route, call.method, splitPath(call.path)));
445
+ }
446
+ };
447
+ /**
448
+ * Types a user that calls your service in tests, so it can be defined once
449
+ * and reused (role names autocomplete). Pass it to `serverFetch(path, { as })`.
450
+ *
451
+ * @example
452
+ * ```ts
453
+ * const operator = definePersona({ userName: 'operator', roles: ['ROLE_ALARM_ADMIN'] })
454
+ * const partner = definePersona({ userName: 'eve', roles: [], tenant: 't99999' })
455
+ * ```
456
+ * @param user - The user
457
+ */
458
+ function definePersona(user) {
459
+ return user;
460
+ }
461
+ /**
462
+ * Calls a route of your service, like `serverFetch()` from `nitro`. With
463
+ * `as`, the request is sent as that user: the platform answers
464
+ * `GET /user/currentUser` with it during the request.
465
+ *
466
+ * @example
467
+ * ```ts
468
+ * await serverFetch('/pumps/SN-1') // default user
469
+ * await serverFetch('/pumps/SN-1/check', { method: 'POST', as: operator }) // a persona
470
+ * await serverFetch('/pumps/SN-1', { as: { userName: 'visitor', roles: [] } }) // inline
471
+ * await serverFetch('/user', { as: null }) // not logged in
472
+ * ```
473
+ * @param path - Route path, including the query
474
+ * @param init - Request options, plus `as`
475
+ */
476
+ async function serverFetch(path, init = {}) {
477
+ if (!("as" in init)) return serverFetch$1(path, init);
478
+ const { as: user, ...requestInit } = init;
479
+ assertActiveTest("serverFetch({ as })");
480
+ const previous = platform.user;
481
+ platform.setUser(user ?? null);
482
+ try {
483
+ const headers = new Headers(requestInit.headers);
484
+ const login = user ? `${user.tenant ?? platform.tenant}/${user.userName}:${DEFAULTS.developmentPassword}` : `${platform.tenant}/anonymous:invalid`;
485
+ headers.set("authorization", `Basic ${Buffer.from(login).toString("base64")}`);
486
+ return await serverFetch$1(path, {
487
+ ...requestInit,
488
+ headers
489
+ });
490
+ } finally {
491
+ platform.setUser(previous);
492
+ }
493
+ }
494
+ export { c8y, definePersona, serverFetch };
@@ -1,11 +1,11 @@
1
- import { i as C8Y_NITRO_TYPES_FILE, r as C8Y_NITRO_TYPES_DIR } from "./manifest-DV3UW9Dl.mjs";
2
- import { t as writeAPIClient } from "./apiClient-kxv5KHYJ.mjs";
3
- import { resolve } from "pathe";
1
+ import { t as writeAPIClient } from "./apiClient-GMQqcgQe.mjs";
2
+ import { i as C8Y_NITRO_TYPES_FILE, r as C8Y_NITRO_TYPES_DIR } from "./manifest-BhrHDm8Y.mjs";
4
3
  import process from "node:process";
4
+ import { resolve } from "pathe";
5
5
  import { defineCommand } from "citty";
6
6
  import { consola } from "consola";
7
7
  import { createNitro } from "nitro/builder";
8
- //#region src/cli/commands/typegen.ts
8
+ // src/cli/commands/typegen.ts
9
9
  var typegen_default = defineCommand({
10
10
  meta: {
11
11
  name: "typegen",
@@ -26,5 +26,4 @@ var typegen_default = defineCommand({
26
26
  consola.info(`Add "${C8Y_NITRO_TYPES_DIR}/*.d.ts" to your tsconfig \`include\` if you have not already.`);
27
27
  }
28
28
  });
29
- //#endregion
30
29
  export { typegen_default as default };
@@ -0,0 +1,140 @@
1
+ import { C8YRole, C8YRoles, C8YTenantOptionKey } from "c8y-nitro/types";
2
+ /**
3
+ * A role: the manifest's custom roles, known platform roles, or any string.
4
+ */
5
+ type C8yTestRole = keyof C8YRoles | C8YRole;
6
+ /**
7
+ * The user calling the microservice (returned by `GET /user/currentUser`).
8
+ */
9
+ interface C8yTestUser {
10
+ userName: string;
11
+ /**
12
+ * Effective roles of the user.
13
+ */
14
+ roles?: C8yTestRole[];
15
+ /**
16
+ * Tenant the user belongs to. Defaults to the tenant the microservice is
17
+ * deployed in. Drives the injected dev user's auth header.
18
+ */
19
+ tenant?: string;
20
+ /**
21
+ * Any other `ICurrentUser` fields (email, firstName, …).
22
+ */
23
+ [field: string]: unknown;
24
+ }
25
+ /**
26
+ * Service user credentials of a tenant subscribed to the microservice.
27
+ */
28
+ interface C8yTestSubscription {
29
+ tenant: string;
30
+ user: string;
31
+ password: string;
32
+ }
33
+ /**
34
+ * What to do with platform requests no handler answers.
35
+ */
36
+ type UnhandledRequestStrategy = 'error' | 'warn' | 'ignore';
37
+ /**
38
+ * State of the fake Cumulocity tenant. Used as the baseline in the
39
+ * `c8yTest()` plugin config, and as partial overrides in
40
+ * `c8y.apply()`.
41
+ */
42
+ interface C8yTestState {
43
+ /**
44
+ * Tenant the microservice is deployed in (the bootstrap tenant).
45
+ * @default 't12345'
46
+ */
47
+ tenant?: string;
48
+ /**
49
+ * User calling the microservice. `null` makes `GET /user/currentUser`
50
+ * answer 401.
51
+ * @default { userName: 'test-user', roles: [] }
52
+ */
53
+ user?: C8yTestUser | null;
54
+ /**
55
+ * Tenants subscribed to the microservice, with their service users.
56
+ * @default one subscription for the deployed tenant
57
+ */
58
+ subscriptions?: C8yTestSubscription[];
59
+ /**
60
+ * Tenant options of the microservice's own settings category, in the deployed tenant.
61
+ */
62
+ tenantOptions?: Partial<Record<C8YTenantOptionKey, string>> & Record<string, string>;
63
+ /**
64
+ * What to do with platform requests that no handler answers. `'error'`
65
+ * fails the test after it ran.
66
+ * @default 'error'
67
+ */
68
+ onUnhandledRequest?: UnhandledRequestStrategy;
69
+ }
70
+ /**
71
+ * A request the microservice sent to the (fake) platform.
72
+ */
73
+ interface C8yRequest {
74
+ method: string;
75
+ /**
76
+ * Path without query, e.g. `/inventory/managedObjects/42`.
77
+ */
78
+ path: string;
79
+ query: Record<string, string>;
80
+ /**
81
+ * Parsed JSON body, raw text, or `undefined`.
82
+ */
83
+ body: unknown;
84
+ headers: Record<string, string>;
85
+ /**
86
+ * Tenant the request targets: `targetTenant` query, else the caller's tenant.
87
+ */
88
+ tenant: string;
89
+ /**
90
+ * User name from basic auth, if any.
91
+ */
92
+ user: string | undefined;
93
+ /**
94
+ * Route params captured by the matching `c8y.override()` pattern.
95
+ */
96
+ params: Record<string, string>;
97
+ }
98
+ /**
99
+ * A platform request the fake tenant could not answer.
100
+ */
101
+ interface C8yUnhandledRequest extends C8yRequest {
102
+ /**
103
+ * Why the fake tenant could not answer it.
104
+ */
105
+ reason: string;
106
+ }
107
+ /**
108
+ * Context passed to a `c8y.override()` handler.
109
+ */
110
+ interface C8yRequestContext extends C8yRequest {
111
+ /**
112
+ * The raw request, body already consumed.
113
+ */
114
+ request: Request;
115
+ }
116
+ /**
117
+ * Answers a platform request. Return a `Response` for full control (status,
118
+ * headers), any other value to answer `200` with it as JSON, or nothing for
119
+ * `204`.
120
+ */
121
+ type C8yRequestHandler = (context: C8yRequestContext) => unknown;
122
+ /**
123
+ * Options for the `c8yTest()` Vite plugin.
124
+ */
125
+ interface C8yTestOptions extends C8yTestState {
126
+ /**
127
+ * Base URL the fake platform pretends to live at (`C8Y_BASEURL`).
128
+ * @default 'https://t12345.cumulocity.test'
129
+ */
130
+ baseUrl?: string;
131
+ }
132
+ declare module 'vitest' {
133
+ interface ProvidedContext {
134
+ /**
135
+ * Baseline options of the `c8yTest()` plugin.
136
+ */
137
+ c8yNitroTest: C8yTestOptions;
138
+ }
139
+ }
140
+ export { C8yTestRole as a, C8yTestUser as c, C8yTestOptions as i, C8yUnhandledRequest as l, C8yRequestContext as n, C8yTestState as o, C8yRequestHandler as r, C8yTestSubscription as s, C8yRequest as t, UnhandledRequestStrategy as u };
package/dist/types.d.mts CHANGED
@@ -1,3 +1,3 @@
1
- import { a as C8YTenantOptionKey, c as C8yDockerOptions, d as C8YManifest, f as C8YManifestOptions, g as C8YAPIClientOptions, h as C8YKnownRole, i as C8YSettingsCategory, l as C8yCacheOptions, m as C8YRoles, n as C8yNitroModuleOptions, o as C8YTenantOptionKeysCacheConfig, p as C8YRole, r as C8yOpenAPIOptions, s as C8yRealtimeOptions, t as C8yDevOptions, u as C8YZipOptions } from "./index-BEFoDy-y.mjs";
2
- import { t as TenantCredentials } from "./credentials-eKnJ7m9Y.mjs";
3
- export { C8YAPIClientOptions, type C8YKnownRole, type C8YManifest, type C8YManifestOptions, C8YRole, C8YRoles, C8YSettingsCategory, C8YTenantOptionKey, C8YTenantOptionKeysCacheConfig, type C8YZipOptions, type C8yCacheOptions, C8yDevOptions, type C8yDockerOptions, C8yNitroModuleOptions, C8yOpenAPIOptions, type C8yRealtimeOptions, type TenantCredentials };
1
+ import { _ as C8YAPIClientOptions, a as C8YTenantOptionKey, c as C8yDockerOptions, d as C8YManifest, f as C8YManifestOptions, g as C8YKnownRole, h as C8YRoles, i as C8YSettingsCategory, l as C8yCacheOptions, m as C8YRole, n as C8yNitroModuleOptions, o as C8YTenantOptionKeysCacheConfig, p as ExposedMcpServer, r as C8yOpenAPIOptions, s as C8yRealtimeOptions, t as C8yDevOptions, u as C8YZipOptions } from "./index-CmIQfBpj.mjs";
2
+ import { t as TenantCredentials } from "./credentials-C_3m4LVX.mjs";
3
+ export { C8YAPIClientOptions, type C8YKnownRole, type C8YManifest, type C8YManifestOptions, C8YRole, C8YRoles, C8YSettingsCategory, C8YTenantOptionKey, C8YTenantOptionKeysCacheConfig, type C8YZipOptions, type C8yCacheOptions, C8yDevOptions, type C8yDockerOptions, C8yNitroModuleOptions, C8yOpenAPIOptions, type C8yRealtimeOptions, type ExposedMcpServer, type TenantCredentials };