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.
- package/README.md +2 -0
- package/dist/{apiClient-kxv5KHYJ.mjs → apiClient-GMQqcgQe.mjs} +2 -3
- package/dist/{autoBootstrap-DUjHuHe_.mjs → autoBootstrap-D4vEytWW.mjs} +6 -8
- package/dist/{bootstrap-DrMjty-k.mjs → bootstrap-DpYAhM7C.mjs} +6 -7
- package/dist/cli/index.mjs +6 -7
- package/dist/{config-BVQHqlgV.mjs → config-Dozrdw3A.mjs} +11 -8
- package/dist/{credentials-eKnJ7m9Y.d.mts → credentials-C_3m4LVX.d.mts} +0 -2
- package/dist/defaults-CrrCinRJ.mjs +20 -0
- package/dist/{c8y-api-DSs1xIIh.mjs → env-fUAhIWsI.mjs} +76 -3
- package/dist/{env-file-BrOTmVjA.mjs → env-file-DWbaFlIp.mjs} +1 -2
- package/dist/{index-BEFoDy-y.d.mts → index-CmIQfBpj.d.mts} +56 -22
- package/dist/index.d.mts +4 -6
- package/dist/index.mjs +20 -21
- package/dist/{logging-BUYT8Z2V.mjs → logging-B1PuWdiF.mjs} +1 -2
- package/dist/{manifest-DV3UW9Dl.mjs → manifest-BhrHDm8Y.mjs} +4 -6
- package/dist/{options-54uJmuUV.mjs → options-CgTptIpI.mjs} +5 -6
- package/dist/{package-Dqlz6AoZ.mjs → package-y3p_br3i.mjs} +2 -3
- package/dist/{roles-CNCObfa3.mjs → roles-DEIHe2Xt.mjs} +4 -5
- package/dist/runtime/handlers/invalidateTenantOptions.mjs +2 -408
- package/dist/runtime/handlers/liveness-readiness.mjs +1 -2
- package/dist/runtime/handlers/openapi.mjs +4 -5
- package/dist/runtime/handlers/rewriteRequestUrl.mjs +3 -4
- package/dist/runtime/middlewares/c8y-client.mjs +3 -4
- package/dist/runtime/middlewares/dev-user.dev.mjs +2 -3
- package/dist/runtime/plugins/c8y-variables.dev.mjs +2 -3
- package/dist/runtime/plugins/enrich-logs.mjs +2 -3
- package/dist/scope-CxEPZ74p.mjs +2763 -0
- package/dist/{client-BmSKIcuq.mjs → tenant-0MBgBY_o.mjs} +115 -139
- package/dist/testing/setup.d.mts +1 -0
- package/dist/testing/setup.mjs +62 -0
- package/dist/testing/vite.d.mts +26 -0
- package/dist/testing/vite.mjs +58 -0
- package/dist/testing-D5VjeD-F.mjs +14 -0
- package/dist/testing.d.mts +521 -0
- package/dist/testing.mjs +494 -0
- package/dist/{typegen-BO_AkyNc.mjs → typegen-Bw4VxFmA.mjs} +4 -5
- package/dist/types-D663gk80.d.mts +140 -0
- package/dist/types.d.mts +3 -3
- package/dist/utils.d.mts +48 -66
- package/dist/utils.mjs +16 -27
- package/package.json +36 -19
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { t as createError } from "./logging-
|
|
2
|
-
import
|
|
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
|
|
7
|
-
import { useRuntimeConfig } from "nitro/runtime-config";
|
|
7
|
+
import { Client, MicroserviceClientRequestAuth } from "@c8y/client";
|
|
8
8
|
import { useNitroHooks } from "nitro/app";
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
276
|
-
*
|
|
277
|
-
* @
|
|
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
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
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 };
|