@stratal/testing 0.0.13 → 0.0.15

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