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
@@ -1,12 +1,12 @@
1
- import { t as createError } from "./logging-BUYT8Z2V.mjs";
2
- import { BasicAuth, Client, MicroserviceClientRequestAuth } from "@c8y/client";
1
+ import { t as createError } from "./logging-B1PuWdiF.mjs";
2
+ import process from "node:process";
3
+ import { useRuntimeConfig } from "nitro/runtime-config";
3
4
  import { defineCachedFunction } from "nitro/cache";
4
5
  import { Buffer } from "node:buffer";
5
6
  import { createHash, randomBytes } from "node:crypto";
6
- import process from "node:process";
7
- import { useRuntimeConfig } from "nitro/runtime-config";
7
+ import { Client, MicroserviceClientRequestAuth } from "@c8y/client";
8
8
  import { useNitroHooks } from "nitro/app";
9
- //#region src/utils/internal/common.ts
9
+ // src/utils/internal/common.ts
10
10
  /**
11
11
  * Whether two sets of tenant credentials are equivalent for the purposes of
12
12
  * change detection: same tenant, user, and password. Used to detect a
@@ -41,119 +41,7 @@ function convertRequestHeadersToC8yFormat(request) {
41
41
  if (cookie) headers.cookie = cookie;
42
42
  return headers;
43
43
  }
44
- //#endregion
45
- //#region src/utils/internal/tenant.ts
46
- const USER_TENANT_CACHE_SALT = randomBytes(32).toString("hex");
47
- function getCookieValue(cookieHeader, name) {
48
- try {
49
- const value = cookieHeader?.match(`(^|;)\\s*${name}\\s*=\\s*([^;]+)`);
50
- return value ? value.pop() : void 0;
51
- } catch {
52
- return;
53
- }
54
- }
55
- function getCurrentUserTenantCacheKeyMaterial(requestOrEvent) {
56
- const request = "req" in requestOrEvent ? requestOrEvent.req : requestOrEvent;
57
- const cookieAuth = getCookieValue(request.headers.get("cookie"), "authorization");
58
- if (cookieAuth) return `cookie:${cookieAuth}`;
59
- const authorization = request.headers.get("authorization");
60
- if (authorization) return `header:${authorization}`;
61
- }
62
- function createCurrentUserTenantCacheKey(requestOrEvent) {
63
- const material = getCurrentUserTenantCacheKeyMaterial(requestOrEvent);
64
- if (!material) throw new Error("Cannot create current user tenant cache key without auth material");
65
- return createHash("sha256").update(USER_TENANT_CACHE_SALT).update(":").update(material).digest("hex");
66
- }
67
- function tryGetTenantFromBasicAuth(requestOrEvent) {
68
- const authorization = ("req" in requestOrEvent ? requestOrEvent.req : requestOrEvent).headers.get("authorization");
69
- if (!authorization?.startsWith("Basic ")) return;
70
- try {
71
- const decoded = Buffer.from(authorization.slice(6), "base64").toString("utf8");
72
- const separatorIndex = decoded.indexOf(":");
73
- const userPart = separatorIndex === -1 ? decoded : decoded.slice(0, separatorIndex);
74
- const slashIndex = userPart.indexOf("/");
75
- if (slashIndex <= 0) return;
76
- return userPart.slice(0, slashIndex);
77
- } catch {
78
- return;
79
- }
80
- }
81
- const getCurrentUserTenantId = defineCachedFunction(async (requestOrEvent) => {
82
- const basicTenant = tryGetTenantFromBasicAuth(requestOrEvent);
83
- if (basicTenant) return basicTenant;
84
- return (await useUserClient(requestOrEvent).tenant.current()).data.name;
85
- }, {
86
- maxAge: 60,
87
- name: "_c8y_nitro_get_current_user_tenant_id",
88
- group: "c8y_nitro",
89
- swr: false,
90
- getKey: (requestOrEvent) => createCurrentUserTenantCacheKey(requestOrEvent),
91
- shouldBypassCache: (requestOrEvent) => !getCurrentUserTenantCacheKeyMaterial(requestOrEvent)
92
- });
93
- const cachedTenantDomain = defineCachedFunction(async (requestOrEvent, _tenantId) => {
94
- return (await useUserClient(requestOrEvent).tenant.current()).data.domainName;
95
- }, {
96
- maxAge: 86400,
97
- name: "_c8y_nitro_get_user_tenant_domain",
98
- group: "c8y_nitro",
99
- swr: false,
100
- getKey: (_requestOrEvent, tenantId) => tenantId
101
- });
102
- /**
103
- * Resolves the public domain of the current user's tenant (e.g. `tenant.cumulocity.com`)
104
- * via `/tenant/currentTenant` with the user's credentials from the current request.
105
- * @param requestOrEvent - The H3Event or ServerRequest from the current request
106
- */
107
- async function getUserTenantDomain(requestOrEvent) {
108
- const tenantId = await getCurrentUserTenantId(requestOrEvent);
109
- return cachedTenantDomain(requestOrEvent, tenantId);
110
- }
111
- const FAST_DOMAIN_SUCCESS_TTL = 6e4;
112
- const FAST_DOMAIN_FAILURE_TTL = 1e4;
113
- const FAST_DOMAIN_MAX_ENTRIES = 1e3;
114
- const fastDomainCache = /* @__PURE__ */ new Map();
115
- /**
116
- * Best-effort variant of {@link getUserTenantDomain} for per-request hot paths:
117
- * never throws (returns `undefined` for unauthenticated requests or failed
118
- * lookups) and memoizes results in-process per auth material. Failures are
119
- * cached briefly so requests with unresolvable credentials don't trigger a
120
- * platform call each time.
121
- * @param requestOrEvent - The H3Event or ServerRequest from the current request
122
- * @param resolveDomain - Domain resolver, only overridden in tests
123
- */
124
- async function tryGetUserTenantDomainFast(requestOrEvent, resolveDomain = getUserTenantDomain) {
125
- if (!getCurrentUserTenantCacheKeyMaterial(requestOrEvent)) return;
126
- const key = createCurrentUserTenantCacheKey(requestOrEvent);
127
- const now = Date.now();
128
- const cached = fastDomainCache.get(key);
129
- if (cached && cached.expiresAt > now) return cached.domain;
130
- let domain;
131
- try {
132
- domain = await resolveDomain(requestOrEvent);
133
- } catch {
134
- domain = void 0;
135
- }
136
- if (!fastDomainCache.has(key) && fastDomainCache.size >= FAST_DOMAIN_MAX_ENTRIES) fastDomainCache.delete(fastDomainCache.keys().next().value);
137
- fastDomainCache.set(key, {
138
- domain,
139
- expiresAt: now + (domain ? FAST_DOMAIN_SUCCESS_TTL : FAST_DOMAIN_FAILURE_TTL)
140
- });
141
- return domain;
142
- }
143
- /**
144
- * Builds the public URL a request is reachable at through the platform proxy:
145
- * `https://<tenant domain>/service/<contextPath><pathname><search>`.
146
- * The proxy strips the `/service/<contextPath>` prefix before the request
147
- * reaches the microservice, so it is re-added here.
148
- * @param tenantDomain - Public domain of the tenant (e.g. `tenant.cumulocity.com`)
149
- * @param contextPath - Context path of the microservice
150
- * @param url - URL the request was received with (only pathname and search are used)
151
- */
152
- function buildPublicRequestUrl(tenantDomain, contextPath, url) {
153
- return new URL(`https://${tenantDomain}/service/${contextPath}${url.pathname}${url.search}`);
154
- }
155
- //#endregion
156
- //#region src/utils/credentials.ts
44
+ // src/utils/credentials.ts
157
45
  let prevCredentials = null;
158
46
  function shouldEmitTenantCredentialsUpdated(prev, next) {
159
47
  if (!prev) return true;
@@ -247,8 +135,7 @@ const useDeployedTenantCredentials = Object.assign(async () => {
247
135
  return await useDeployedTenantCredentials();
248
136
  }
249
137
  });
250
- //#endregion
251
- //#region src/utils/client.ts
138
+ // src/utils/client.ts
252
139
  /**
253
140
  * Creates a Cumulocity client authenticated with the current user's credentials.\
254
141
  * Extracts credentials from the Authorization header of the current request.
@@ -266,30 +153,119 @@ function useUserClient(requestOrEvent) {
266
153
  const auth = new MicroserviceClientRequestAuth(headers);
267
154
  const client = new Client(auth, process.env.C8Y_BASEURL);
268
155
  const forwardedHost = request.headers.get("x-forwarded-host")?.split(",")[0]?.trim();
269
- client.core.defaultHeaders["X-Forwarded-Host"] = forwardedHost || process.env.C8Y_BASEURL;
156
+ if (forwardedHost) client.core.defaultHeaders["X-Forwarded-Host"] = forwardedHost;
270
157
  request.context ??= {};
271
158
  request.context["c8y_user_client"] = client;
272
159
  return client;
273
160
  }
161
+ // src/utils/internal/tenant.ts
162
+ const USER_TENANT_CACHE_SALT = randomBytes(32).toString("hex");
163
+ function getCookieValue(cookieHeader, name) {
164
+ try {
165
+ const value = cookieHeader?.match(`(^|;)\\s*${name}\\s*=\\s*([^;]+)`);
166
+ return value ? value.pop() : void 0;
167
+ } catch {
168
+ return;
169
+ }
170
+ }
171
+ function getCurrentUserTenantCacheKeyMaterial(requestOrEvent) {
172
+ const request = "req" in requestOrEvent ? requestOrEvent.req : requestOrEvent;
173
+ const cookieAuth = getCookieValue(request.headers.get("cookie"), "authorization");
174
+ if (cookieAuth) return `cookie:${cookieAuth}`;
175
+ const authorization = request.headers.get("authorization");
176
+ if (authorization) return `header:${authorization}`;
177
+ }
178
+ function createCurrentUserTenantCacheKey(requestOrEvent) {
179
+ const material = getCurrentUserTenantCacheKeyMaterial(requestOrEvent);
180
+ if (!material) throw new Error("Cannot create current user tenant cache key without auth material");
181
+ return createHash("sha256").update(USER_TENANT_CACHE_SALT).update(":").update(material).digest("hex");
182
+ }
183
+ function tryGetTenantFromBasicAuth(requestOrEvent) {
184
+ const authorization = ("req" in requestOrEvent ? requestOrEvent.req : requestOrEvent).headers.get("authorization");
185
+ if (!authorization?.startsWith("Basic ")) return;
186
+ try {
187
+ const decoded = Buffer.from(authorization.slice(6), "base64").toString("utf8");
188
+ const separatorIndex = decoded.indexOf(":");
189
+ const userPart = separatorIndex === -1 ? decoded : decoded.slice(0, separatorIndex);
190
+ const slashIndex = userPart.indexOf("/");
191
+ if (slashIndex <= 0) return;
192
+ return userPart.slice(0, slashIndex);
193
+ } catch {
194
+ return;
195
+ }
196
+ }
197
+ const getCurrentUserTenantId = defineCachedFunction(async (requestOrEvent) => {
198
+ const basicTenant = tryGetTenantFromBasicAuth(requestOrEvent);
199
+ if (basicTenant) return basicTenant;
200
+ return (await useUserClient(requestOrEvent).tenant.current()).data.name;
201
+ }, {
202
+ maxAge: 60,
203
+ name: "_c8y_nitro_get_current_user_tenant_id",
204
+ group: "c8y_nitro",
205
+ swr: false,
206
+ getKey: (requestOrEvent) => createCurrentUserTenantCacheKey(requestOrEvent),
207
+ shouldBypassCache: (requestOrEvent) => !getCurrentUserTenantCacheKeyMaterial(requestOrEvent)
208
+ });
209
+ const cachedTenantDomain = defineCachedFunction(async (requestOrEvent, _tenantId) => {
210
+ return (await useUserClient(requestOrEvent).tenant.current()).data.domainName;
211
+ }, {
212
+ maxAge: 86400,
213
+ name: "_c8y_nitro_get_user_tenant_domain",
214
+ group: "c8y_nitro",
215
+ swr: false,
216
+ getKey: (_requestOrEvent, tenantId) => tenantId
217
+ });
274
218
  /**
275
- * Creates a Cumulocity client for the tenant where this microservice is deployed.\
276
- * Uses the bootstrap tenant ID from runtime config to identify the deployed tenant.\
277
- * @returns A configured Cumulocity Client instance for the deployed tenant
278
- * @example
279
- * // Get client for the tenant hosting this microservice:
280
- * const client = await useDeployedTenantClient()
281
- * const { data } = await client.application.list()
219
+ * Resolves the public domain of the current user's tenant (e.g. `tenant.cumulocity.com`)
220
+ * via `/tenant/currentTenant` with the user's credentials from the current request.
221
+ * @param requestOrEvent - The H3Event or ServerRequest from the current request
282
222
  */
283
- async function useDeployedTenantClient() {
284
- const creds = await useSubscribedTenantCredentials();
285
- const tenant = process.env.C8Y_BOOTSTRAP_TENANT;
286
- if (!creds[tenant]) throw createError({
287
- message: `No subscribed tenant credentials found for tenant '${tenant}'`,
288
- status: 500
223
+ async function getUserTenantDomain(requestOrEvent) {
224
+ const tenantId = await getCurrentUserTenantId(requestOrEvent);
225
+ return cachedTenantDomain(requestOrEvent, tenantId);
226
+ }
227
+ const FAST_DOMAIN_SUCCESS_TTL = 6e4;
228
+ const FAST_DOMAIN_FAILURE_TTL = 1e4;
229
+ const FAST_DOMAIN_MAX_ENTRIES = 1e3;
230
+ const fastDomainCache = /* @__PURE__ */ new Map();
231
+ /**
232
+ * Best-effort variant of {@link getUserTenantDomain} for per-request hot paths:
233
+ * never throws (returns `undefined` for unauthenticated requests or failed
234
+ * lookups) and memoizes results in-process per auth material. Failures are
235
+ * cached briefly so requests with unresolvable credentials don't trigger a
236
+ * platform call each time.
237
+ * @param requestOrEvent - The H3Event or ServerRequest from the current request
238
+ * @param resolveDomain - Domain resolver, only overridden in tests
239
+ */
240
+ async function tryGetUserTenantDomainFast(requestOrEvent, resolveDomain = getUserTenantDomain) {
241
+ if (!getCurrentUserTenantCacheKeyMaterial(requestOrEvent)) return;
242
+ const key = createCurrentUserTenantCacheKey(requestOrEvent);
243
+ const now = Date.now();
244
+ const cached = fastDomainCache.get(key);
245
+ if (cached && cached.expiresAt > now) return cached.domain;
246
+ let domain;
247
+ try {
248
+ domain = await resolveDomain(requestOrEvent);
249
+ } catch {
250
+ domain = void 0;
251
+ }
252
+ if (!fastDomainCache.has(key) && fastDomainCache.size >= FAST_DOMAIN_MAX_ENTRIES) fastDomainCache.delete(fastDomainCache.keys().next().value);
253
+ fastDomainCache.set(key, {
254
+ domain,
255
+ expiresAt: now + (domain ? FAST_DOMAIN_SUCCESS_TTL : FAST_DOMAIN_FAILURE_TTL)
289
256
  });
290
- const client = new Client(new BasicAuth(creds[tenant]), process.env.C8Y_BASEURL);
291
- client.core.tenant = tenant;
292
- return client;
257
+ return domain;
258
+ }
259
+ /**
260
+ * Builds the public URL a request is reachable at through the platform proxy:
261
+ * `https://<tenant domain>/service/<contextPath><pathname><search>`.
262
+ * The proxy strips the `/service/<contextPath>` prefix before the request
263
+ * reaches the microservice, so it is re-added here.
264
+ * @param tenantDomain - Public domain of the tenant (e.g. `tenant.cumulocity.com`)
265
+ * @param contextPath - Context path of the microservice
266
+ * @param url - URL the request was received with (only pathname and search are used)
267
+ */
268
+ function buildPublicRequestUrl(tenantDomain, contextPath, url) {
269
+ return new URL(`https://${tenantDomain}/service/${contextPath}${url.pathname}${url.search}`);
293
270
  }
294
- //#endregion
295
- export { buildPublicRequestUrl as n, tryGetUserTenantDomainFast as r, useDeployedTenantClient as t };
271
+ export { tryGetUserTenantDomainFast as n, buildPublicRequestUrl as t };
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,62 @@
1
+ import { t as DEFAULTS } from "../defaults-CrrCinRJ.mjs";
2
+ import { _ as FakeWebSocket, a as unhandledMessage, b as waitForRealtimeStartup, c as splitPath, i as platform, o as compileRoute, r as markRunning, s as matchRoute, v as realtime, y as trackInflight } from "../scope-CxEPZ74p.mjs";
3
+ import { t as setTestingHooks } from "../testing-D5VjeD-F.mjs";
4
+ import process from "node:process";
5
+ import { FetchClient } from "@c8y/client";
6
+ import { afterEach, beforeAll, beforeEach, expect, inject } from "vitest";
7
+ // src/testing/setup.ts
8
+ FetchClient.prototype.fetch = async function(url, init) {
9
+ const options = this.adjustOptionsForFormData(this.getFetchOptions(init));
10
+ const fullUrl = new URL(this.getUrl(url, init), process.env.C8Y_BASEURL || DEFAULTS.baseUrl);
11
+ const body = options.body;
12
+ if (body && typeof body.getBuffer === "function" && typeof body.getBoundary === "function") {
13
+ const headers = new Headers(options.headers);
14
+ headers.set("content-type", `multipart/form-data; boundary=${body.getBoundary()}`);
15
+ return platform.fetch(new Request(fullUrl, {
16
+ ...options,
17
+ headers,
18
+ body: body.getBuffer()
19
+ }));
20
+ }
21
+ return platform.fetch(new Request(fullUrl, options));
22
+ };
23
+ const originalFetch = globalThis.fetch;
24
+ globalThis.fetch = (async (input, init) => {
25
+ const request = new Request(input, init);
26
+ const platformOrigin = new URL(process.env.C8Y_BASEURL || DEFAULTS.baseUrl).origin;
27
+ return new URL(request.url).origin === platformOrigin ? platform.fetch(request) : originalFetch(input, init);
28
+ });
29
+ setTestingHooks({ WebSocket: FakeWebSocket });
30
+ platform.prepare(inject("c8yNitroTest") ?? {});
31
+ trackInflight(() => platform.inflight);
32
+ beforeAll(async () => {
33
+ markRunning();
34
+ const { useNitroApp } = await import("nitro/app");
35
+ await useNitroApp().ready;
36
+ await waitForRealtimeStartup();
37
+ });
38
+ beforeEach(async () => {
39
+ platform.baseline = inject("c8yNitroTest") ?? {};
40
+ platform.reset();
41
+ realtime.dropPending();
42
+ const { useSubscribedTenantCredentials, useTenantOptions } = await import("c8y-nitro/utils");
43
+ await useSubscribedTenantCredentials.invalidate();
44
+ await useTenantOptions.invalidateAll();
45
+ });
46
+ afterEach(() => {
47
+ const unhandled = platform.unhandled;
48
+ platform.unhandled = [];
49
+ if (unhandled.length > 0 && platform.onUnhandledRequest === "error") throw new Error(unhandledMessage(unhandled));
50
+ });
51
+ expect.extend({ toHaveReceived(_received, pattern, options = {}) {
52
+ const route = compileRoute(pattern);
53
+ const matching = platform.calls.filter((call) => matchRoute(route, call.method, splitPath(call.path)));
54
+ const pass = options.times === void 0 ? matching.length > 0 : matching.length === options.times;
55
+ const expected = options.times === void 0 ? "at least one request" : `${options.times} request(s)`;
56
+ const received = platform.calls.length > 0 ? platform.calls.map((call) => ` ${call.method} ${call.path}`).join("\n") : " (none)";
57
+ return {
58
+ pass,
59
+ message: () => pass ? `expected no request matching ${pattern}, but got ${matching.length}` : `expected ${expected} matching ${pattern}, got ${matching.length}.\nRequests to the platform in this test:\n${received}`
60
+ };
61
+ } });
62
+ export {};
@@ -0,0 +1,26 @@
1
+ import { i as C8yTestOptions } from "../types-D663gk80.mjs";
2
+ import { Plugin } from "vite";
3
+ /**
4
+ * Vite plugin that turns Nitro's Vitest environment into a fake Cumulocity
5
+ * tenant. Add it next to `nitro()` in `vitest.config.ts`:
6
+ *
7
+ * - Requests of the real `@c8y/client` are answered in memory by the fake
8
+ * tenant behind `c8y` from `c8y-nitro/testing`. Nothing reaches the network.
9
+ * - The `C8Y_*` dev and bootstrap env is set (existing `test.env` values win).
10
+ * - Before every test, the tenant is reset to `options` and c8y-nitro's caches
11
+ * are cleared.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import { nitro } from 'nitro/vite'
16
+ * import { c8yTest } from 'c8y-nitro/testing/vite'
17
+ * import { defineConfig } from 'vitest/config'
18
+ *
19
+ * export default defineConfig({
20
+ * plugins: [nitro(), c8yTest({ user: { userName: 'alice', roles: ['ROLE_ORDERS_READ'] } })],
21
+ * })
22
+ * ```
23
+ * @param options - Baseline state of the fake tenant for every test
24
+ */
25
+ export declare function c8yTest(options?: C8yTestOptions): Plugin;
26
+ export type { C8yTestOptions };
@@ -0,0 +1,58 @@
1
+ import { n as PROVIDE_KEY, t as DEFAULTS } from "../defaults-CrrCinRJ.mjs";
2
+ import { fileURLToPath } from "node:url";
3
+ // src/testing/vite.ts
4
+ /**
5
+ * Vite plugin that turns Nitro's Vitest environment into a fake Cumulocity
6
+ * tenant. Add it next to `nitro()` in `vitest.config.ts`:
7
+ *
8
+ * - Requests of the real `@c8y/client` are answered in memory by the fake
9
+ * tenant behind `c8y` from `c8y-nitro/testing`. Nothing reaches the network.
10
+ * - The `C8Y_*` dev and bootstrap env is set (existing `test.env` values win).
11
+ * - Before every test, the tenant is reset to `options` and c8y-nitro's caches
12
+ * are cleared.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * import { nitro } from 'nitro/vite'
17
+ * import { c8yTest } from 'c8y-nitro/testing/vite'
18
+ * import { defineConfig } from 'vitest/config'
19
+ *
20
+ * export default defineConfig({
21
+ * plugins: [nitro(), c8yTest({ user: { userName: 'alice', roles: ['ROLE_ORDERS_READ'] } })],
22
+ * })
23
+ * ```
24
+ * @param options - Baseline state of the fake tenant for every test
25
+ */
26
+ function c8yTest(options = {}) {
27
+ const setupFile = fileURLToPath(new URL("./setup.mjs", import.meta.url));
28
+ const tenant = options.tenant ?? DEFAULTS.tenant;
29
+ const user = options.user === void 0 ? DEFAULTS.user : options.user;
30
+ return {
31
+ name: "c8y-nitro:test",
32
+ config(config) {
33
+ const env = {
34
+ C8Y_BASEURL: options.baseUrl ?? DEFAULTS.baseUrl,
35
+ C8Y_BOOTSTRAP_TENANT: tenant,
36
+ C8Y_BOOTSTRAP_USER: DEFAULTS.bootstrapUser,
37
+ C8Y_BOOTSTRAP_PASSWORD: DEFAULTS.bootstrapPassword,
38
+ C8Y_DEVELOPMENT_TENANT: user?.tenant ?? tenant,
39
+ C8Y_DEVELOPMENT_USER: user?.userName ?? DEFAULTS.user.userName,
40
+ C8Y_DEVELOPMENT_PASSWORD: DEFAULTS.developmentPassword
41
+ };
42
+ return { test: {
43
+ setupFiles: [setupFile],
44
+ env: {
45
+ ...env,
46
+ ...config.test?.env
47
+ }
48
+ } };
49
+ },
50
+ configEnvironment(name) {
51
+ return name === "nitro" ? { resolve: { external: ["@c8y/client"] } } : void 0;
52
+ },
53
+ configureVitest({ project }) {
54
+ project.provide(PROVIDE_KEY, JSON.parse(JSON.stringify(options)));
55
+ }
56
+ };
57
+ }
58
+ export { c8yTest };
@@ -0,0 +1,14 @@
1
+ // src/utils/internal/testing.ts
2
+ /**
3
+ * Test seam for `c8y-nitro/testing`. Its setup file stores replacements here
4
+ * (on `globalThis`, so the server's copy of c8y-nitro sees them); in
5
+ * production nothing is set and every helper returns `undefined`.
6
+ */
7
+ const TESTING_HOOKS = Symbol.for("c8y-nitro.testing");
8
+ function useTestingHooks() {
9
+ return globalThis[TESTING_HOOKS];
10
+ }
11
+ function setTestingHooks(hooks) {
12
+ globalThis[TESTING_HOOKS] = hooks;
13
+ }
14
+ export { useTestingHooks as n, setTestingHooks as t };