@stratal/testing 0.0.12 → 0.0.14

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 (131) hide show
  1. package/dist/index.d.mts +1070 -0
  2. package/dist/index.d.mts.map +1 -0
  3. package/dist/index.mjs +1689 -0
  4. package/dist/index.mjs.map +1 -0
  5. package/dist/mocks/index.d.mts +2 -0
  6. package/dist/mocks/index.mjs +2 -0
  7. package/dist/mocks/nodemailer.d.mts +12 -0
  8. package/dist/mocks/nodemailer.d.mts.map +1 -0
  9. package/dist/mocks/nodemailer.mjs +7 -0
  10. package/dist/mocks/nodemailer.mjs.map +1 -0
  11. package/dist/mocks/zenstack-language.d.mts +48 -0
  12. package/dist/mocks/zenstack-language.d.mts.map +1 -0
  13. package/dist/mocks/zenstack-language.mjs +48 -0
  14. package/dist/mocks/zenstack-language.mjs.map +1 -0
  15. package/dist/vitest-plugin/index.d.mts +50 -0
  16. package/dist/vitest-plugin/index.d.mts.map +1 -0
  17. package/dist/vitest-plugin/index.mjs +86 -0
  18. package/dist/vitest-plugin/index.mjs.map +1 -0
  19. package/package.json +21 -19
  20. package/dist/auth/acting-as.d.ts +0 -21
  21. package/dist/auth/acting-as.d.ts.map +0 -1
  22. package/dist/auth/acting-as.js +0 -68
  23. package/dist/auth/acting-as.js.map +0 -1
  24. package/dist/auth/index.d.ts +0 -2
  25. package/dist/auth/index.d.ts.map +0 -1
  26. package/dist/auth/index.js +0 -2
  27. package/dist/auth/index.js.map +0 -1
  28. package/dist/core/env/index.d.ts +0 -2
  29. package/dist/core/env/index.d.ts.map +0 -1
  30. package/dist/core/env/index.js +0 -2
  31. package/dist/core/env/index.js.map +0 -1
  32. package/dist/core/env/test-env.d.ts +0 -9
  33. package/dist/core/env/test-env.d.ts.map +0 -1
  34. package/dist/core/env/test-env.js +0 -14
  35. package/dist/core/env/test-env.js.map +0 -1
  36. package/dist/core/http/fetch-mock.types.d.ts +0 -48
  37. package/dist/core/http/fetch-mock.types.d.ts.map +0 -1
  38. package/dist/core/http/fetch-mock.types.js +0 -2
  39. package/dist/core/http/fetch-mock.types.js.map +0 -1
  40. package/dist/core/http/index.d.ts +0 -6
  41. package/dist/core/http/index.d.ts.map +0 -1
  42. package/dist/core/http/index.js +0 -5
  43. package/dist/core/http/index.js.map +0 -1
  44. package/dist/core/http/mock-fetch.d.ts +0 -88
  45. package/dist/core/http/mock-fetch.d.ts.map +0 -1
  46. package/dist/core/http/mock-fetch.js +0 -111
  47. package/dist/core/http/mock-fetch.js.map +0 -1
  48. package/dist/core/http/test-http-client.d.ts +0 -54
  49. package/dist/core/http/test-http-client.d.ts.map +0 -1
  50. package/dist/core/http/test-http-client.js +0 -75
  51. package/dist/core/http/test-http-client.js.map +0 -1
  52. package/dist/core/http/test-http-request.d.ts +0 -60
  53. package/dist/core/http/test-http-request.d.ts.map +0 -1
  54. package/dist/core/http/test-http-request.js +0 -106
  55. package/dist/core/http/test-http-request.js.map +0 -1
  56. package/dist/core/http/test-response.d.ts +0 -161
  57. package/dist/core/http/test-response.d.ts.map +0 -1
  58. package/dist/core/http/test-response.js +0 -309
  59. package/dist/core/http/test-response.js.map +0 -1
  60. package/dist/core/index.d.ts +0 -7
  61. package/dist/core/index.d.ts.map +0 -1
  62. package/dist/core/index.js +0 -7
  63. package/dist/core/index.js.map +0 -1
  64. package/dist/core/override/index.d.ts +0 -2
  65. package/dist/core/override/index.d.ts.map +0 -1
  66. package/dist/core/override/index.js +0 -2
  67. package/dist/core/override/index.js.map +0 -1
  68. package/dist/core/override/provider-override-builder.d.ts +0 -78
  69. package/dist/core/override/provider-override-builder.d.ts.map +0 -1
  70. package/dist/core/override/provider-override-builder.js +0 -94
  71. package/dist/core/override/provider-override-builder.js.map +0 -1
  72. package/dist/core/test.d.ts +0 -48
  73. package/dist/core/test.d.ts.map +0 -1
  74. package/dist/core/test.js +0 -53
  75. package/dist/core/test.js.map +0 -1
  76. package/dist/core/testing-module-builder.d.ts +0 -57
  77. package/dist/core/testing-module-builder.d.ts.map +0 -1
  78. package/dist/core/testing-module-builder.js +0 -109
  79. package/dist/core/testing-module-builder.js.map +0 -1
  80. package/dist/core/testing-module.d.ts +0 -103
  81. package/dist/core/testing-module.d.ts.map +0 -1
  82. package/dist/core/testing-module.js +0 -165
  83. package/dist/core/testing-module.js.map +0 -1
  84. package/dist/errors/index.d.ts +0 -3
  85. package/dist/errors/index.d.ts.map +0 -1
  86. package/dist/errors/index.js +0 -3
  87. package/dist/errors/index.js.map +0 -1
  88. package/dist/errors/setup-error.d.ts +0 -9
  89. package/dist/errors/setup-error.d.ts.map +0 -1
  90. package/dist/errors/setup-error.js +0 -11
  91. package/dist/errors/setup-error.js.map +0 -1
  92. package/dist/errors/test-error.d.ts +0 -9
  93. package/dist/errors/test-error.d.ts.map +0 -1
  94. package/dist/errors/test-error.js +0 -15
  95. package/dist/errors/test-error.js.map +0 -1
  96. package/dist/index.d.ts +0 -16
  97. package/dist/index.d.ts.map +0 -1
  98. package/dist/index.js +0 -23
  99. package/dist/index.js.map +0 -1
  100. package/dist/mocks/index.d.ts +0 -3
  101. package/dist/mocks/index.d.ts.map +0 -1
  102. package/dist/mocks/index.js +0 -3
  103. package/dist/mocks/index.js.map +0 -1
  104. package/dist/mocks/nodemailer.d.ts +0 -10
  105. package/dist/mocks/nodemailer.d.ts.map +0 -1
  106. package/dist/mocks/nodemailer.js +0 -9
  107. package/dist/mocks/nodemailer.js.map +0 -1
  108. package/dist/mocks/zenstack-language.d.ts +0 -46
  109. package/dist/mocks/zenstack-language.d.ts.map +0 -1
  110. package/dist/mocks/zenstack-language.js +0 -47
  111. package/dist/mocks/zenstack-language.js.map +0 -1
  112. package/dist/storage/fake-storage.service.d.ts +0 -114
  113. package/dist/storage/fake-storage.service.d.ts.map +0 -1
  114. package/dist/storage/fake-storage.service.js +0 -233
  115. package/dist/storage/fake-storage.service.js.map +0 -1
  116. package/dist/storage/index.d.ts +0 -2
  117. package/dist/storage/index.d.ts.map +0 -1
  118. package/dist/storage/index.js +0 -2
  119. package/dist/storage/index.js.map +0 -1
  120. package/dist/types.d.ts +0 -5
  121. package/dist/types.d.ts.map +0 -1
  122. package/dist/types.js +0 -3
  123. package/dist/types.js.map +0 -1
  124. package/dist/vitest-plugin/index.d.ts +0 -2
  125. package/dist/vitest-plugin/index.d.ts.map +0 -1
  126. package/dist/vitest-plugin/index.js +0 -2
  127. package/dist/vitest-plugin/index.js.map +0 -1
  128. package/dist/vitest-plugin/stratal-test.d.ts +0 -28
  129. package/dist/vitest-plugin/stratal-test.d.ts.map +0 -1
  130. package/dist/vitest-plugin/stratal-test.js +0 -47
  131. package/dist/vitest-plugin/stratal-test.js.map +0 -1
package/dist/index.mjs ADDED
@@ -0,0 +1,1689 @@
1
+ import { env, waitUntil } from "cloudflare:workers";
2
+ import { Application } from "stratal";
3
+ import { LogLevel } from "stratal/logger";
4
+ import { Module } from "stratal/module";
5
+ import { FileNotFoundError, STORAGE_TOKENS, StorageManagerService, StorageService } from "stratal/storage";
6
+ import { DI_TOKENS, Transient, inject } from "stratal/di";
7
+ import { expect } from "vitest";
8
+ import { connectionSymbol } from "@stratal/framework/database";
9
+ import { AUTH_SERVICE } from "@stratal/framework/auth";
10
+ import { setSessionCookie } from "better-auth/cookies";
11
+ import { convertSetCookieToCookie } from "better-auth/test";
12
+ import { HttpResponse, HttpResponse as HttpResponse$1, http, http as http$1 } from "msw";
13
+ import { setupServer } from "msw/node";
14
+ //#region src/core/override/provider-override-builder.ts
15
+ /**
16
+ * Fluent builder for provider overrides
17
+ *
18
+ * Provides a NestJS-style API for overriding providers in tests.
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * const module = await Test.createTestingModule({
23
+ * imports: [RegistrationModule],
24
+ * })
25
+ * .overrideProvider(EMAIL_TOKENS.EmailService)
26
+ * .useValue(mockEmailService)
27
+ * .compile()
28
+ * ```
29
+ */
30
+ var ProviderOverrideBuilder = class {
31
+ constructor(parent, token) {
32
+ this.parent = parent;
33
+ this.token = token;
34
+ }
35
+ /**
36
+ * Use a static value as the provider
37
+ *
38
+ * The value will be registered directly in the container.
39
+ *
40
+ * @param value - The value to use as the provider
41
+ * @returns The parent TestingModuleBuilder for chaining
42
+ */
43
+ useValue(value) {
44
+ return this.parent.addProviderOverride({
45
+ token: this.token,
46
+ type: "value",
47
+ implementation: value
48
+ });
49
+ }
50
+ /**
51
+ * Use a class as the provider
52
+ *
53
+ * The class will be registered as a singleton.
54
+ *
55
+ * @param cls - The class constructor to use as the provider
56
+ * @returns The parent TestingModuleBuilder for chaining
57
+ */
58
+ useClass(cls) {
59
+ return this.parent.addProviderOverride({
60
+ token: this.token,
61
+ type: "class",
62
+ implementation: cls
63
+ });
64
+ }
65
+ /**
66
+ * Use a factory function as the provider
67
+ *
68
+ * The factory receives the container and should return the provider instance.
69
+ *
70
+ * @param factory - Factory function that creates the provider
71
+ * @returns The parent TestingModuleBuilder for chaining
72
+ */
73
+ useFactory(factory) {
74
+ return this.parent.addProviderOverride({
75
+ token: this.token,
76
+ type: "factory",
77
+ implementation: factory
78
+ });
79
+ }
80
+ /**
81
+ * Use an existing token as the provider (alias)
82
+ *
83
+ * The override token will resolve to the same instance as the target token.
84
+ *
85
+ * @param existingToken - The token to alias
86
+ * @returns The parent TestingModuleBuilder for chaining
87
+ *
88
+ * @example
89
+ * ```typescript
90
+ * const module = await Test.createTestingModule({
91
+ * imports: [MyModule],
92
+ * })
93
+ * .overrideProvider(ABSTRACT_TOKEN)
94
+ * .useExisting(ConcreteService)
95
+ * .compile()
96
+ * ```
97
+ */
98
+ useExisting(existingToken) {
99
+ return this.parent.addProviderOverride({
100
+ token: this.token,
101
+ type: "existing",
102
+ implementation: existingToken
103
+ });
104
+ }
105
+ };
106
+ //#endregion
107
+ //#region \0@oxc-project+runtime@0.115.0/helpers/decorateMetadata.js
108
+ function __decorateMetadata(k, v) {
109
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
110
+ }
111
+ //#endregion
112
+ //#region \0@oxc-project+runtime@0.115.0/helpers/decorateParam.js
113
+ function __decorateParam(paramIndex, decorator) {
114
+ return function(target, key) {
115
+ decorator(target, key, paramIndex);
116
+ };
117
+ }
118
+ //#endregion
119
+ //#region \0@oxc-project+runtime@0.115.0/helpers/decorate.js
120
+ function __decorate(decorators, target, key, desc) {
121
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
122
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
123
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
124
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
125
+ }
126
+ //#endregion
127
+ //#region src/storage/fake-storage.service.ts
128
+ var _ref;
129
+ let FakeStorageService = class FakeStorageService extends StorageService {
130
+ files = /* @__PURE__ */ new Map();
131
+ constructor(storageManager, options) {
132
+ super(storageManager, options);
133
+ this.storageManager = storageManager;
134
+ this.options = options;
135
+ }
136
+ /**
137
+ * Upload content to fake storage
138
+ */
139
+ async upload(body, relativePath, options, disk) {
140
+ const content = await this.bodyToUint8Array(body);
141
+ const diskName = this.resolveDisk(disk);
142
+ this.files.set(relativePath, {
143
+ content,
144
+ mimeType: options.mimeType ?? "application/octet-stream",
145
+ size: options.size,
146
+ metadata: options.metadata,
147
+ uploadedAt: /* @__PURE__ */ new Date()
148
+ });
149
+ return {
150
+ path: relativePath,
151
+ disk: diskName,
152
+ fullPath: relativePath,
153
+ size: options.size,
154
+ mimeType: options.mimeType ?? "application/octet-stream",
155
+ uploadedAt: /* @__PURE__ */ new Date()
156
+ };
157
+ }
158
+ /**
159
+ * Download a file from fake storage
160
+ */
161
+ download(path) {
162
+ const file = this.files.get(path);
163
+ if (!file) return Promise.reject(new FileNotFoundError(path));
164
+ return Promise.resolve({
165
+ toStream: () => new ReadableStream({ start(controller) {
166
+ controller.enqueue(file.content);
167
+ controller.close();
168
+ } }),
169
+ toString: () => Promise.resolve(new TextDecoder().decode(file.content)),
170
+ toArrayBuffer: () => Promise.resolve(file.content),
171
+ contentType: file.mimeType,
172
+ size: file.size,
173
+ metadata: file.metadata
174
+ });
175
+ }
176
+ /**
177
+ * Delete a file from fake storage
178
+ */
179
+ delete(path) {
180
+ this.files.delete(path);
181
+ return Promise.resolve();
182
+ }
183
+ /**
184
+ * Check if a file exists in fake storage
185
+ */
186
+ exists(path) {
187
+ return Promise.resolve(this.files.has(path));
188
+ }
189
+ /**
190
+ * Generate a fake presigned download URL
191
+ */
192
+ getPresignedDownloadUrl(path, expiresIn) {
193
+ return Promise.resolve(this.createPresignedUrl(path, "GET", expiresIn));
194
+ }
195
+ /**
196
+ * Generate a fake presigned upload URL
197
+ */
198
+ getPresignedUploadUrl(path, expiresIn) {
199
+ return Promise.resolve(this.createPresignedUrl(path, "PUT", expiresIn));
200
+ }
201
+ /**
202
+ * Generate a fake presigned delete URL
203
+ */
204
+ getPresignedDeleteUrl(path, expiresIn) {
205
+ return Promise.resolve(this.createPresignedUrl(path, "DELETE", expiresIn));
206
+ }
207
+ /**
208
+ * Chunked upload (same as regular upload for fake)
209
+ */
210
+ async chunkedUpload(body, path, options, disk) {
211
+ const content = await this.bodyToUint8Array(body);
212
+ const size = options.size ?? content.length;
213
+ return this.upload(body, path, {
214
+ ...options,
215
+ size
216
+ }, disk);
217
+ }
218
+ /**
219
+ * Assert that a file exists at the given path
220
+ *
221
+ * @param path - Path to check
222
+ * @throws AssertionError if file does not exist
223
+ */
224
+ assertExists(path) {
225
+ expect(this.files.has(path), `Expected file to exist at: ${path}\nStored files: ${this.getStoredPaths().join(", ") || "(none)"}`).toBe(true);
226
+ }
227
+ /**
228
+ * Assert that a file does NOT exist at the given path
229
+ *
230
+ * @param path - Path to check
231
+ * @throws AssertionError if file exists
232
+ */
233
+ assertMissing(path) {
234
+ expect(this.files.has(path), `Expected file NOT to exist at: ${path}`).toBe(false);
235
+ }
236
+ /**
237
+ * Assert storage is empty
238
+ *
239
+ * @throws AssertionError if any files exist
240
+ */
241
+ assertEmpty() {
242
+ expect(this.files.size, `Expected storage to be empty but found ${this.files.size} files: ${this.getStoredPaths().join(", ")}`).toBe(0);
243
+ }
244
+ /**
245
+ * Assert storage has exactly N files
246
+ *
247
+ * @param count - Expected number of files
248
+ * @throws AssertionError if count doesn't match
249
+ */
250
+ assertCount(count) {
251
+ expect(this.files.size, `Expected ${count} files in storage but found ${this.files.size}`).toBe(count);
252
+ }
253
+ /**
254
+ * Get all stored files (for inspection)
255
+ */
256
+ getStoredFiles() {
257
+ return new Map(this.files);
258
+ }
259
+ /**
260
+ * Get all stored file paths
261
+ */
262
+ getStoredPaths() {
263
+ return Array.from(this.files.keys());
264
+ }
265
+ /**
266
+ * Get a specific file by path
267
+ */
268
+ getFile(path) {
269
+ return this.files.get(path);
270
+ }
271
+ /**
272
+ * Clear all stored files (call in beforeEach for test isolation)
273
+ */
274
+ clear() {
275
+ this.files.clear();
276
+ }
277
+ createPresignedUrl(path, method, expiresIn = 300) {
278
+ const expiresAt = new Date(Date.now() + expiresIn * 1e3);
279
+ return {
280
+ url: `https://fake-storage.test/${path}?method=${method}&expires=${expiresAt.toISOString()}`,
281
+ expiresIn,
282
+ expiresAt,
283
+ method
284
+ };
285
+ }
286
+ async bodyToUint8Array(body) {
287
+ if (!body) return new Uint8Array(0);
288
+ if (body instanceof Uint8Array) return body;
289
+ if (body instanceof ArrayBuffer) return new Uint8Array(body);
290
+ if (typeof body === "string") return new TextEncoder().encode(body);
291
+ if (body instanceof Blob) {
292
+ const buffer = await body.arrayBuffer();
293
+ return new Uint8Array(buffer);
294
+ }
295
+ if (body instanceof ReadableStream) return new Uint8Array(await new Response(body).arrayBuffer());
296
+ if (body instanceof FormData || body instanceof URLSearchParams) return new Uint8Array(await new Response(body).arrayBuffer());
297
+ return new Uint8Array(0);
298
+ }
299
+ };
300
+ FakeStorageService = __decorate([
301
+ Transient(STORAGE_TOKENS.StorageService),
302
+ __decorateParam(0, inject(STORAGE_TOKENS.StorageManager)),
303
+ __decorateParam(1, inject(STORAGE_TOKENS.Options)),
304
+ __decorateMetadata("design:paramtypes", [typeof (_ref = typeof StorageManagerService !== "undefined" && StorageManagerService) === "function" ? _ref : Object, Object])
305
+ ], FakeStorageService);
306
+ //#endregion
307
+ //#region src/core/env/test-env.ts
308
+ /**
309
+ * Get test environment with optional overrides
310
+ *
311
+ * @param overrides - Optional partial env to merge with cloudflare:test env
312
+ * @returns Complete Env object for testing
313
+ */
314
+ function getTestEnv(overrides) {
315
+ return {
316
+ ...env,
317
+ ...overrides
318
+ };
319
+ }
320
+ //#endregion
321
+ //#region src/auth/acting-as.ts
322
+ async function makeSignature(value, secret) {
323
+ const algorithm = {
324
+ name: "HMAC",
325
+ hash: "SHA-256"
326
+ };
327
+ const secretBuf = new TextEncoder().encode(secret);
328
+ const key = await crypto.subtle.importKey("raw", secretBuf, algorithm, false, ["sign"]);
329
+ const signature = await crypto.subtle.sign(algorithm.name, key, new TextEncoder().encode(value));
330
+ return btoa(String.fromCharCode(...new Uint8Array(signature)));
331
+ }
332
+ function buildCookieString(name, value, options = {}) {
333
+ let str = `${name}=${encodeURIComponent(value)}`;
334
+ if (options.path) str += `; Path=${options.path}`;
335
+ if (options.httpOnly) str += "; HttpOnly";
336
+ if (options.secure) str += "; Secure";
337
+ if (options.sameSite) str += `; SameSite=${options.sameSite}`;
338
+ if (options.maxAge !== void 0) str += `; Max-Age=${Math.floor(options.maxAge)}`;
339
+ return str;
340
+ }
341
+ /**
342
+ * ActingAs
343
+ *
344
+ * Creates authentication sessions for testing.
345
+ * Uses Better Auth's internalAdapter to create real database sessions.
346
+ *
347
+ * @example
348
+ * ```typescript
349
+ * const actingAs = new ActingAs(authService)
350
+ * const headers = await actingAs.createSessionForUser({ id: 'user-123' })
351
+ * ```
352
+ */
353
+ var ActingAs = class {
354
+ constructor(authService) {
355
+ this.authService = authService;
356
+ }
357
+ async createSessionForUser(user) {
358
+ const ctx = await this.authService.auth.$context;
359
+ const secret = ctx.secret;
360
+ const session = await ctx.internalAdapter.createSession(user.id, void 0, {
361
+ ipAddress: "127.0.0.1",
362
+ userAgent: "test-client"
363
+ });
364
+ const dbUser = await ctx.internalAdapter.findUserById(user.id);
365
+ if (!dbUser) throw new Error(`User not found: ${user.id}`);
366
+ const responseHeaders = new Headers();
367
+ await setSessionCookie({
368
+ context: ctx,
369
+ getSignedCookie: () => null,
370
+ setSignedCookie: async (name, value, _secret, options = {}) => {
371
+ const signedValue = `${value}.${await makeSignature(value, secret)}`;
372
+ responseHeaders.append("Set-Cookie", buildCookieString(name, signedValue, options));
373
+ },
374
+ setCookie: (name, value, options = {}) => {
375
+ responseHeaders.append("Set-Cookie", buildCookieString(name, value, options));
376
+ }
377
+ }, {
378
+ session,
379
+ user: dbUser
380
+ }, false);
381
+ return convertSetCookieToCookie(responseHeaders);
382
+ }
383
+ };
384
+ //#endregion
385
+ //#region src/core/http/test-response.ts
386
+ /**
387
+ * TestResponse
388
+ *
389
+ * Wrapper around Response with assertion methods.
390
+ *
391
+ * @example
392
+ * ```typescript
393
+ * response
394
+ * .assertOk()
395
+ * .assertStatus(200)
396
+ * .assertJsonPath('data.id', userId)
397
+ * ```
398
+ */
399
+ var TestResponse = class {
400
+ jsonData = null;
401
+ textData = null;
402
+ constructor(response) {
403
+ this.response = response;
404
+ }
405
+ /**
406
+ * Get the raw Response object.
407
+ */
408
+ get raw() {
409
+ return this.response;
410
+ }
411
+ /**
412
+ * Get the response status code.
413
+ */
414
+ get status() {
415
+ return this.response.status;
416
+ }
417
+ /**
418
+ * Get response headers.
419
+ */
420
+ get headers() {
421
+ return this.response.headers;
422
+ }
423
+ /**
424
+ * Get the response body as JSON.
425
+ */
426
+ async json() {
427
+ if (this.jsonData === null) this.jsonData = await this.response.clone().json();
428
+ return this.jsonData;
429
+ }
430
+ /**
431
+ * Get the response body as text.
432
+ */
433
+ async text() {
434
+ this.textData ??= await this.response.clone().text();
435
+ return this.textData;
436
+ }
437
+ /**
438
+ * Assert response status is 200 OK.
439
+ */
440
+ assertOk() {
441
+ return this.assertStatus(200);
442
+ }
443
+ /**
444
+ * Assert response status is 201 Created.
445
+ */
446
+ assertCreated() {
447
+ return this.assertStatus(201);
448
+ }
449
+ /**
450
+ * Assert response status is 204 No Content.
451
+ */
452
+ assertNoContent() {
453
+ return this.assertStatus(204);
454
+ }
455
+ /**
456
+ * Assert response status is 400 Bad Request.
457
+ */
458
+ assertBadRequest() {
459
+ return this.assertStatus(400);
460
+ }
461
+ /**
462
+ * Assert response status is 401 Unauthorized.
463
+ */
464
+ assertUnauthorized() {
465
+ return this.assertStatus(401);
466
+ }
467
+ /**
468
+ * Assert response status is 403 Forbidden.
469
+ */
470
+ assertForbidden() {
471
+ return this.assertStatus(403);
472
+ }
473
+ /**
474
+ * Assert response status is 404 Not Found.
475
+ */
476
+ assertNotFound() {
477
+ return this.assertStatus(404);
478
+ }
479
+ /**
480
+ * Assert response status is 422 Unprocessable Entity.
481
+ */
482
+ assertUnprocessable() {
483
+ return this.assertStatus(422);
484
+ }
485
+ /**
486
+ * Assert response status is 500 Internal Server Error.
487
+ */
488
+ assertServerError() {
489
+ return this.assertStatus(500);
490
+ }
491
+ /**
492
+ * Assert response has the given status code.
493
+ */
494
+ assertStatus(expected) {
495
+ expect(this.response.status, `Expected status ${expected}, got ${this.response.status}`).toBe(expected);
496
+ return this;
497
+ }
498
+ /**
499
+ * Assert response status is in the 2xx success range.
500
+ */
501
+ assertSuccessful() {
502
+ expect(this.response.status >= 200 && this.response.status < 300, `Expected successful status (2xx), got ${this.response.status}`).toBe(true);
503
+ return this;
504
+ }
505
+ /**
506
+ * Assert JSON response contains the given data.
507
+ */
508
+ async assertJson(expected) {
509
+ const actual = await this.json();
510
+ for (const [key, value] of Object.entries(expected)) expect(actual[key], `Expected JSON key "${key}" to be ${JSON.stringify(value)}, got ${JSON.stringify(actual[key])}`).toBe(value);
511
+ return this;
512
+ }
513
+ /**
514
+ * Assert JSON response has value at the given path.
515
+ *
516
+ * @param path - Dot-notation path (e.g., 'data.user.id')
517
+ * @param expected - Expected value at path
518
+ */
519
+ async assertJsonPath(path, expected) {
520
+ const json = await this.json();
521
+ const actual = this.getValueAtPath(json, path);
522
+ expect(actual, `Expected JSON path "${path}" to be ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`).toBe(expected);
523
+ return this;
524
+ }
525
+ /**
526
+ * Assert JSON response structure matches the given schema.
527
+ */
528
+ async assertJsonStructure(structure) {
529
+ const json = await this.json();
530
+ for (const key of structure) expect(key in json, `Expected JSON to have key "${key}", got keys: ${JSON.stringify(Object.keys(json))}`).toBe(true);
531
+ return this;
532
+ }
533
+ /**
534
+ * Assert a JSON path exists (value can be anything, including null).
535
+ *
536
+ * @param path - Dot-notation path (e.g., 'data.user.id')
537
+ */
538
+ async assertJsonPathExists(path) {
539
+ const json = await this.json();
540
+ expect(this.hasValueAtPath(json, path), `Expected JSON path "${path}" to exist`).toBe(true);
541
+ return this;
542
+ }
543
+ /**
544
+ * Assert a JSON path does not exist.
545
+ *
546
+ * @param path - Dot-notation path (e.g., 'data.user.deletedAt')
547
+ */
548
+ async assertJsonPathMissing(path) {
549
+ const json = await this.json();
550
+ expect(this.hasValueAtPath(json, path), `Expected JSON path "${path}" to not exist`).toBe(false);
551
+ return this;
552
+ }
553
+ /**
554
+ * Assert JSON value at path matches a custom predicate.
555
+ *
556
+ * @param path - Dot-notation path (e.g., 'data.items')
557
+ * @param matcher - Predicate function to validate the value
558
+ */
559
+ async assertJsonPathMatches(path, matcher) {
560
+ const json = await this.json();
561
+ const value = this.getValueAtPath(json, path);
562
+ expect(matcher(value), `Expected JSON path "${path}" to match predicate, got ${JSON.stringify(value)}`).toBe(true);
563
+ return this;
564
+ }
565
+ /**
566
+ * Assert string value at path contains a substring.
567
+ *
568
+ * @param path - Dot-notation path (e.g., 'data.message')
569
+ * @param substring - Substring to search for
570
+ */
571
+ async assertJsonPathContains(path, substring) {
572
+ const json = await this.json();
573
+ const value = this.getValueAtPath(json, path);
574
+ expect(typeof value === "string", `Expected JSON path "${path}" to be a string, got ${typeof value}`).toBe(true);
575
+ expect(value.includes(substring), `Expected JSON path "${path}" to contain "${substring}", got "${String(value)}"`).toBe(true);
576
+ return this;
577
+ }
578
+ /**
579
+ * Assert array value at path includes a specific item.
580
+ *
581
+ * @param path - Dot-notation path (e.g., 'data.tags')
582
+ * @param item - Item to search for in the array
583
+ */
584
+ async assertJsonPathIncludes(path, item) {
585
+ const json = await this.json();
586
+ const value = this.getValueAtPath(json, path);
587
+ expect(Array.isArray(value), `Expected JSON path "${path}" to be an array, got ${typeof value}`).toBe(true);
588
+ expect(value.includes(item), `Expected JSON path "${path}" to include ${JSON.stringify(item)}`).toBe(true);
589
+ return this;
590
+ }
591
+ /**
592
+ * Assert array length at path equals the expected count.
593
+ *
594
+ * @param path - Dot-notation path (e.g., 'data.items')
595
+ * @param count - Expected array length
596
+ */
597
+ async assertJsonPathCount(path, count) {
598
+ const json = await this.json();
599
+ const value = this.getValueAtPath(json, path);
600
+ expect(Array.isArray(value), `Expected JSON path "${path}" to be an array, got ${typeof value}`).toBe(true);
601
+ expect(value.length, `Expected JSON path "${path}" to have ${count} items, got ${value.length}`).toBe(count);
602
+ return this;
603
+ }
604
+ /**
605
+ * Assert multiple JSON paths at once (batch assertion).
606
+ *
607
+ * @param expectations - Object mapping paths to expected values
608
+ */
609
+ async assertJsonPaths(expectations) {
610
+ const json = await this.json();
611
+ for (const [path, expected] of Object.entries(expectations)) {
612
+ const actual = this.getValueAtPath(json, path);
613
+ expect(actual, `Expected JSON path "${path}" to be ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`).toBe(expected);
614
+ }
615
+ return this;
616
+ }
617
+ /**
618
+ * Assert response has the given header.
619
+ */
620
+ assertHeader(name, expected) {
621
+ const actual = this.response.headers.get(name);
622
+ expect(actual !== null, `Expected header "${name}" to be present`).toBe(true);
623
+ if (expected !== void 0) expect(actual, `Expected header "${name}" to be "${expected}", got "${actual}"`).toBe(expected);
624
+ return this;
625
+ }
626
+ /**
627
+ * Assert response does not have the given header.
628
+ */
629
+ assertHeaderMissing(name) {
630
+ const actual = this.response.headers.get(name);
631
+ expect(actual, `Expected header "${name}" to be absent, but got "${actual}"`).toBeNull();
632
+ return this;
633
+ }
634
+ /**
635
+ * Get value at dot-notation path.
636
+ */
637
+ getValueAtPath(obj, path) {
638
+ const parts = path.split(".");
639
+ let current = obj;
640
+ for (const part of parts) {
641
+ if (current === null || current === void 0) return;
642
+ current = current[part];
643
+ }
644
+ return current;
645
+ }
646
+ /**
647
+ * Check if a path exists in the object (even if value is null/undefined).
648
+ */
649
+ hasValueAtPath(obj, path) {
650
+ const parts = path.split(".");
651
+ let current = obj;
652
+ for (const part of parts) {
653
+ if (current === null || current === void 0) return false;
654
+ if (typeof current !== "object") return false;
655
+ const record = current;
656
+ if (!(part in record)) return false;
657
+ current = record[part];
658
+ }
659
+ return true;
660
+ }
661
+ };
662
+ //#endregion
663
+ //#region src/core/http/test-http-request.ts
664
+ /**
665
+ * TestHttpRequest
666
+ *
667
+ * Request builder with fluent API for configuring test HTTP requests.
668
+ *
669
+ * @example
670
+ * ```typescript
671
+ * const response = await module.http
672
+ * .post('/api/v1/register')
673
+ * .withBody({ name: 'Test School' })
674
+ * .withHeaders({ 'X-Custom': 'value' })
675
+ * .send()
676
+ * ```
677
+ *
678
+ * @example Authenticated request
679
+ * ```typescript
680
+ * const response = await module.http
681
+ * .get('/api/v1/profile')
682
+ * .actingAs({ id: user.id })
683
+ * .send()
684
+ * ```
685
+ */
686
+ var TestHttpRequest = class {
687
+ body = null;
688
+ requestHeaders;
689
+ actingAsUser = null;
690
+ constructor(method, path, headers, module, host = null) {
691
+ this.method = method;
692
+ this.path = path;
693
+ this.module = module;
694
+ this.host = host;
695
+ this.requestHeaders = new Headers(headers);
696
+ }
697
+ /**
698
+ * Set the request body
699
+ */
700
+ withBody(data) {
701
+ this.body = data;
702
+ return this;
703
+ }
704
+ /**
705
+ * Add headers to the request
706
+ */
707
+ withHeaders(headers) {
708
+ for (const [key, value] of Object.entries(headers)) this.requestHeaders.set(key, value);
709
+ return this;
710
+ }
711
+ /**
712
+ * Set Content-Type to application/json
713
+ */
714
+ asJson() {
715
+ this.requestHeaders.set("Content-Type", "application/json");
716
+ return this;
717
+ }
718
+ /**
719
+ * Authenticate the request as a specific user
720
+ */
721
+ actingAs(user) {
722
+ this.actingAsUser = user;
723
+ return this;
724
+ }
725
+ /**
726
+ * Send the request and return response
727
+ *
728
+ * Calls module.fetch() - NOT SELF.fetch()
729
+ */
730
+ async send() {
731
+ await this.applyAuthentication();
732
+ if (this.body && !this.requestHeaders.has("Content-Type")) this.requestHeaders.set("Content-Type", "application/json");
733
+ const url = new URL(this.path, `http://${this.host ?? "localhost"}`);
734
+ const request = new Request(url.toString(), {
735
+ method: this.method,
736
+ headers: this.requestHeaders,
737
+ body: this.body ? JSON.stringify(this.body) : null
738
+ });
739
+ return new TestResponse(await this.module.fetch(request));
740
+ }
741
+ async applyAuthentication() {
742
+ if (!this.actingAsUser) return;
743
+ await this.module.runInRequestScope(async () => {
744
+ const actingAs = new ActingAs(this.module.get(AUTH_SERVICE));
745
+ const authHeaders = this.actingAsUser ? await actingAs.createSessionForUser(this.actingAsUser) : new Headers();
746
+ for (const [key, value] of authHeaders.entries()) this.requestHeaders.set(key, value);
747
+ });
748
+ }
749
+ };
750
+ //#endregion
751
+ //#region src/core/http/test-http-client.ts
752
+ /**
753
+ * TestHttpClient
754
+ *
755
+ * Fluent HTTP client for making test requests.
756
+ *
757
+ * @example
758
+ * ```typescript
759
+ * const response = await module.http
760
+ * .forHost('example.com')
761
+ * .post('/api/v1/users')
762
+ * .withBody({ name: 'Test' })
763
+ * .send()
764
+ *
765
+ * response.assertCreated()
766
+ * ```
767
+ */
768
+ var TestHttpClient = class {
769
+ defaultHeaders = new Headers();
770
+ host = null;
771
+ constructor(module) {
772
+ this.module = module;
773
+ }
774
+ /**
775
+ * Set the host for the request
776
+ */
777
+ forHost(host) {
778
+ this.host = host;
779
+ return this;
780
+ }
781
+ /**
782
+ * Set default headers for all requests
783
+ */
784
+ withHeaders(headers) {
785
+ for (const [key, value] of Object.entries(headers)) this.defaultHeaders.set(key, value);
786
+ return this;
787
+ }
788
+ /**
789
+ * Create a GET request
790
+ */
791
+ get(path) {
792
+ return this.createRequest("GET", path);
793
+ }
794
+ /**
795
+ * Create a POST request
796
+ */
797
+ post(path) {
798
+ return this.createRequest("POST", path);
799
+ }
800
+ /**
801
+ * Create a PUT request
802
+ */
803
+ put(path) {
804
+ return this.createRequest("PUT", path);
805
+ }
806
+ /**
807
+ * Create a PATCH request
808
+ */
809
+ patch(path) {
810
+ return this.createRequest("PATCH", path);
811
+ }
812
+ /**
813
+ * Create a DELETE request
814
+ */
815
+ delete(path) {
816
+ return this.createRequest("DELETE", path);
817
+ }
818
+ createRequest(method, path) {
819
+ return new TestHttpRequest(method, path, this.defaultHeaders, this.module, this.host);
820
+ }
821
+ };
822
+ //#endregion
823
+ //#region src/core/sse/test-sse-connection.ts
824
+ /**
825
+ * TestSseConnection
826
+ *
827
+ * Live SSE connection wrapper with assertion helpers for testing.
828
+ *
829
+ * @example
830
+ * ```typescript
831
+ * const sse = await module.sse('/streaming/sse').connect()
832
+ * await sse.assertEvent({ event: 'message', data: 'hello', id: '1' })
833
+ * await sse.waitForEnd()
834
+ * ```
835
+ */
836
+ var TestSseConnection = class {
837
+ eventQueue = [];
838
+ eventWaiters = [];
839
+ streamEnded = false;
840
+ endWaiters = [];
841
+ constructor(response) {
842
+ this.response = response;
843
+ this.startReading();
844
+ }
845
+ /**
846
+ * Wait for the next SSE event
847
+ */
848
+ async waitForEvent(timeout = 5e3) {
849
+ if (this.eventQueue.length > 0) return this.eventQueue.shift();
850
+ if (this.streamEnded) throw new Error("SSE: stream has ended, no more events");
851
+ return new Promise((resolve, reject) => {
852
+ const waiter = (event) => {
853
+ clearTimeout(timer);
854
+ resolve(event);
855
+ };
856
+ const timer = setTimeout(() => {
857
+ const index = this.eventWaiters.indexOf(waiter);
858
+ if (index !== -1) this.eventWaiters.splice(index, 1);
859
+ reject(/* @__PURE__ */ new Error(`SSE: no event received within ${timeout}ms`));
860
+ }, timeout);
861
+ this.eventWaiters.push(waiter);
862
+ });
863
+ }
864
+ /**
865
+ * Wait for the stream to end
866
+ */
867
+ async waitForEnd(timeout = 5e3) {
868
+ if (this.streamEnded) return;
869
+ return new Promise((resolve, reject) => {
870
+ const waiter = () => {
871
+ clearTimeout(timer);
872
+ resolve();
873
+ };
874
+ const timer = setTimeout(() => {
875
+ const index = this.endWaiters.indexOf(waiter);
876
+ if (index !== -1) this.endWaiters.splice(index, 1);
877
+ reject(/* @__PURE__ */ new Error(`SSE: stream did not end within ${timeout}ms`));
878
+ }, timeout);
879
+ this.endWaiters.push(waiter);
880
+ });
881
+ }
882
+ /**
883
+ * Collect all remaining events until the stream ends
884
+ */
885
+ async collectEvents(timeout = 5e3) {
886
+ const events = [];
887
+ if (this.streamEnded) return [...this.eventQueue.splice(0)];
888
+ return new Promise((resolve, reject) => {
889
+ const originalDispatch = this.dispatchEvent.bind(this);
890
+ this.dispatchEvent = (event) => {
891
+ events.push(event);
892
+ originalDispatch(event);
893
+ };
894
+ const endWaiter = () => {
895
+ clearTimeout(timer);
896
+ this.dispatchEvent = originalDispatch;
897
+ resolve(events);
898
+ };
899
+ const timer = setTimeout(() => {
900
+ this.dispatchEvent = originalDispatch;
901
+ const index = this.endWaiters.indexOf(endWaiter);
902
+ if (index !== -1) this.endWaiters.splice(index, 1);
903
+ reject(/* @__PURE__ */ new Error(`SSE: stream did not end within ${timeout}ms`));
904
+ }, timeout);
905
+ events.push(...this.eventQueue.splice(0));
906
+ this.endWaiters.push(endWaiter);
907
+ });
908
+ }
909
+ /**
910
+ * Assert that the next event matches the expected partial shape
911
+ */
912
+ async assertEvent(expected, timeout = 5e3) {
913
+ expect(await this.waitForEvent(timeout)).toMatchObject(expected);
914
+ }
915
+ /**
916
+ * Assert that the next event's data matches the expected string
917
+ */
918
+ async assertEventData(expected, timeout = 5e3) {
919
+ const event = await this.waitForEvent(timeout);
920
+ expect(event.data, `Expected SSE data "${expected}", got "${event.data}"`).toBe(expected);
921
+ }
922
+ /**
923
+ * Assert that the next event's data is JSON matching the expected value
924
+ */
925
+ async assertJsonEventData(expected, timeout = 5e3) {
926
+ const event = await this.waitForEvent(timeout);
927
+ expect(JSON.parse(event.data)).toEqual(expected);
928
+ }
929
+ /**
930
+ * Access the raw Response
931
+ */
932
+ get raw() {
933
+ return this.response;
934
+ }
935
+ startReading() {
936
+ const body = this.response.body;
937
+ if (!body) {
938
+ this.streamEnded = true;
939
+ return;
940
+ }
941
+ const reader = body.getReader();
942
+ const decoder = new TextDecoder();
943
+ let buffer = "";
944
+ const read = async () => {
945
+ try {
946
+ while (true) {
947
+ const { done, value } = await reader.read();
948
+ if (done) {
949
+ if (buffer.trim()) {
950
+ const event = this.parseEvent(buffer);
951
+ if (event) this.dispatchEvent(event);
952
+ }
953
+ this.streamEnded = true;
954
+ for (const waiter of this.endWaiters) waiter();
955
+ this.endWaiters = [];
956
+ return;
957
+ }
958
+ buffer += decoder.decode(value, { stream: true });
959
+ const parts = buffer.split("\n\n");
960
+ buffer = parts.pop();
961
+ for (const part of parts) {
962
+ if (!part.trim()) continue;
963
+ const event = this.parseEvent(part);
964
+ if (event) this.dispatchEvent(event);
965
+ }
966
+ }
967
+ } catch {
968
+ this.streamEnded = true;
969
+ for (const waiter of this.endWaiters) waiter();
970
+ this.endWaiters = [];
971
+ }
972
+ };
973
+ read();
974
+ }
975
+ parseEvent(raw) {
976
+ const lines = raw.split("\n");
977
+ const dataLines = [];
978
+ let event;
979
+ let id;
980
+ let retry;
981
+ for (const line of lines) {
982
+ if (line.startsWith(":")) continue;
983
+ const colonIndex = line.indexOf(":");
984
+ if (colonIndex === -1) continue;
985
+ const field = line.slice(0, colonIndex);
986
+ const value = line[colonIndex + 1] === " " ? line.slice(colonIndex + 2) : line.slice(colonIndex + 1);
987
+ switch (field) {
988
+ case "data":
989
+ dataLines.push(value);
990
+ break;
991
+ case "event":
992
+ event = value;
993
+ break;
994
+ case "id":
995
+ id = value;
996
+ break;
997
+ case "retry": {
998
+ const parsed = parseInt(value, 10);
999
+ if (!isNaN(parsed)) retry = parsed;
1000
+ break;
1001
+ }
1002
+ }
1003
+ }
1004
+ if (dataLines.length === 0) return null;
1005
+ const result = { data: dataLines.join("\n") };
1006
+ if (event !== void 0) result.event = event;
1007
+ if (id !== void 0) result.id = id;
1008
+ if (retry !== void 0) result.retry = retry;
1009
+ return result;
1010
+ }
1011
+ dispatchEvent(event) {
1012
+ if (this.eventWaiters.length > 0) this.eventWaiters.shift()(event);
1013
+ else this.eventQueue.push(event);
1014
+ }
1015
+ };
1016
+ //#endregion
1017
+ //#region src/core/sse/test-sse-request.ts
1018
+ /**
1019
+ * TestSseRequest
1020
+ *
1021
+ * Builder for SSE connection requests. Follows the TestWsRequest pattern.
1022
+ *
1023
+ * @example
1024
+ * ```typescript
1025
+ * const sse = await module.sse('/streaming/sse').connect()
1026
+ * await sse.assertEvent({ event: 'message', data: 'hello' })
1027
+ * await sse.waitForEnd()
1028
+ * ```
1029
+ *
1030
+ * @example Authenticated SSE
1031
+ * ```typescript
1032
+ * const sse = await module.sse('/streaming/sse').actingAs({ id: user.id }).connect()
1033
+ * ```
1034
+ */
1035
+ var TestSseRequest = class {
1036
+ requestHeaders = new Headers();
1037
+ actingAsUser = null;
1038
+ constructor(path, module) {
1039
+ this.path = path;
1040
+ this.module = module;
1041
+ }
1042
+ /**
1043
+ * Add custom headers to the request
1044
+ */
1045
+ withHeaders(headers) {
1046
+ for (const [key, value] of Object.entries(headers)) this.requestHeaders.set(key, value);
1047
+ return this;
1048
+ }
1049
+ /**
1050
+ * Authenticate the SSE connection as a specific user
1051
+ */
1052
+ actingAs(user) {
1053
+ this.actingAsUser = user;
1054
+ return this;
1055
+ }
1056
+ /**
1057
+ * Send the request and return a live SSE connection
1058
+ */
1059
+ async connect() {
1060
+ await this.applyAuthentication();
1061
+ this.requestHeaders.set("Accept", "text/event-stream");
1062
+ const url = new URL(this.path, "http://localhost");
1063
+ const request = new Request(url.toString(), { headers: this.requestHeaders });
1064
+ const response = await this.module.fetch(request);
1065
+ expect(response.status, `Expected status 200, got ${response.status}`).toBe(200);
1066
+ const contentType = response.headers.get("content-type") ?? "";
1067
+ expect(contentType.includes("text/event-stream"), `Expected content-type "text/event-stream", got "${contentType}"`).toBe(true);
1068
+ return new TestSseConnection(response);
1069
+ }
1070
+ async applyAuthentication() {
1071
+ if (!this.actingAsUser) return;
1072
+ await this.module.runInRequestScope(async () => {
1073
+ const actingAs = new ActingAs(this.module.get(AUTH_SERVICE));
1074
+ const authHeaders = this.actingAsUser ? await actingAs.createSessionForUser(this.actingAsUser) : new Headers();
1075
+ for (const [key, value] of authHeaders.entries()) this.requestHeaders.set(key, value);
1076
+ });
1077
+ }
1078
+ };
1079
+ //#endregion
1080
+ //#region src/core/ws/test-ws-connection.ts
1081
+ /**
1082
+ * TestWsConnection
1083
+ *
1084
+ * Live WebSocket connection wrapper with assertion helpers for testing.
1085
+ *
1086
+ * @example
1087
+ * ```typescript
1088
+ * const ws = await module.ws('/ws/chat').connect()
1089
+ * ws.send('hello')
1090
+ * await ws.assertMessage('echo:hello')
1091
+ * ws.close()
1092
+ * await ws.waitForClose()
1093
+ * ```
1094
+ */
1095
+ var TestWsConnection = class {
1096
+ messageQueue = [];
1097
+ messageWaiters = [];
1098
+ closeEvent = null;
1099
+ closeWaiters = [];
1100
+ constructor(ws) {
1101
+ this.ws = ws;
1102
+ this.ws.addEventListener("message", (event) => {
1103
+ const data = event.data;
1104
+ if (this.messageWaiters.length > 0) this.messageWaiters.shift()(data);
1105
+ else this.messageQueue.push(data);
1106
+ });
1107
+ this.ws.addEventListener("close", (event) => {
1108
+ this.closeEvent = {
1109
+ code: event.code,
1110
+ reason: event.reason
1111
+ };
1112
+ for (const waiter of this.closeWaiters) waiter(this.closeEvent);
1113
+ this.closeWaiters = [];
1114
+ });
1115
+ }
1116
+ /**
1117
+ * Send a message through the WebSocket
1118
+ */
1119
+ send(data) {
1120
+ this.ws.send(data);
1121
+ }
1122
+ /**
1123
+ * Close the WebSocket connection
1124
+ */
1125
+ close(code, reason) {
1126
+ this.ws.close(code, reason);
1127
+ }
1128
+ /**
1129
+ * Wait for the next message, returning its data
1130
+ */
1131
+ async waitForMessage(timeout = 5e3) {
1132
+ if (this.messageQueue.length > 0) return this.messageQueue.shift();
1133
+ return new Promise((resolve, reject) => {
1134
+ const waiter = (data) => {
1135
+ clearTimeout(timer);
1136
+ resolve(data);
1137
+ };
1138
+ const timer = setTimeout(() => {
1139
+ const index = this.messageWaiters.indexOf(waiter);
1140
+ if (index !== -1) this.messageWaiters.splice(index, 1);
1141
+ reject(/* @__PURE__ */ new Error(`WebSocket: no message received within ${timeout}ms`));
1142
+ }, timeout);
1143
+ this.messageWaiters.push(waiter);
1144
+ });
1145
+ }
1146
+ /**
1147
+ * Wait for the connection to close
1148
+ */
1149
+ async waitForClose(timeout = 5e3) {
1150
+ if (this.closeEvent) return this.closeEvent;
1151
+ return new Promise((resolve, reject) => {
1152
+ const waiter = (event) => {
1153
+ clearTimeout(timer);
1154
+ resolve(event);
1155
+ };
1156
+ const timer = setTimeout(() => {
1157
+ const index = this.closeWaiters.indexOf(waiter);
1158
+ if (index !== -1) this.closeWaiters.splice(index, 1);
1159
+ reject(/* @__PURE__ */ new Error(`WebSocket: connection did not close within ${timeout}ms`));
1160
+ }, timeout);
1161
+ this.closeWaiters.push(waiter);
1162
+ });
1163
+ }
1164
+ /**
1165
+ * Assert that the next message equals the expected value
1166
+ */
1167
+ async assertMessage(expected, timeout = 5e3) {
1168
+ const data = await this.waitForMessage(timeout);
1169
+ const message = typeof data === "string" ? data : "[ArrayBuffer]";
1170
+ expect(message, `Expected WebSocket message "${expected}", got "${message}"`).toBe(expected);
1171
+ }
1172
+ /**
1173
+ * Assert that the connection closes, optionally with an expected code
1174
+ */
1175
+ async assertClosed(expectedCode, timeout = 5e3) {
1176
+ const event = await this.waitForClose(timeout);
1177
+ if (expectedCode !== void 0) expect(event.code, `Expected close code ${expectedCode}, got ${event.code}`).toBe(expectedCode);
1178
+ }
1179
+ /**
1180
+ * Access the raw Cloudflare WebSocket
1181
+ */
1182
+ get raw() {
1183
+ return this.ws;
1184
+ }
1185
+ };
1186
+ //#endregion
1187
+ //#region src/core/ws/test-ws-request.ts
1188
+ /**
1189
+ * TestWsRequest
1190
+ *
1191
+ * Builder for WebSocket upgrade requests. Follows the TestHttpRequest pattern.
1192
+ *
1193
+ * @example
1194
+ * ```typescript
1195
+ * const ws = await module.ws('/ws/chat').connect()
1196
+ * ws.send('hello')
1197
+ * await ws.assertMessage('echo:hello')
1198
+ * ws.close()
1199
+ * ```
1200
+ *
1201
+ * @example Authenticated WebSocket
1202
+ * ```typescript
1203
+ * const ws = await module.ws('/ws/chat').actingAs({ id: user.id }).connect()
1204
+ * ```
1205
+ */
1206
+ var TestWsRequest = class {
1207
+ requestHeaders = new Headers();
1208
+ actingAsUser = null;
1209
+ constructor(path, module) {
1210
+ this.path = path;
1211
+ this.module = module;
1212
+ }
1213
+ /**
1214
+ * Add custom headers to the upgrade request
1215
+ */
1216
+ withHeaders(headers) {
1217
+ for (const [key, value] of Object.entries(headers)) this.requestHeaders.set(key, value);
1218
+ return this;
1219
+ }
1220
+ /**
1221
+ * Authenticate the WebSocket connection as a specific user
1222
+ */
1223
+ actingAs(user) {
1224
+ this.actingAsUser = user;
1225
+ return this;
1226
+ }
1227
+ /**
1228
+ * Send the upgrade request and return a live WebSocket connection
1229
+ */
1230
+ async connect() {
1231
+ await this.applyAuthentication();
1232
+ this.requestHeaders.set("Upgrade", "websocket");
1233
+ this.requestHeaders.set("Connection", "Upgrade");
1234
+ this.requestHeaders.set("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ==");
1235
+ this.requestHeaders.set("Sec-WebSocket-Version", "13");
1236
+ const url = new URL(this.path, "http://localhost");
1237
+ const request = new Request(url.toString(), { headers: this.requestHeaders });
1238
+ const response = await this.module.fetch(request);
1239
+ expect(response.status, `Expected status 101 (Switching Protocols), got ${response.status}`).toBe(101);
1240
+ const ws = response.webSocket;
1241
+ if (!ws) throw new Error("Response did not include a WebSocket connection");
1242
+ ws.accept();
1243
+ return new TestWsConnection(ws);
1244
+ }
1245
+ async applyAuthentication() {
1246
+ if (!this.actingAsUser) return;
1247
+ await this.module.runInRequestScope(async () => {
1248
+ const actingAs = new ActingAs(this.module.get(AUTH_SERVICE));
1249
+ const authHeaders = this.actingAsUser ? await actingAs.createSessionForUser(this.actingAsUser) : new Headers();
1250
+ for (const [key, value] of authHeaders.entries()) this.requestHeaders.set(key, value);
1251
+ });
1252
+ }
1253
+ };
1254
+ //#endregion
1255
+ //#region src/core/testing-module.ts
1256
+ /**
1257
+ * TestingModule
1258
+ *
1259
+ * Provides access to the test application, container, HTTP client, and utilities.
1260
+ *
1261
+ * @example
1262
+ * ```typescript
1263
+ * const module = await Test.createTestingModule({
1264
+ * modules: [RegistrationModule],
1265
+ * }).compile()
1266
+ *
1267
+ * // Make HTTP requests
1268
+ * const response = await module.http
1269
+ * .post('/api/v1/register')
1270
+ * .withBody({ ... })
1271
+ * .send()
1272
+ *
1273
+ * // Access services
1274
+ * const service = module.get(REGISTRATION_TOKENS.RegistrationService)
1275
+ *
1276
+ * // Database utilities
1277
+ * await module.truncateDb()
1278
+ * await module.seed(new UserSeeder())
1279
+ * await module.assertDatabaseHas('user', { email: 'test@example.com' })
1280
+ *
1281
+ * // Cleanup
1282
+ * await module.close()
1283
+ * ```
1284
+ */
1285
+ var TestingModule = class {
1286
+ _http = null;
1287
+ _requestContainer;
1288
+ constructor(app, env, ctx) {
1289
+ this.app = app;
1290
+ this.env = env;
1291
+ this.ctx = ctx;
1292
+ const mockContext = this.app.createMockRouterContext();
1293
+ this._requestContainer = this.app.container.createRequestScope(mockContext);
1294
+ }
1295
+ /**
1296
+ * Resolve a service from the container
1297
+ */
1298
+ get(token) {
1299
+ return this._requestContainer.resolve(token);
1300
+ }
1301
+ /**
1302
+ * Get HTTP test client for making requests
1303
+ */
1304
+ get http() {
1305
+ this._http ??= new TestHttpClient(this);
1306
+ return this._http;
1307
+ }
1308
+ /**
1309
+ * Get fake storage service for assertions
1310
+ */
1311
+ get storage() {
1312
+ return this.get(STORAGE_TOKENS.StorageService);
1313
+ }
1314
+ /**
1315
+ * Create a WebSocket test request builder for the given path
1316
+ */
1317
+ ws(path) {
1318
+ return new TestWsRequest(path, this);
1319
+ }
1320
+ /**
1321
+ * Create an SSE test request builder for the given path
1322
+ */
1323
+ sse(path) {
1324
+ return new TestSseRequest(path, this);
1325
+ }
1326
+ /**
1327
+ * Get Application instance
1328
+ */
1329
+ get application() {
1330
+ return this.app;
1331
+ }
1332
+ /**
1333
+ * Get DI Container (request-scoped)
1334
+ */
1335
+ get container() {
1336
+ return this._requestContainer;
1337
+ }
1338
+ /**
1339
+ * Execute an HTTP request through HonoApp
1340
+ */
1341
+ async fetch(request) {
1342
+ return this.app.hono.fetch(request, this.env, this.ctx);
1343
+ }
1344
+ /**
1345
+ * Run callback in request scope (for DB operations, service access)
1346
+ */
1347
+ async runInRequestScope(callback) {
1348
+ const mockContext = this.app.createMockRouterContext();
1349
+ return this.app.container.runInRequestScope(mockContext, callback);
1350
+ }
1351
+ getDb(name) {
1352
+ const token = name ? connectionSymbol(name) : DI_TOKENS.Database;
1353
+ return this._requestContainer.resolve(token);
1354
+ }
1355
+ /**
1356
+ * Truncate all non-prisma tables in the database
1357
+ */
1358
+ async truncateDb(name) {
1359
+ const db = this.getDb(name);
1360
+ const tables = await db.$queryRaw`
1361
+ SELECT tablename::text as tablename FROM pg_tables
1362
+ WHERE schemaname = current_schema()
1363
+ AND tablename NOT LIKE '_prisma%'
1364
+ `;
1365
+ if (tables.length === 0) return;
1366
+ const tableList = tables.map((t) => `"${t.tablename}"`).join(", ");
1367
+ await db.$executeRawUnsafe(`TRUNCATE ${tableList} RESTART IDENTITY CASCADE`);
1368
+ }
1369
+ async seed(...args) {
1370
+ let name;
1371
+ let seeders;
1372
+ if (typeof args[0] === "string") {
1373
+ name = args[0];
1374
+ seeders = args.slice(1);
1375
+ } else seeders = args;
1376
+ await this.getDb(name).$transaction(async (tx) => {
1377
+ for (const seeder of seeders) await seeder.run(tx);
1378
+ });
1379
+ }
1380
+ /**
1381
+ * Assert that a record exists in the database
1382
+ */
1383
+ async assertDatabaseHas(table, data, name) {
1384
+ expect(await this.getDb(name)[table].findFirst({ where: data }), `Expected ${table} with ${JSON.stringify(data)}`).not.toBeNull();
1385
+ }
1386
+ /**
1387
+ * Assert that a record does not exist in the database
1388
+ */
1389
+ async assertDatabaseMissing(table, data, name) {
1390
+ expect(await this.getDb(name)[table].findFirst({ where: data }), `Expected ${table} NOT to have ${JSON.stringify(data)}`).toBeNull();
1391
+ }
1392
+ /**
1393
+ * Assert the number of records in a table
1394
+ */
1395
+ async assertDatabaseCount(table, expected, name) {
1396
+ const actual = await this.getDb(name)[table].count();
1397
+ expect(actual, `Expected ${table} count ${expected}, got ${actual}`).toBe(expected);
1398
+ }
1399
+ /**
1400
+ * Cleanup - call in afterAll
1401
+ */
1402
+ async close() {
1403
+ await this._requestContainer.dispose();
1404
+ await this.app.shutdown();
1405
+ }
1406
+ };
1407
+ //#endregion
1408
+ //#region src/core/testing-module-builder.ts
1409
+ /**
1410
+ * Builder for creating test modules with provider overrides
1411
+ */
1412
+ var TestingModuleBuilder = class {
1413
+ overrides = [];
1414
+ constructor(config) {
1415
+ this.config = config;
1416
+ }
1417
+ /**
1418
+ * Override a provider with a custom implementation
1419
+ */
1420
+ overrideProvider(token) {
1421
+ return new ProviderOverrideBuilder(this, token);
1422
+ }
1423
+ /**
1424
+ * Add a provider override (internal use by ProviderOverrideBuilder)
1425
+ *
1426
+ * @internal
1427
+ */
1428
+ addProviderOverride(override) {
1429
+ this.overrides.push(override);
1430
+ return this;
1431
+ }
1432
+ /**
1433
+ * Merge additional environment bindings
1434
+ */
1435
+ withEnv(env) {
1436
+ this.config.env = {
1437
+ ...this.config.env,
1438
+ ...env
1439
+ };
1440
+ return this;
1441
+ }
1442
+ /**
1443
+ * Compile the testing module
1444
+ *
1445
+ * Creates the Application, applies overrides, initializes, and returns TestingModule.
1446
+ */
1447
+ async compile() {
1448
+ const env = getTestEnv(this.config.env);
1449
+ const ctx = { waitUntil };
1450
+ const allImports = [...Test.getBaseModules(), ...this.config.imports ?? []];
1451
+ const app = new Application({
1452
+ module: this.createTestRootModule({
1453
+ imports: allImports,
1454
+ providers: this.config.providers,
1455
+ controllers: this.config.controllers,
1456
+ consumers: this.config.consumers,
1457
+ jobs: this.config.jobs
1458
+ }),
1459
+ logging: {
1460
+ level: this.config.logging?.level ?? LogLevel.ERROR,
1461
+ formatter: this.config.logging?.formatter ?? "pretty"
1462
+ },
1463
+ env,
1464
+ ctx
1465
+ });
1466
+ app.container.registerSingleton(STORAGE_TOKENS.StorageService, FakeStorageService);
1467
+ for (const override of this.overrides) switch (override.type) {
1468
+ case "value":
1469
+ app.container.registerValue(override.token, override.implementation);
1470
+ break;
1471
+ case "class":
1472
+ app.container.registerSingleton(override.token, override.implementation);
1473
+ break;
1474
+ case "factory":
1475
+ app.container.registerFactory(override.token, override.implementation);
1476
+ break;
1477
+ case "existing":
1478
+ app.container.registerExisting(override.token, override.implementation);
1479
+ break;
1480
+ }
1481
+ await app.initialize();
1482
+ return new TestingModule(app, env, ctx);
1483
+ }
1484
+ /**
1485
+ * Create a test root module with the given options
1486
+ */
1487
+ createTestRootModule(options) {
1488
+ let TestRootModule = class TestRootModule {};
1489
+ TestRootModule = __decorate([Module(options)], TestRootModule);
1490
+ return TestRootModule;
1491
+ }
1492
+ };
1493
+ //#endregion
1494
+ //#region src/core/test.ts
1495
+ /**
1496
+ * Test
1497
+ *
1498
+ * Static class for creating testing modules.
1499
+ * Provides a NestJS-style API for configuring test modules.
1500
+ *
1501
+ * @example
1502
+ * ```typescript
1503
+ * // In vitest.setup.ts:
1504
+ * Test.setBaseModules([CoreModule])
1505
+ *
1506
+ * // In test files:
1507
+ * const module = await Test.createTestingModule({
1508
+ * imports: [RegistrationModule, GeoModule],
1509
+ * })
1510
+ * .overrideProvider(EMAIL_TOKENS.EmailService)
1511
+ * .useValue(mockEmailService)
1512
+ * .compile()
1513
+ * ```
1514
+ */
1515
+ var Test = class {
1516
+ /**
1517
+ * Base modules to include in all test modules
1518
+ * Set once in vitest.setup.ts
1519
+ */
1520
+ static baseModules = [];
1521
+ /**
1522
+ * Set base modules to include in all test modules
1523
+ * Should be called once in vitest.setup.ts
1524
+ *
1525
+ * @param modules - Modules to include before test-specific modules (e.g., CoreModule)
1526
+ */
1527
+ static setBaseModules(modules) {
1528
+ this.baseModules = modules;
1529
+ }
1530
+ /**
1531
+ * Get base modules
1532
+ */
1533
+ static getBaseModules() {
1534
+ return this.baseModules;
1535
+ }
1536
+ /**
1537
+ * Create a testing module builder
1538
+ *
1539
+ * @param config - Configuration with modules and optional env overrides
1540
+ * @returns TestingModuleBuilder for configuring and compiling the module
1541
+ */
1542
+ static createTestingModule(config) {
1543
+ return new TestingModuleBuilder(config);
1544
+ }
1545
+ };
1546
+ //#endregion
1547
+ //#region src/core/http/mock-fetch.ts
1548
+ /**
1549
+ * MSW-based fetch mock for declarative HTTP mocking in tests.
1550
+ *
1551
+ * Replaces the old Cloudflare `fetchMock` (undici MockAgent) with MSW's `setupServer`.
1552
+ * Works in both Node.js and workerd test environments.
1553
+ *
1554
+ * @example
1555
+ * ```typescript
1556
+ * import { createMockFetch } from '@stratal/testing'
1557
+ *
1558
+ * const mock = createMockFetch()
1559
+ *
1560
+ * beforeAll(() => mock.listen())
1561
+ * afterEach(() => mock.reset())
1562
+ * afterAll(() => mock.close())
1563
+ *
1564
+ * it('should mock external API', async () => {
1565
+ * mock.mockJsonResponse('https://api.example.com/data', { success: true })
1566
+ *
1567
+ * const response = await fetch('https://api.example.com/data')
1568
+ * const json = await response.json()
1569
+ *
1570
+ * expect(json.success).toBe(true)
1571
+ * })
1572
+ * ```
1573
+ */
1574
+ var MockFetch = class {
1575
+ server;
1576
+ constructor(handlers = []) {
1577
+ this.server = setupServer(...handlers);
1578
+ }
1579
+ /** Start intercepting. Call in beforeAll/beforeEach. */
1580
+ listen() {
1581
+ this.server.listen({ onUnhandledRequest: "error" });
1582
+ }
1583
+ /** Reset runtime handlers. Call in afterEach. */
1584
+ reset() {
1585
+ this.server.resetHandlers();
1586
+ }
1587
+ /** Stop intercepting. Call in afterAll. */
1588
+ close() {
1589
+ this.server.close();
1590
+ }
1591
+ /** Add runtime handler(s) for a single test. */
1592
+ use(...handlers) {
1593
+ this.server.use(...handlers);
1594
+ }
1595
+ /**
1596
+ * Mock a JSON response.
1597
+ *
1598
+ * @param url - Full URL to mock (e.g., 'https://api.example.com/users')
1599
+ * @param data - JSON data to return
1600
+ * @param options - HTTP method, status code, headers
1601
+ *
1602
+ * @example
1603
+ * ```typescript
1604
+ * mock.mockJsonResponse('https://api.example.com/users', { users: [] })
1605
+ * mock.mockJsonResponse('https://api.example.com/users', { created: true }, { method: 'POST', status: 201 })
1606
+ * ```
1607
+ */
1608
+ mockJsonResponse(url, data, options = {}) {
1609
+ const handler = http$1[(options.method ?? "GET").toLowerCase()](url, () => HttpResponse$1.json(data, {
1610
+ status: options.status ?? 200,
1611
+ headers: options.headers
1612
+ }));
1613
+ this.server.use(handler);
1614
+ }
1615
+ /**
1616
+ * Mock an error response.
1617
+ *
1618
+ * @param url - Full URL to mock
1619
+ * @param status - HTTP error status code
1620
+ * @param message - Optional error message
1621
+ * @param options - HTTP method, headers
1622
+ *
1623
+ * @example
1624
+ * ```typescript
1625
+ * mock.mockError('https://api.example.com/fail', 401, 'Unauthorized')
1626
+ * mock.mockError('https://api.example.com/fail', 500, 'Server Error', { method: 'POST' })
1627
+ * ```
1628
+ */
1629
+ mockError(url, status, message, options = {}) {
1630
+ const method = (options.method ?? "GET").toLowerCase();
1631
+ const body = message ? { error: message } : void 0;
1632
+ this.server.use(http$1[method](url, () => HttpResponse$1.json(body, {
1633
+ status,
1634
+ headers: options.headers
1635
+ })));
1636
+ }
1637
+ };
1638
+ /**
1639
+ * Factory function to create a new MockFetch instance
1640
+ *
1641
+ * @param handlers - Optional initial MSW request handlers
1642
+ * @returns A new MockFetch instance
1643
+ *
1644
+ * @example
1645
+ * ```typescript
1646
+ * import { createMockFetch } from '@stratal/testing'
1647
+ *
1648
+ * const mock = createMockFetch()
1649
+ *
1650
+ * beforeAll(() => mock.listen())
1651
+ * afterEach(() => mock.reset())
1652
+ * afterAll(() => mock.close())
1653
+ * ```
1654
+ */
1655
+ function createMockFetch(handlers) {
1656
+ return new MockFetch(handlers);
1657
+ }
1658
+ //#endregion
1659
+ //#region src/types.ts
1660
+ var Seeder = class {};
1661
+ //#endregion
1662
+ //#region src/errors/test-error.ts
1663
+ /**
1664
+ * Base error class for all test framework errors.
1665
+ * Extends from Error and allows easy identification via `instanceof`.
1666
+ */
1667
+ var TestError = class extends Error {
1668
+ constructor(message, cause) {
1669
+ super(message);
1670
+ this.cause = cause;
1671
+ this.name = "TestError";
1672
+ Error.captureStackTrace(this, this.constructor);
1673
+ }
1674
+ };
1675
+ //#endregion
1676
+ //#region src/errors/setup-error.ts
1677
+ /**
1678
+ * Error thrown when test setup fails.
1679
+ * Examples: schema creation failure, migration failure, application bootstrap failure.
1680
+ */
1681
+ var TestSetupError = class extends TestError {
1682
+ constructor(message, cause) {
1683
+ super(`Test setup failed: ${message}`, cause);
1684
+ }
1685
+ };
1686
+ //#endregion
1687
+ export { ActingAs, FakeStorageService, HttpResponse, MockFetch, ProviderOverrideBuilder, Seeder, Test, TestError, TestHttpClient, TestHttpRequest, TestResponse, TestSetupError, TestSseConnection, TestSseRequest, TestWsConnection, TestWsRequest, TestingModule, TestingModuleBuilder, createMockFetch, getTestEnv, http };
1688
+
1689
+ //# sourceMappingURL=index.mjs.map