@stratal/testing 0.0.25 → 0.0.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +9 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +23 -4
- package/dist/index.mjs.map +1 -1
- package/dist/mocks/index.d.mts +2 -1
- package/dist/mocks/index.mjs +2 -1
- package/dist/mocks/noop-rate-limiter-store.d.mts +21 -0
- package/dist/mocks/noop-rate-limiter-store.d.mts.map +1 -0
- package/dist/mocks/noop-rate-limiter-store.mjs +26 -0
- package/dist/mocks/noop-rate-limiter-store.mjs.map +1 -0
- package/dist/test-email-provider-B7cjj97-.d.mts +21 -0
- package/dist/test-email-provider-B7cjj97-.d.mts.map +1 -0
- package/dist/test-email-provider-Dr-nhE0x.mjs +34 -0
- package/dist/test-email-provider-Dr-nhE0x.mjs.map +1 -0
- package/package.json +7 -3
package/dist/index.d.mts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { n as FakeFeatureFlagService } from "./index-qgWNJRdC.mjs";
|
|
2
2
|
import { t as FakeStorageService } from "./index-CrHzUDKX.mjs";
|
|
3
|
+
import { t as TestEmailProvider } from "./test-email-provider-B7cjj97-.mjs";
|
|
3
4
|
import { Application, ApplicationConfig, Constructor, StratalEnv, StratalExecutionContext } from "stratal";
|
|
4
5
|
import { DynamicModule, InjectionToken, ModuleClass, ModuleOptions } from "stratal/module";
|
|
6
|
+
import { ResolvedEmailMessage } from "stratal/email";
|
|
5
7
|
import { Container } from "stratal/di";
|
|
6
8
|
import { ConnectionName, DatabaseService } from "@stratal/framework/database";
|
|
7
9
|
import { Seeder } from "stratal/seeder";
|
|
@@ -627,9 +629,15 @@ declare class TestingModule {
|
|
|
627
629
|
private readonly env;
|
|
628
630
|
private readonly ctx;
|
|
629
631
|
private readonly isolatedDatabase;
|
|
632
|
+
private readonly testEmailProvider;
|
|
630
633
|
private _http;
|
|
631
634
|
private readonly _requestContainer;
|
|
632
|
-
constructor(app: Application, env: StratalEnv, ctx: StratalExecutionContext, isolatedDatabase?: IsolatedDatabase | null);
|
|
635
|
+
constructor(app: Application, env: StratalEnv, ctx: StratalExecutionContext, isolatedDatabase?: IsolatedDatabase | null, testEmailProvider?: TestEmailProvider | null);
|
|
636
|
+
/**
|
|
637
|
+
* Emails recorded by the default {@link TestEmailProvider}, in send order.
|
|
638
|
+
* Empty when the email provider factory was overridden.
|
|
639
|
+
*/
|
|
640
|
+
get sentEmails(): ResolvedEmailMessage[];
|
|
633
641
|
/**
|
|
634
642
|
* Resolve a service from the container
|
|
635
643
|
*/
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/core/http/test-response.ts","../src/core/http/test-http-request.ts","../src/core/http/test-http-client.ts","../src/core/quarry/test-command-result.ts","../src/core/quarry/test-command-request.ts","../src/core/sse/test-sse-connection.ts","../src/core/sse/test-sse-request.ts","../src/core/ws/test-ws-connection.ts","../src/core/ws/test-ws-request.ts","../src/core/testing-module.ts","../src/core/testing-module-builder.ts","../src/core/override/provider-override-builder.ts","../src/core/test.ts","../src/core/http/path-utils.ts","../src/core/http/fetch-mock.types.ts","../src/core/http/mock-fetch.ts","../src/auth/acting-as.ts","../src/errors/test-error.ts","../src/errors/setup-error.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/core/http/test-response.ts","../src/core/http/test-http-request.ts","../src/core/http/test-http-client.ts","../src/core/quarry/test-command-result.ts","../src/core/quarry/test-command-request.ts","../src/core/sse/test-sse-connection.ts","../src/core/sse/test-sse-request.ts","../src/core/ws/test-ws-connection.ts","../src/core/ws/test-ws-request.ts","../src/core/testing-module.ts","../src/core/testing-module-builder.ts","../src/core/override/provider-override-builder.ts","../src/core/test.ts","../src/core/http/path-utils.ts","../src/core/http/fetch-mock.types.ts","../src/core/http/mock-fetch.ts","../src/auth/acting-as.ts","../src/errors/test-error.ts","../src/errors/setup-error.ts"],"mappings":"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"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { a as createDatabaseFromTemplate, c as deriveAdminConnectionString, d as dropDatabase, f as normalizeIsolation, i as buildConnectionString, l as deriveDbName, u as deriveTemplateName } from "./database-B02eYKhE.mjs";
|
|
2
2
|
import { t as FakeStorageService } from "./storage-DhoxWqyF.mjs";
|
|
3
3
|
import { t as __decorate } from "./decorate-B7nr7eBl.mjs";
|
|
4
|
+
import { NoopRateLimiterStore } from "./mocks/noop-rate-limiter-store.mjs";
|
|
5
|
+
import { t as TestEmailProvider } from "./test-email-provider-Dr-nhE0x.mjs";
|
|
4
6
|
import { n as FakeFeatureFlagService, t as FEATURE_FLAG_SERVICE_TOKEN } from "./feature-flags-BiLhfSGh.mjs";
|
|
5
7
|
import { Application } from "stratal";
|
|
6
8
|
import { LogLevel } from "stratal/logger";
|
|
7
9
|
import { Module } from "stratal/module";
|
|
10
|
+
import { EMAIL_TOKENS } from "stratal/email";
|
|
11
|
+
import { RATE_LIMITER_TOKENS } from "stratal/rate-limiter";
|
|
8
12
|
import { STORAGE_TOKENS } from "stratal/storage";
|
|
9
13
|
import { DI_TOKENS } from "stratal/di";
|
|
10
14
|
import { expect } from "vitest";
|
|
@@ -1263,17 +1267,26 @@ var TestingModule = class {
|
|
|
1263
1267
|
env;
|
|
1264
1268
|
ctx;
|
|
1265
1269
|
isolatedDatabase;
|
|
1270
|
+
testEmailProvider;
|
|
1266
1271
|
_http = null;
|
|
1267
1272
|
_requestContainer;
|
|
1268
|
-
constructor(app, env, ctx, isolatedDatabase = null) {
|
|
1273
|
+
constructor(app, env, ctx, isolatedDatabase = null, testEmailProvider = null) {
|
|
1269
1274
|
this.app = app;
|
|
1270
1275
|
this.env = env;
|
|
1271
1276
|
this.ctx = ctx;
|
|
1272
1277
|
this.isolatedDatabase = isolatedDatabase;
|
|
1278
|
+
this.testEmailProvider = testEmailProvider;
|
|
1273
1279
|
const mockContext = this.app.createMockRouterContext();
|
|
1274
1280
|
this._requestContainer = this.app.container.createRequestScope(mockContext);
|
|
1275
1281
|
}
|
|
1276
1282
|
/**
|
|
1283
|
+
* Emails recorded by the default {@link TestEmailProvider}, in send order.
|
|
1284
|
+
* Empty when the email provider factory was overridden.
|
|
1285
|
+
*/
|
|
1286
|
+
get sentEmails() {
|
|
1287
|
+
return this.testEmailProvider?.sent ?? [];
|
|
1288
|
+
}
|
|
1289
|
+
/**
|
|
1277
1290
|
* Resolve a service from the container
|
|
1278
1291
|
*/
|
|
1279
1292
|
get(token) {
|
|
@@ -1402,7 +1415,7 @@ var TestingModule = class {
|
|
|
1402
1415
|
* Cleanup - call in afterAll
|
|
1403
1416
|
*/
|
|
1404
1417
|
async close() {
|
|
1405
|
-
this._requestContainer.dispose();
|
|
1418
|
+
await this._requestContainer.dispose();
|
|
1406
1419
|
try {
|
|
1407
1420
|
await this.app.shutdown();
|
|
1408
1421
|
} finally {
|
|
@@ -1472,9 +1485,10 @@ var TestingModuleBuilder = class {
|
|
|
1472
1485
|
p.catch(() => {});
|
|
1473
1486
|
} };
|
|
1474
1487
|
const isolatedDb = await this.setupIsolatedDatabase(env);
|
|
1488
|
+
let app = null;
|
|
1475
1489
|
try {
|
|
1476
1490
|
const allImports = [...Test.getBaseModules(), ...this.config.imports ?? []];
|
|
1477
|
-
|
|
1491
|
+
app = new Application({
|
|
1478
1492
|
module: this.createTestRootModule({
|
|
1479
1493
|
imports: allImports,
|
|
1480
1494
|
providers: this.config.providers,
|
|
@@ -1493,6 +1507,9 @@ var TestingModuleBuilder = class {
|
|
|
1493
1507
|
await app.initialize();
|
|
1494
1508
|
app.container.registerSingleton(STORAGE_TOKENS.StorageService, FakeStorageService);
|
|
1495
1509
|
app.container.registerSingleton(FEATURE_FLAG_SERVICE_TOKEN, FakeFeatureFlagService);
|
|
1510
|
+
app.container.registerSingleton(RATE_LIMITER_TOKENS.Store, NoopRateLimiterStore);
|
|
1511
|
+
const testEmailProvider = new TestEmailProvider();
|
|
1512
|
+
app.container.registerValue(EMAIL_TOKENS.EmailProviderFactory, { create: () => testEmailProvider });
|
|
1496
1513
|
for (const override of this.overrides) switch (override.type) {
|
|
1497
1514
|
case "value":
|
|
1498
1515
|
app.container.registerValue(override.token, override.implementation);
|
|
@@ -1507,8 +1524,10 @@ var TestingModuleBuilder = class {
|
|
|
1507
1524
|
app.container.registerExisting(override.token, override.implementation);
|
|
1508
1525
|
break;
|
|
1509
1526
|
}
|
|
1510
|
-
|
|
1527
|
+
await app.ensureHono();
|
|
1528
|
+
return new TestingModule(app, env, ctx, isolatedDb, testEmailProvider);
|
|
1511
1529
|
} catch (error) {
|
|
1530
|
+
if (app) await app.shutdown().catch(() => {});
|
|
1512
1531
|
if (isolatedDb) await dropDatabase(isolatedDb.adminConnectionString, isolatedDb.name).catch(() => {});
|
|
1513
1532
|
throw error;
|
|
1514
1533
|
}
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":["http","HttpResponse"],"sources":["../src/core/override/provider-override-builder.ts","../src/core/http/locale-helper.ts","../src/auth/acting-as.ts","../src/core/http/path-utils.ts","../src/core/http/test-response.ts","../src/core/http/test-http-request.ts","../src/core/http/test-http-client.ts","../src/core/quarry/test-command-result.ts","../src/core/quarry/test-command-request.ts","../src/core/sse/test-sse-connection.ts","../src/core/sse/test-sse-request.ts","../src/core/ws/test-ws-connection.ts","../src/core/ws/test-ws-request.ts","../src/core/testing-module.ts","../src/core/testing-module-builder.ts","../src/core/test.ts","../src/core/http/mock-fetch.ts","../src/errors/test-error.ts","../src/errors/setup-error.ts"],"sourcesContent":["import { type Container } from 'stratal/di'\nimport { type InjectionToken } from 'stratal/module'\nimport type { TestingModuleBuilder } from '../testing-module-builder'\n\n/**\n * Provider override configuration\n */\nexport interface ProviderOverrideConfig<T = unknown> {\n token: InjectionToken<T>\n type: 'value' | 'class' | 'factory' | 'existing'\n implementation: T | (new (...args: unknown[]) => T) | ((container: Container) => T) | InjectionToken<T>\n}\n\n/**\n * Fluent builder for provider overrides\n *\n * Provides a NestJS-style API for overriding providers in tests.\n *\n * @example\n * ```typescript\n * const module = await Test.createTestingModule({\n * imports: [RegistrationModule],\n * })\n * .overrideProvider(EMAIL_TOKENS.EmailService)\n * .useValue(mockEmailService)\n * .compile()\n * ```\n */\nexport class ProviderOverrideBuilder<T> {\n constructor(\n private readonly parent: TestingModuleBuilder,\n private readonly token: InjectionToken<T>\n ) { }\n\n /**\n * Use a static value as the provider\n *\n * The value will be registered directly in the container.\n *\n * @param value - The value to use as the provider\n * @returns The parent TestingModuleBuilder for chaining\n */\n useValue(value: T): TestingModuleBuilder {\n return this.parent.addProviderOverride({\n token: this.token,\n type: 'value',\n implementation: value,\n })\n }\n\n /**\n * Use a class as the provider\n *\n * The class will be registered as a singleton.\n *\n * @param cls - The class constructor to use as the provider\n * @returns The parent TestingModuleBuilder for chaining\n */\n useClass(cls: new (...args: unknown[]) => T): TestingModuleBuilder {\n return this.parent.addProviderOverride({\n token: this.token,\n type: 'class',\n implementation: cls,\n })\n }\n\n /**\n * Use a factory function as the provider\n *\n * The factory receives the container and should return the provider instance.\n *\n * @param factory - Factory function that creates the provider\n * @returns The parent TestingModuleBuilder for chaining\n */\n useFactory(factory: (container: Container) => T): TestingModuleBuilder {\n return this.parent.addProviderOverride({\n token: this.token,\n type: 'factory',\n implementation: factory,\n })\n }\n\n /**\n * Use an existing token as the provider (alias)\n *\n * The override token will resolve to the same instance as the target token.\n *\n * @param existingToken - The token to alias\n * @returns The parent TestingModuleBuilder for chaining\n *\n * @example\n * ```typescript\n * const module = await Test.createTestingModule({\n * imports: [MyModule],\n * })\n * .overrideProvider(ABSTRACT_TOKEN)\n * .useExisting(ConcreteService)\n * .compile()\n * ```\n */\n useExisting(existingToken: InjectionToken<T>): TestingModuleBuilder {\n return this.parent.addProviderOverride({\n token: this.token,\n type: 'existing',\n implementation: existingToken,\n })\n }\n}\n","import type { DetectionStrategy, I18nModuleOptions } from 'stratal/i18n'\nimport { I18N_TOKENS } from 'stratal/i18n'\nimport type { TestingModule } from '../testing-module'\n\n/**\n * Resolve the configured detection strategy from the testing module's DI container.\n * Falls back to 'cookie' if I18n is not configured.\n */\nexport function resolveLocaleStrategy(module: TestingModule): DetectionStrategy {\n try {\n const options = module.get<I18nModuleOptions>(I18N_TOKENS.Options)\n const detection = options.detection\n if (detection && 'strategy' in detection && detection.strategy) {\n return detection.strategy\n }\n return 'cookie'\n } catch {\n return 'cookie'\n }\n}\n\n/**\n * Apply locale to request headers based on detection strategy.\n */\nexport function applyLocaleToHeaders(\n headers: Headers,\n locale: string,\n strategy: DetectionStrategy,\n): void {\n switch (strategy) {\n case 'cookie':\n headers.set('Cookie', `locale=${locale}`)\n break\n case 'header':\n headers.set('Accept-Language', locale)\n break\n }\n}\n\n/**\n * Apply locale to URL based on detection strategy.\n */\nexport function applyLocaleToUrl(\n url: URL,\n locale: string,\n strategy: DetectionStrategy,\n): void {\n if (strategy === 'querystring') {\n url.searchParams.set('locale', locale)\n }\n}\n","import { runWithEndpointContext, type AuthEndpointContext } from '@better-auth/core/context'\nimport type { AuthService } from '@stratal/framework/auth'\nimport { type GenericEndpointContext } from 'better-auth'\nimport { setSessionCookie } from 'better-auth/cookies'\nimport { convertSetCookieToCookie } from 'better-auth/test'\n\nasync function makeSignature(value: string, secret: string): Promise<string> {\n const algorithm = { name: 'HMAC', hash: 'SHA-256' }\n const secretBuf = new TextEncoder().encode(secret)\n const key = await crypto.subtle.importKey('raw', secretBuf, algorithm, false, ['sign'])\n const signature = await crypto.subtle.sign(algorithm.name, key, new TextEncoder().encode(value))\n return btoa(String.fromCharCode(...new Uint8Array(signature)))\n}\n\nfunction buildCookieString(name: string, value: string, options: Record<string, unknown> = {}): string {\n const encodedValue = encodeURIComponent(value)\n let str = `${name}=${encodedValue}`\n if (options.path) str += `; Path=${options.path as string}`\n if (options.httpOnly) str += '; HttpOnly'\n if (options.secure) str += '; Secure'\n if (options.sameSite) str += `; SameSite=${options.sameSite as string}`\n if (options.maxAge !== undefined) str += `; Max-Age=${Math.floor(options.maxAge as number)}`\n return str\n}\n\n/**\n * ActingAs\n *\n * Creates authentication sessions for testing.\n * Uses Better Auth's internalAdapter to create real database sessions.\n *\n * @example\n * ```typescript\n * const actingAs = new ActingAs(authService)\n * const headers = await actingAs.createSessionForUser({ id: 'user-123' })\n * ```\n */\nexport class ActingAs {\n constructor(private readonly authService: AuthService) { }\n\n async createSessionForUser(user: { id: string }): Promise<Headers> {\n const auth = this.authService.auth\n const ctx = await auth.$context\n\n const secret = ctx.secret\n\n // Mirror production: session writes always happen inside a Better Auth\n // endpoint, so database hooks receive the endpoint context (e.g. for\n // internalAdapter lookups). The cast bridges better-auth's own generic\n // variance — `$context` is typed with the instance's concrete plugin\n // registry while AuthEndpointContext expects the default generics (same\n // impedance as the GenericEndpointContext cast below).\n const session = await runWithEndpointContext(\n { context: ctx } as unknown as AuthEndpointContext,\n () =>\n ctx.internalAdapter.createSession(\n user.id,\n undefined,\n { ipAddress: '127.0.0.1', userAgent: 'test-client' }\n )\n )\n\n const dbUser = await ctx.internalAdapter.findUserById(user.id)\n if (!dbUser) {\n throw new Error(`User not found: ${user.id}`)\n }\n\n const responseHeaders = new Headers()\n const mockCtx = {\n context: ctx,\n getSignedCookie: () => null,\n setSignedCookie: async (name: string, value: string, _secret: string, options: Record<string, unknown> = {}) => {\n const signature = await makeSignature(value, secret)\n const signedValue = `${value}.${signature}`\n responseHeaders.append('Set-Cookie', buildCookieString(name, signedValue, options))\n },\n setCookie: (name: string, value: string, options: Record<string, unknown> = {}) => {\n responseHeaders.append('Set-Cookie', buildCookieString(name, value, options))\n },\n }\n\n await setSessionCookie(mockCtx as unknown as GenericEndpointContext, { session, user: dbUser }, false)\n return convertSetCookieToCookie(responseHeaders)\n }\n}\n","/**\n * Get value at dot-notation path.\n */\nexport function getValueAtPath(obj: unknown, path: string): unknown {\n const parts = path.split('.')\n let current: unknown = obj\n\n for (const part of parts) {\n if (current === null || current === undefined) {\n return undefined\n }\n current = (current as Record<string, unknown>)[part]\n }\n\n return current\n}\n\n/**\n * Check if a path exists in the object (even if value is null/undefined).\n */\nexport function hasValueAtPath(obj: unknown, path: string): boolean {\n const parts = path.split('.')\n let current: unknown = obj\n\n for (const part of parts) {\n if (current === null || current === undefined) {\n return false\n }\n\n if (typeof current !== 'object') {\n return false\n }\n\n const record = current as Record<string, unknown>\n\n if (!(part in record)) {\n return false\n }\n\n current = record[part]\n }\n\n return true\n}\n","import { Macroable } from 'stratal/macroable'\nimport { expect } from 'vitest'\nimport { getValueAtPath, hasValueAtPath } from './path-utils'\n\n/**\n * TestResponse\n *\n * Wrapper around Response with assertion methods.\n *\n * @example\n * ```typescript\n * response\n * .assertOk()\n * .assertStatus(200)\n * .assertJsonPath('data.id', userId)\n * ```\n */\nexport class TestResponse extends Macroable {\n private jsonData: unknown = null\n private textData: string | null = null\n\n constructor(private readonly response: Response) {\n super()\n }\n\n /**\n * Get the raw Response object.\n */\n get raw(): Response {\n return this.response\n }\n\n /**\n * Get the response status code.\n */\n get status(): number {\n return this.response.status\n }\n\n /**\n * Get response headers.\n */\n get headers(): Headers {\n return this.response.headers\n }\n\n /**\n * Get the response body as JSON.\n */\n async json<T = unknown>(): Promise<T> {\n if (this.jsonData === null) {\n this.jsonData = await this.response.clone().json()\n }\n return this.jsonData as T\n }\n\n /**\n * Get the response body as text.\n */\n async text(): Promise<string> {\n this.textData ??= await this.response.clone().text()\n return this.textData\n }\n\n // ============================================================\n // Status Assertions\n // ============================================================\n\n /**\n * Assert response status is 200 OK.\n */\n assertOk(): this {\n return this.assertStatus(200)\n }\n\n /**\n * Assert response status is 201 Created.\n */\n assertCreated(): this {\n return this.assertStatus(201)\n }\n\n /**\n * Assert response status is 204 No Content.\n */\n assertNoContent(): this {\n return this.assertStatus(204)\n }\n\n /**\n * Assert response status is 400 Bad Request.\n */\n assertBadRequest(): this {\n return this.assertStatus(400)\n }\n\n /**\n * Assert response status is 401 Unauthorized.\n */\n assertUnauthorized(): this {\n return this.assertStatus(401)\n }\n\n /**\n * Assert response status is 403 Forbidden.\n */\n assertForbidden(): this {\n return this.assertStatus(403)\n }\n\n /**\n * Assert response status is 404 Not Found.\n */\n assertNotFound(): this {\n return this.assertStatus(404)\n }\n\n /**\n * Assert response status is 422 Unprocessable Entity.\n */\n assertUnprocessable(): this {\n return this.assertStatus(422)\n }\n\n /**\n * Assert response status is 500 Internal Server Error.\n */\n assertServerError(): this {\n return this.assertStatus(500)\n }\n\n /**\n * Assert response has the given status code.\n */\n assertStatus(expected: number): this {\n expect(\n this.response.status,\n `Expected status ${expected}, got ${this.response.status}`\n ).toBe(expected)\n return this\n }\n\n /**\n * Assert response status is in the 2xx success range.\n */\n assertSuccessful(): this {\n expect(\n this.response.status >= 200 && this.response.status < 300,\n `Expected successful status (2xx), got ${this.response.status}`\n ).toBe(true)\n return this\n }\n\n // ============================================================\n // JSON Assertions\n // ============================================================\n\n /**\n * Assert JSON response contains the given data.\n */\n async assertJson(expected: Record<string, unknown>): Promise<this> {\n const actual = await this.json<Record<string, unknown>>()\n\n for (const [key, value] of Object.entries(expected)) {\n expect(\n actual[key],\n `Expected JSON key \"${key}\" to be ${JSON.stringify(value)}, got ${JSON.stringify(actual[key])}`\n ).toStrictEqual(value)\n }\n\n return this\n }\n\n /**\n * Assert JSON response has value at the given path.\n *\n * @param path - Dot-notation path (e.g., 'data.user.id')\n * @param expected - Expected value at path\n */\n async assertJsonPath(path: string, expected: unknown): Promise<this> {\n const json = await this.json()\n const actual = getValueAtPath(json, path)\n\n expect(\n actual,\n `Expected JSON path \"${path}\" to be ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`\n ).toStrictEqual(expected)\n\n return this\n }\n\n /**\n * Assert JSON response structure matches the given schema.\n */\n async assertJsonStructure(structure: string[]): Promise<this> {\n const json = await this.json<Record<string, unknown>>()\n\n for (const key of structure) {\n expect(\n key in json,\n `Expected JSON to have key \"${key}\", got keys: ${JSON.stringify(Object.keys(json))}`\n ).toBe(true)\n }\n\n return this\n }\n\n /**\n * Assert a JSON path exists (value can be anything, including null).\n *\n * @param path - Dot-notation path (e.g., 'data.user.id')\n */\n async assertJsonPathExists(path: string): Promise<this> {\n const json = await this.json()\n const exists = hasValueAtPath(json, path)\n\n expect(\n exists,\n `Expected JSON path \"${path}\" to exist`\n ).toBe(true)\n\n return this\n }\n\n /**\n * Assert a JSON path does not exist.\n *\n * @param path - Dot-notation path (e.g., 'data.user.deletedAt')\n */\n async assertJsonPathMissing(path: string): Promise<this> {\n const json = await this.json()\n const exists = hasValueAtPath(json, path)\n\n expect(\n exists,\n `Expected JSON path \"${path}\" to not exist`\n ).toBe(false)\n\n return this\n }\n\n /**\n * Assert JSON value at path matches a custom predicate.\n *\n * @param path - Dot-notation path (e.g., 'data.items')\n * @param matcher - Predicate function to validate the value\n */\n async assertJsonPathMatches(\n path: string,\n matcher: (value: unknown) => boolean\n ): Promise<this> {\n const json = await this.json()\n const value = getValueAtPath(json, path)\n\n expect(\n matcher(value),\n `Expected JSON path \"${path}\" to match predicate, got ${JSON.stringify(value)}`\n ).toBe(true)\n\n return this\n }\n\n /**\n * Assert string value at path contains a substring.\n *\n * @param path - Dot-notation path (e.g., 'data.message')\n * @param substring - Substring to search for\n */\n async assertJsonPathContains(path: string, substring: string): Promise<this> {\n const json = await this.json()\n const value = getValueAtPath(json, path)\n\n expect(\n typeof value === 'string',\n `Expected JSON path \"${path}\" to be a string, got ${typeof value}`\n ).toBe(true)\n\n expect(\n (value as string).includes(substring),\n `Expected JSON path \"${path}\" to contain \"${substring}\", got \"${String(value)}\"`\n ).toBe(true)\n\n return this\n }\n\n /**\n * Assert array value at path includes a specific item.\n *\n * @param path - Dot-notation path (e.g., 'data.tags')\n * @param item - Item to search for in the array\n */\n async assertJsonPathIncludes(path: string, item: unknown): Promise<this> {\n const json = await this.json()\n const value = getValueAtPath(json, path)\n\n expect(\n Array.isArray(value),\n `Expected JSON path \"${path}\" to be an array, got ${typeof value}`\n ).toBe(true)\n\n expect(\n (value as unknown[]).includes(item),\n `Expected JSON path \"${path}\" to include ${JSON.stringify(item)}`\n ).toBe(true)\n\n return this\n }\n\n /**\n * Assert array length at path equals the expected count.\n *\n * @param path - Dot-notation path (e.g., 'data.items')\n * @param count - Expected array length\n */\n async assertJsonPathCount(path: string, count: number): Promise<this> {\n const json = await this.json()\n const value = getValueAtPath(json, path)\n\n expect(\n Array.isArray(value),\n `Expected JSON path \"${path}\" to be an array, got ${typeof value}`\n ).toBe(true)\n\n expect(\n (value as unknown[]).length,\n `Expected JSON path \"${path}\" to have ${count} items, got ${(value as unknown[]).length}`\n ).toBe(count)\n\n return this\n }\n\n /**\n * Assert multiple JSON paths at once (batch assertion).\n *\n * @param expectations - Object mapping paths to expected values\n */\n async assertJsonPaths(expectations: Record<string, unknown>): Promise<this> {\n const json = await this.json()\n\n for (const [path, expected] of Object.entries(expectations)) {\n const actual = getValueAtPath(json, path)\n expect(\n actual,\n `Expected JSON path \"${path}\" to be ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`\n ).toStrictEqual(expected)\n }\n\n return this\n }\n\n // ============================================================\n // Header Assertions\n // ============================================================\n\n /**\n * Assert response has the given header.\n */\n assertHeader(name: string, expected?: string): this {\n const actual = this.response.headers.get(name)\n\n expect(\n actual !== null,\n `Expected header \"${name}\" to be present`\n ).toBe(true)\n\n if (expected !== undefined) {\n expect(\n actual,\n `Expected header \"${name}\" to be \"${expected}\", got \"${actual}\"`\n ).toBe(expected)\n }\n\n return this\n }\n\n /**\n * Assert response does not have the given header.\n */\n assertHeaderMissing(name: string): this {\n const actual = this.response.headers.get(name)\n\n expect(\n actual,\n `Expected header \"${name}\" to be absent, but got \"${actual}\"`\n ).toBeNull()\n\n return this\n }\n\n}\n","import type { AuthService } from '@stratal/framework/auth'\nimport { AUTH_SERVICE } from '@stratal/framework/auth'\nimport type { DetectionStrategy } from 'stratal/i18n'\nimport { Macroable } from 'stratal/macroable'\nimport { ActingAs } from '../../auth'\nimport type { TestingModule } from '../testing-module'\nimport { applyLocaleToHeaders, applyLocaleToUrl, resolveLocaleStrategy } from './locale-helper'\nimport { TestResponse } from './test-response'\n\n/**\n * TestHttpRequest\n *\n * Request builder with fluent API for configuring test HTTP requests.\n *\n * @example\n * ```typescript\n * const response = await module.http\n * .post('/api/v1/register')\n * .withBody({ name: 'Test School' })\n * .withHeaders({ 'X-Custom': 'value' })\n * .send()\n * ```\n *\n * @example Authenticated request\n * ```typescript\n * const response = await module.http\n * .get('/api/v1/profile')\n * .actingAs({ id: user.id })\n * .send()\n * ```\n */\nexport class TestHttpRequest extends Macroable {\n\tprotected body: unknown = null\n\tprotected requestHeaders: Headers\n\tprotected actingAsUser: { id: string } | null = null\n\tprotected authResolver: ((module: TestingModule, user: { id: string }) => Promise<Headers>) | null = null\n\tprotected localeConfig: { locale: string; strategy: DetectionStrategy } | null\n\n\tconstructor(\n\t\tprotected readonly method: string,\n\t\tprotected readonly path: string,\n\t\theaders: Headers,\n\t\tprotected readonly module: TestingModule,\n\t\tprotected readonly host: string | null = null,\n\t\tlocaleConfig: { locale: string; strategy: DetectionStrategy } | null = null,\n\t) {\n\t\tsuper()\n\t\tthis.requestHeaders = new Headers(headers)\n\t\tthis.localeConfig = localeConfig\n\t}\n\n\t/**\n\t * Set the request body\n\t */\n\twithBody(data: unknown): this {\n\t\tthis.body = data\n\t\treturn this\n\t}\n\n\t/**\n\t * Add headers to the request\n\t */\n\twithHeaders(headers: Record<string, string>): this {\n\t\tfor (const [key, value] of Object.entries(headers)) {\n\t\t\tthis.requestHeaders.set(key, value)\n\t\t}\n\t\treturn this\n\t}\n\n\t/**\n\t * Set the locale for this request.\n\t * If strategy is not provided, resolves from the module's I18n configuration.\n\t *\n\t * @param locale - Locale code (e.g., 'en', 'fr')\n\t * @param strategy - Detection strategy override\n\t */\n\twithLocale(locale: string, strategy?: DetectionStrategy): this {\n\t\tconst resolved = strategy ?? resolveLocaleStrategy(this.module)\n\t\tthis.localeConfig = { locale, strategy: resolved }\n\t\tapplyLocaleToHeaders(this.requestHeaders, locale, resolved)\n\t\treturn this\n\t}\n\n\t/**\n\t * Set Content-Type to application/json\n\t */\n\tasJson(): this {\n\t\tthis.requestHeaders.set('Content-Type', 'application/json')\n\t\treturn this\n\t}\n\n\t/**\n\t * Authenticate the request as a specific user\n\t */\n\tactingAs(user: { id: string }): this {\n\t\tthis.actingAsUser = user\n\t\tthis.authResolver = null\n\t\treturn this\n\t}\n\n\t/**\n\t * Send the request and return response\n\t *\n\t * Calls module.fetch() - NOT SELF.fetch()\n\t */\n\tasync send(): Promise<TestResponse> {\n\t\tawait this.applyAuthentication()\n\n\t\t// Auto-set Content-Type for body\n\t\tif (this.body && !this.requestHeaders.has('Content-Type')) {\n\t\t\tthis.requestHeaders.set('Content-Type', 'application/json')\n\t\t}\n\n\t\t// Build request\n\t\tconst url = new URL(this.path, `http://${this.host ?? 'localhost'}`)\n\n\t\t// Apply locale to URL for querystring strategy\n\t\tif (this.localeConfig) {\n\t\t\tapplyLocaleToUrl(url, this.localeConfig.locale, this.localeConfig.strategy)\n\t\t}\n\n\t\tconst request = new Request(url.toString(), {\n\t\t\tmethod: this.method,\n\t\t\theaders: this.requestHeaders,\n\t\t\tbody: this.body ? JSON.stringify(this.body) : null,\n\t\t})\n\n\t\t// Call module.fetch() - NO SELF.fetch()\n\t\tconst response = await this.module.fetch(request)\n\t\treturn new TestResponse(response)\n\t}\n\n\tprotected async applyAuthentication(): Promise<void> {\n\t\tif (!this.actingAsUser) return\n\n\t\tif (this.authResolver) {\n\t\t\tconst headers = await this.authResolver(this.module, this.actingAsUser)\n\t\t\tfor (const [key, value] of headers.entries()) {\n\t\t\t\tthis.requestHeaders.set(key, value)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\tawait this.module.runInRequestScope(async () => {\n\t\t\tconst authService = this.module.get<AuthService>(AUTH_SERVICE)\n\t\t\tconst actingAs = new ActingAs(authService)\n\t\t\tconst authHeaders = await actingAs.createSessionForUser(this.actingAsUser!)\n\n\t\t\tfor (const [key, value] of authHeaders.entries()) {\n\t\t\t\tthis.requestHeaders.set(key, value)\n\t\t\t}\n\t\t})\n\t}\n}\n","import type { DetectionStrategy } from 'stratal/i18n'\nimport type { TestingModule } from '../testing-module'\nimport { applyLocaleToHeaders, resolveLocaleStrategy } from './locale-helper'\nimport { TestHttpRequest } from './test-http-request'\n\n/**\n * TestHttpClient\n *\n * Fluent HTTP client for making test requests.\n *\n * @example\n * ```typescript\n * const response = await module.http\n * .forHost('example.com')\n * .post('/api/v1/users')\n * .withBody({ name: 'Test' })\n * .send()\n *\n * response.assertCreated()\n * ```\n */\nexport class TestHttpClient {\n private defaultHeaders: Headers\n private host: string | null\n private localeConfig: { locale: string; strategy: DetectionStrategy } | null\n\n constructor(\n private readonly module: TestingModule,\n host: string | null = null,\n headers: Headers = new Headers(),\n localeConfig: { locale: string; strategy: DetectionStrategy } | null = null,\n ) {\n this.host = host\n this.defaultHeaders = headers\n this.localeConfig = localeConfig\n }\n\n /**\n * Set the host for the request (returns a new client).\n * Also sets the Host header to ensure domain routing works\n * even when the runtime reads the header instead of the URL host.\n */\n forHost(host: string): TestHttpClient {\n const newHeaders = new Headers(this.defaultHeaders)\n newHeaders.set('Host', host)\n return new TestHttpClient(this.module, host, newHeaders, this.localeConfig)\n }\n\n /**\n * Set default headers for all requests (returns a new client)\n */\n withHeaders(headers: Record<string, string>): TestHttpClient {\n const newHeaders = new Headers(this.defaultHeaders)\n for (const [key, value] of Object.entries(headers)) {\n newHeaders.set(key, value)\n }\n return new TestHttpClient(this.module, this.host, newHeaders, this.localeConfig)\n }\n\n /**\n * Set the locale for all requests from this client (returns a new client).\n * If strategy is not provided, resolves from the module's I18n configuration.\n *\n * @param locale - Locale code (e.g., 'en', 'fr')\n * @param strategy - Detection strategy override\n */\n withLocale(locale: string, strategy?: DetectionStrategy): TestHttpClient {\n const resolved = strategy ?? resolveLocaleStrategy(this.module)\n const newHeaders = new Headers(this.defaultHeaders)\n applyLocaleToHeaders(newHeaders, locale, resolved)\n return new TestHttpClient(this.module, this.host, newHeaders, { locale, strategy: resolved })\n }\n\n /**\n * Create a GET request\n */\n get(path: string): TestHttpRequest {\n return this.createRequest('GET', path)\n }\n\n /**\n * Create a POST request\n */\n post(path: string): TestHttpRequest {\n return this.createRequest('POST', path)\n }\n\n /**\n * Create a PUT request\n */\n put(path: string): TestHttpRequest {\n return this.createRequest('PUT', path)\n }\n\n /**\n * Create a PATCH request\n */\n patch(path: string): TestHttpRequest {\n return this.createRequest('PATCH', path)\n }\n\n /**\n * Create a DELETE request\n */\n delete(path: string): TestHttpRequest {\n return this.createRequest('DELETE', path)\n }\n\n private createRequest(method: string, path: string): TestHttpRequest {\n return new TestHttpRequest(method, path, this.defaultHeaders, this.module, this.host, this.localeConfig)\n }\n}\n","import type { CommandResult } from 'stratal/quarry'\nimport { expect } from 'vitest'\n\n/**\n * Fluent assertion wrapper for command results.\n *\n * @example\n * ```typescript\n * const result = await module\n * .quarry('users:create')\n * .withInput({ email: 'test@example.com' })\n * .run()\n *\n * result.assertSuccessful()\n * result.assertOutputContains('User created')\n * ```\n */\nexport class TestCommandResult {\n constructor(private readonly result: CommandResult) {}\n\n get exitCode(): number {\n return this.result.exitCode\n }\n\n get output(): string[] {\n return this.result.output\n }\n\n get errors(): string[] {\n return this.result.errors\n }\n\n assertSuccessful(): this {\n expect(this.result.exitCode, `Expected exit code 0, got ${this.result.exitCode}. Errors: ${this.result.errors.join(', ')}`).toBe(0)\n expect(this.result.errors, 'Expected no errors').toHaveLength(0)\n return this\n }\n\n assertFailed(exitCode?: number): this {\n if (exitCode !== undefined) {\n expect(this.result.exitCode, `Expected exit code ${exitCode}, got ${this.result.exitCode}`).toBe(exitCode)\n } else {\n expect(this.result.exitCode, 'Expected non-zero exit code').not.toBe(0)\n }\n return this\n }\n\n assertExitCode(code: number): this {\n expect(this.result.exitCode, `Expected exit code ${code}, got ${this.result.exitCode}`).toBe(code)\n return this\n }\n\n assertOutputContains(text: string): this {\n const joined = this.result.output.join('\\n')\n expect(joined, `Expected output to contain \"${text}\"`).toContain(text)\n return this\n }\n\n assertOutputMissing(text: string): this {\n const joined = this.result.output.join('\\n')\n expect(joined, `Expected output NOT to contain \"${text}\"`).not.toContain(text)\n return this\n }\n\n assertErrorContains(text: string): this {\n const joined = this.result.errors.join('\\n')\n expect(joined, `Expected errors to contain \"${text}\"`).toContain(text)\n return this\n }\n\n assertErrorMissing(text: string): this {\n const joined = this.result.errors.join('\\n')\n expect(joined, `Expected errors NOT to contain \"${text}\"`).not.toContain(text)\n return this\n }\n}\n","import type { CommandInput } from 'stratal/quarry'\nimport type { TestingModule } from '../testing-module'\nimport { TestCommandResult } from './test-command-result'\n\n/**\n * Fluent builder for testing Quarry commands.\n *\n * @example\n * ```typescript\n * const result = await module\n * .quarry('users:create')\n * .withInput({ email: 'test@example.com', admin: true })\n * .run()\n *\n * result.assertSuccessful()\n * result.assertOutputContains('User created')\n * ```\n */\nexport class TestCommandRequest {\n private _input: CommandInput = {}\n\n constructor(\n private readonly commandName: string,\n private readonly module: TestingModule,\n ) {}\n\n /**\n * Set the flat input for the command.\n */\n withInput(input: CommandInput): this {\n this._input = { ...input }\n return this\n }\n\n /**\n * Execute the command and return a TestCommandResult for assertions.\n */\n async run(): Promise<TestCommandResult> {\n const result = await this.module.application.handleCommand(this.commandName, this._input)\n return new TestCommandResult(result)\n }\n}\n","import { expect } from \"vitest\"\n\n/**\n * Represents a parsed SSE event\n */\nexport interface TestSseEvent {\n\tdata: string\n\tevent?: string\n\tid?: string\n\tretry?: number\n}\n\n/**\n * TestSseConnection\n *\n * Live SSE connection wrapper with assertion helpers for testing.\n *\n * @example\n * ```typescript\n * const sse = await module.sse('/streaming/sse').connect()\n * await sse.assertEvent({ event: 'message', data: 'hello', id: '1' })\n * await sse.waitForEnd()\n * ```\n */\nexport class TestSseConnection {\n\tprivate readonly eventQueue: TestSseEvent[] = []\n\tprivate eventWaiters: ((event: TestSseEvent) => void)[] = []\n\tprivate streamEnded = false\n\tprivate endWaiters: (() => void)[] = []\n\n\tconstructor(private readonly response: Response) {\n\t\tthis.startReading()\n\t}\n\n\t/**\n\t * Wait for the next SSE event\n\t */\n\tasync waitForEvent(timeout = 5000): Promise<TestSseEvent> {\n\t\tif (this.eventQueue.length > 0) {\n\t\t\treturn this.eventQueue.shift()!\n\t\t}\n\n\t\tif (this.streamEnded) {\n\t\t\tthrow new Error('SSE: stream has ended, no more events')\n\t\t}\n\n\t\treturn new Promise<TestSseEvent>((resolve, reject) => {\n\t\t\tconst waiter = (event: TestSseEvent) => {\n\t\t\t\tclearTimeout(timer)\n\t\t\t\tresolve(event)\n\t\t\t}\n\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tconst index = this.eventWaiters.indexOf(waiter)\n\t\t\t\tif (index !== -1) this.eventWaiters.splice(index, 1)\n\t\t\t\treject(new Error(`SSE: no event received within ${timeout}ms`))\n\t\t\t}, timeout)\n\n\t\t\tthis.eventWaiters.push(waiter)\n\t\t})\n\t}\n\n\t/**\n\t * Wait for the stream to end\n\t */\n\tasync waitForEnd(timeout = 5000): Promise<void> {\n\t\tif (this.streamEnded) return\n\n\t\treturn new Promise<void>((resolve, reject) => {\n\t\t\tconst waiter = () => {\n\t\t\t\tclearTimeout(timer)\n\t\t\t\tresolve()\n\t\t\t}\n\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tconst index = this.endWaiters.indexOf(waiter)\n\t\t\t\tif (index !== -1) this.endWaiters.splice(index, 1)\n\t\t\t\treject(new Error(`SSE: stream did not end within ${timeout}ms`))\n\t\t\t}, timeout)\n\n\t\t\tthis.endWaiters.push(waiter)\n\t\t})\n\t}\n\n\t/**\n\t * Collect all remaining events until the stream ends\n\t */\n\tasync collectEvents(timeout = 5000): Promise<TestSseEvent[]> {\n\t\tconst events: TestSseEvent[] = []\n\n\t\tif (this.streamEnded) {\n\t\t\treturn [...this.eventQueue.splice(0)]\n\t\t}\n\n\t\treturn new Promise<TestSseEvent[]>((resolve, reject) => {\n\t\t\t// Listen for new events until stream ends\n\t\t\tconst originalDispatch = this.dispatchEvent.bind(this)\n\t\t\tthis.dispatchEvent = (event: TestSseEvent) => {\n\t\t\t\tevents.push(event)\n\t\t\t\toriginalDispatch(event)\n\t\t\t}\n\n\t\t\tconst endWaiter = () => {\n\t\t\t\tclearTimeout(timer)\n\t\t\t\tthis.dispatchEvent = originalDispatch\n\t\t\t\tresolve(events)\n\t\t\t}\n\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tthis.dispatchEvent = originalDispatch\n\t\t\t\tconst index = this.endWaiters.indexOf(endWaiter)\n\t\t\t\tif (index !== -1) this.endWaiters.splice(index, 1)\n\t\t\t\treject(new Error(`SSE: stream did not end within ${timeout}ms`))\n\t\t\t}, timeout)\n\n\t\t\t// Drain any queued events first\n\t\t\tevents.push(...this.eventQueue.splice(0))\n\n\t\t\tthis.endWaiters.push(endWaiter)\n\t\t})\n\t}\n\n\t/**\n\t * Assert that the next event matches the expected partial shape\n\t */\n\tasync assertEvent(expected: Partial<TestSseEvent>, timeout = 5000): Promise<void> {\n\t\tconst event = await this.waitForEvent(timeout)\n\t\texpect(event).toMatchObject(expected)\n\t}\n\n\t/**\n\t * Assert that the next event's data matches the expected string\n\t */\n\tasync assertEventData(expected: string, timeout = 5000): Promise<void> {\n\t\tconst event = await this.waitForEvent(timeout)\n\t\texpect(event.data, `Expected SSE data \"${expected}\", got \"${event.data}\"`).toBe(expected)\n\t}\n\n\t/**\n\t * Assert that the next event's data is JSON matching the expected value\n\t */\n\tasync assertJsonEventData<T>(expected: T, timeout = 5000): Promise<void> {\n\t\tconst event = await this.waitForEvent(timeout)\n\t\tconst parsed = JSON.parse(event.data) as unknown\n\t\texpect(parsed).toEqual(expected)\n\t}\n\n\t/**\n\t * Access the raw Response\n\t */\n\tget raw(): Response {\n\t\treturn this.response\n\t}\n\n\tprivate startReading(): void {\n\t\tconst body = this.response.body\n\t\tif (!body) {\n\t\t\tthis.streamEnded = true\n\t\t\treturn\n\t\t}\n\n\t\tconst reader = body.getReader() as ReadableStreamDefaultReader<Uint8Array>\n\t\tconst decoder = new TextDecoder()\n\t\tlet buffer = ''\n\n\t\tconst read = async (): Promise<void> => {\n\t\t\ttry {\n\t\t\t\t// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n\t\t\t\twhile (true) {\n\t\t\t\t\tconst { done, value } = await reader.read()\n\n\t\t\t\t\tif (done) {\n\t\t\t\t\t\t// Parse any remaining buffered data\n\t\t\t\t\t\tif (buffer.trim()) {\n\t\t\t\t\t\t\tconst event = this.parseEvent(buffer)\n\t\t\t\t\t\t\tif (event) this.dispatchEvent(event)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthis.streamEnded = true\n\t\t\t\t\t\tfor (const waiter of this.endWaiters) {\n\t\t\t\t\t\t\twaiter()\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthis.endWaiters = []\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\tbuffer += decoder.decode(value, { stream: true })\n\n\t\t\t\t\t// Split on double newlines (SSE event boundary)\n\t\t\t\t\tconst parts = buffer.split('\\n\\n')\n\t\t\t\t\t// Last part may be incomplete, keep it in the buffer\n\t\t\t\t\tbuffer = parts.pop()!\n\n\t\t\t\t\tfor (const part of parts) {\n\t\t\t\t\t\tif (!part.trim()) continue\n\t\t\t\t\t\tconst event = this.parseEvent(part)\n\t\t\t\t\t\tif (event) this.dispatchEvent(event)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\tthis.streamEnded = true\n\t\t\t\tfor (const waiter of this.endWaiters) {\n\t\t\t\t\twaiter()\n\t\t\t\t}\n\t\t\t\tthis.endWaiters = []\n\t\t\t}\n\t\t}\n\n\t\tvoid read()\n\t}\n\n\tprivate parseEvent(raw: string): TestSseEvent | null {\n\t\tconst lines = raw.split('\\n')\n\t\tconst dataLines: string[] = []\n\t\tlet event: string | undefined\n\t\tlet id: string | undefined\n\t\tlet retry: number | undefined\n\n\t\tfor (const line of lines) {\n\t\t\tif (line.startsWith(':')) continue // comment line\n\n\t\t\tconst colonIndex = line.indexOf(':')\n\t\t\tif (colonIndex === -1) continue\n\n\t\t\tconst field = line.slice(0, colonIndex)\n\t\t\t// Strip optional leading space after colon\n\t\t\tconst value = line[colonIndex + 1] === ' ' ? line.slice(colonIndex + 2) : line.slice(colonIndex + 1)\n\n\t\t\tswitch (field) {\n\t\t\t\tcase 'data':\n\t\t\t\t\tdataLines.push(value)\n\t\t\t\t\tbreak\n\t\t\t\tcase 'event':\n\t\t\t\t\tevent = value\n\t\t\t\t\tbreak\n\t\t\t\tcase 'id':\n\t\t\t\t\tid = value\n\t\t\t\t\tbreak\n\t\t\t\tcase 'retry': {\n\t\t\t\t\tconst parsed = parseInt(value, 10)\n\t\t\t\t\tif (!isNaN(parsed)) retry = parsed\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (dataLines.length === 0) return null\n\n\t\tconst result: TestSseEvent = { data: dataLines.join('\\n') }\n\t\tif (event !== undefined) result.event = event\n\t\tif (id !== undefined) result.id = id\n\t\tif (retry !== undefined) result.retry = retry\n\n\t\treturn result\n\t}\n\n\tprivate dispatchEvent(event: TestSseEvent): void {\n\t\tif (this.eventWaiters.length > 0) {\n\t\t\tthis.eventWaiters.shift()!(event)\n\t\t} else {\n\t\t\tthis.eventQueue.push(event)\n\t\t}\n\t}\n}\n","import type { AuthService } from '@stratal/framework/auth'\nimport { AUTH_SERVICE } from '@stratal/framework/auth'\nimport type { DetectionStrategy } from 'stratal/i18n'\nimport { expect } from 'vitest'\nimport { ActingAs } from '../../auth'\nimport type { TestingModule } from '../testing-module'\nimport { applyLocaleToHeaders, resolveLocaleStrategy } from '../http/locale-helper'\nimport { TestSseConnection } from './test-sse-connection'\n\n/**\n * TestSseRequest\n *\n * Builder for SSE connection requests. Follows the TestWsRequest pattern.\n *\n * @example\n * ```typescript\n * const sse = await module.sse('/streaming/sse').connect()\n * await sse.assertEvent({ event: 'message', data: 'hello' })\n * await sse.waitForEnd()\n * ```\n *\n * @example Authenticated SSE\n * ```typescript\n * const sse = await module.sse('/streaming/sse').actingAs({ id: user.id }).connect()\n * ```\n */\nexport class TestSseRequest {\n\tprivate requestHeaders: Headers = new Headers()\n\tprivate actingAsUser: { id: string } | null = null\n\n\tconstructor(\n\t\tprivate readonly path: string,\n\t\tprivate readonly module: TestingModule,\n\t) { }\n\n\t/**\n\t * Add custom headers to the request\n\t */\n\twithHeaders(headers: Record<string, string>): this {\n\t\tfor (const [key, value] of Object.entries(headers)) {\n\t\t\tthis.requestHeaders.set(key, value)\n\t\t}\n\t\treturn this\n\t}\n\n\t/**\n\t * Set the locale for this SSE connection.\n\t * If strategy is not provided, resolves from the module's I18n configuration.\n\t */\n\twithLocale(locale: string, strategy?: DetectionStrategy): this {\n\t\tconst resolved = strategy ?? resolveLocaleStrategy(this.module)\n\t\tapplyLocaleToHeaders(this.requestHeaders, locale, resolved)\n\t\treturn this\n\t}\n\n\t/**\n\t * Authenticate the SSE connection as a specific user\n\t */\n\tactingAs(user: { id: string }): this {\n\t\tthis.actingAsUser = user\n\t\treturn this\n\t}\n\n\t/**\n\t * Send the request and return a live SSE connection\n\t */\n\tasync connect(): Promise<TestSseConnection> {\n\t\tawait this.applyAuthentication()\n\n\t\tthis.requestHeaders.set('Accept', 'text/event-stream')\n\n\t\tconst url = new URL(this.path, 'http://localhost')\n\t\tconst request = new Request(url.toString(), {\n\t\t\theaders: this.requestHeaders,\n\t\t})\n\n\t\tconst response = await this.module.fetch(request)\n\n\t\texpect(\n\t\t\tresponse.status,\n\t\t\t`Expected status 200, got ${response.status}`,\n\t\t).toBe(200)\n\n\t\tconst contentType = response.headers.get('content-type') ?? ''\n\t\texpect(\n\t\t\tcontentType.includes('text/event-stream'),\n\t\t\t`Expected content-type \"text/event-stream\", got \"${contentType}\"`,\n\t\t).toBe(true)\n\n\t\treturn new TestSseConnection(response)\n\t}\n\n\tprivate async applyAuthentication(): Promise<void> {\n\t\tif (!this.actingAsUser) return\n\n\t\tawait this.module.runInRequestScope(async () => {\n\t\t\tconst authService = this.module.get<AuthService>(AUTH_SERVICE)\n\t\t\tconst actingAs = new ActingAs(authService)\n\t\t\tconst authHeaders = this.actingAsUser ? await actingAs.createSessionForUser(this.actingAsUser) : new Headers()\n\n\t\t\tfor (const [key, value] of authHeaders.entries()) {\n\t\t\t\tthis.requestHeaders.set(key, value)\n\t\t\t}\n\t\t})\n\t}\n}\n","import { expect } from \"vitest\";\n\n/**\n * TestWsConnection\n *\n * Live WebSocket connection wrapper with assertion helpers for testing.\n *\n * @example\n * ```typescript\n * const ws = await module.ws('/ws/chat').connect()\n * ws.send('hello')\n * await ws.assertMessage('echo:hello')\n * ws.close()\n * await ws.waitForClose()\n * ```\n */\nexport class TestWsConnection {\n\tprivate readonly messageQueue: (string | ArrayBuffer)[] = []\n\tprivate messageWaiters: ((data: string | ArrayBuffer) => void)[] = []\n\tprivate closeEvent: { code?: number; reason?: string } | null = null\n\tprivate closeWaiters: ((event: { code?: number; reason?: string }) => void)[] = []\n\n\tconstructor(private readonly ws: WebSocket) {\n\t\tthis.ws.addEventListener('message', (event: MessageEvent) => {\n\t\t\tconst data = event.data as string | ArrayBuffer\n\t\t\tif (this.messageWaiters.length > 0) {\n\t\t\t\tthis.messageWaiters.shift()!(data)\n\t\t\t} else {\n\t\t\t\tthis.messageQueue.push(data)\n\t\t\t}\n\t\t})\n\n\t\tthis.ws.addEventListener('close', (event: CloseEvent) => {\n\t\t\tthis.closeEvent = { code: event.code, reason: event.reason }\n\t\t\tfor (const waiter of this.closeWaiters) {\n\t\t\t\twaiter(this.closeEvent)\n\t\t\t}\n\t\t\tthis.closeWaiters = []\n\t\t})\n\t}\n\n\t/**\n\t * Send a message through the WebSocket\n\t */\n\tsend(data: string | ArrayBuffer | Uint8Array): void {\n\t\tthis.ws.send(data)\n\t}\n\n\t/**\n\t * Close the WebSocket connection\n\t */\n\tclose(code?: number, reason?: string): void {\n\t\tthis.ws.close(code, reason)\n\t}\n\n\t/**\n\t * Wait for the next message, returning its data\n\t */\n\tasync waitForMessage(timeout = 5000): Promise<string | ArrayBuffer> {\n\t\tif (this.messageQueue.length > 0) {\n\t\t\treturn this.messageQueue.shift()!\n\t\t}\n\n\t\treturn new Promise<string | ArrayBuffer>((resolve, reject) => {\n\t\t\tconst waiter = (data: string | ArrayBuffer) => {\n\t\t\t\tclearTimeout(timer)\n\t\t\t\tresolve(data)\n\t\t\t}\n\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tconst index = this.messageWaiters.indexOf(waiter)\n\t\t\t\tif (index !== -1) this.messageWaiters.splice(index, 1)\n\t\t\t\treject(new Error(`WebSocket: no message received within ${timeout}ms`))\n\t\t\t}, timeout)\n\n\t\t\tthis.messageWaiters.push(waiter)\n\t\t})\n\t}\n\n\t/**\n\t * Wait for the connection to close\n\t */\n\tasync waitForClose(timeout = 5000): Promise<{ code?: number; reason?: string }> {\n\t\tif (this.closeEvent) {\n\t\t\treturn this.closeEvent\n\t\t}\n\n\t\treturn new Promise<{ code?: number; reason?: string }>((resolve, reject) => {\n\t\t\tconst waiter = (event: { code?: number; reason?: string }) => {\n\t\t\t\tclearTimeout(timer)\n\t\t\t\tresolve(event)\n\t\t\t}\n\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tconst index = this.closeWaiters.indexOf(waiter)\n\t\t\t\tif (index !== -1) this.closeWaiters.splice(index, 1)\n\t\t\t\treject(new Error(`WebSocket: connection did not close within ${timeout}ms`))\n\t\t\t}, timeout)\n\n\t\t\tthis.closeWaiters.push(waiter)\n\t\t})\n\t}\n\n\t/**\n\t * Assert that the next message equals the expected value\n\t */\n\tasync assertMessage(expected: string, timeout = 5000): Promise<void> {\n\t\tconst data = await this.waitForMessage(timeout)\n\t\tconst message = typeof data === 'string' ? data : '[ArrayBuffer]'\n\t\texpect(message, `Expected WebSocket message \"${expected}\", got \"${message}\"`).toBe(expected)\n\t}\n\n\t/**\n\t * Assert that the connection closes, optionally with an expected code\n\t */\n\tasync assertClosed(expectedCode?: number, timeout = 5000): Promise<void> {\n\t\tconst event = await this.waitForClose(timeout)\n\t\tif (expectedCode !== undefined) {\n\t\t\texpect(event.code, `Expected close code ${expectedCode}, got ${event.code}`).toBe(expectedCode)\n\t\t}\n\t}\n\n\t/**\n\t * Access the raw Cloudflare WebSocket\n\t */\n\tget raw(): WebSocket {\n\t\treturn this.ws\n\t}\n}\n","import type { AuthService } from '@stratal/framework/auth'\nimport { AUTH_SERVICE } from '@stratal/framework/auth'\nimport type { DetectionStrategy } from 'stratal/i18n'\nimport { expect } from 'vitest'\nimport { ActingAs } from '../../auth'\nimport type { TestingModule } from '../testing-module'\nimport { applyLocaleToHeaders, resolveLocaleStrategy } from '../http/locale-helper'\nimport { TestWsConnection } from './test-ws-connection'\n\n/**\n * TestWsRequest\n *\n * Builder for WebSocket upgrade requests. Follows the TestHttpRequest pattern.\n *\n * @example\n * ```typescript\n * const ws = await module.ws('/ws/chat').connect()\n * ws.send('hello')\n * await ws.assertMessage('echo:hello')\n * ws.close()\n * ```\n *\n * @example Authenticated WebSocket\n * ```typescript\n * const ws = await module.ws('/ws/chat').actingAs({ id: user.id }).connect()\n * ```\n */\nexport class TestWsRequest {\n\tprivate requestHeaders: Headers = new Headers()\n\tprivate actingAsUser: { id: string } | null = null\n\n\tconstructor(\n\t\tprivate readonly path: string,\n\t\tprivate readonly module: TestingModule,\n\t) { }\n\n\t/**\n\t * Add custom headers to the upgrade request\n\t */\n\twithHeaders(headers: Record<string, string>): this {\n\t\tfor (const [key, value] of Object.entries(headers)) {\n\t\t\tthis.requestHeaders.set(key, value)\n\t\t}\n\t\treturn this\n\t}\n\n\t/**\n\t * Set the locale for this WebSocket connection.\n\t * If strategy is not provided, resolves from the module's I18n configuration.\n\t */\n\twithLocale(locale: string, strategy?: DetectionStrategy): this {\n\t\tconst resolved = strategy ?? resolveLocaleStrategy(this.module)\n\t\tapplyLocaleToHeaders(this.requestHeaders, locale, resolved)\n\t\treturn this\n\t}\n\n\t/**\n\t * Authenticate the WebSocket connection as a specific user\n\t */\n\tactingAs(user: { id: string }): this {\n\t\tthis.actingAsUser = user\n\t\treturn this\n\t}\n\n\t/**\n\t * Send the upgrade request and return a live WebSocket connection\n\t */\n\tasync connect(): Promise<TestWsConnection> {\n\t\tawait this.applyAuthentication()\n\n\t\tthis.requestHeaders.set('Upgrade', 'websocket')\n\t\tthis.requestHeaders.set('Connection', 'Upgrade')\n\t\tthis.requestHeaders.set('Sec-WebSocket-Key', 'dGhlIHNhbXBsZSBub25jZQ==')\n\t\tthis.requestHeaders.set('Sec-WebSocket-Version', '13')\n\n\t\tconst url = new URL(this.path, 'http://localhost')\n\t\tconst request = new Request(url.toString(), {\n\t\t\theaders: this.requestHeaders,\n\t\t})\n\n\t\tconst response = await this.module.fetch(request)\n\n\t\texpect(\n\t\t\tresponse.status,\n\t\t\t`Expected status 101 (Switching Protocols), got ${response.status}`,\n\t\t).toBe(101)\n\n\t\tconst ws = (response as Response & { webSocket: WebSocket | null }).webSocket\n\t\tif (!ws) {\n\t\t\tthrow new Error('Response did not include a WebSocket connection')\n\t\t}\n\n\t\tws.accept()\n\n\t\treturn new TestWsConnection(ws)\n\t}\n\n\tprivate async applyAuthentication(): Promise<void> {\n\t\tif (!this.actingAsUser) return\n\n\t\tawait this.module.runInRequestScope(async () => {\n\t\t\tconst authService = this.module.get<AuthService>(AUTH_SERVICE)\n\t\t\tconst actingAs = new ActingAs(authService)\n\t\t\tconst authHeaders = this.actingAsUser ? await actingAs.createSessionForUser(this.actingAsUser) : new Headers()\n\n\t\t\tfor (const [key, value] of authHeaders.entries()) {\n\t\t\t\tthis.requestHeaders.set(key, value)\n\t\t\t}\n\t\t})\n\t}\n}\n","import type { ConnectionName, DatabaseService } from '@stratal/framework/database'\nimport { connectionSymbol } from '@stratal/framework/database'\nimport type { Application, Constructor, StratalEnv, StratalExecutionContext } from 'stratal'\nimport { DI_TOKENS, type Container } from 'stratal/di'\nimport { type InjectionToken } from 'stratal/module'\nimport { SEEDER_TOKENS, SeederError, type Seeder, type SeederRegistry } from 'stratal/seeder'\nimport { STORAGE_TOKENS } from 'stratal/storage'\nimport { expect } from 'vitest'\nimport { dropDatabase } from '../database'\nimport { FEATURE_FLAG_SERVICE_TOKEN, type FakeFeatureFlagService } from '../feature-flags'\nimport type { FakeStorageService } from '../storage'\nimport { TestHttpClient } from './http/test-http-client'\nimport { TestCommandRequest } from './quarry/test-command-request'\nimport { TestSseRequest } from './sse/test-sse-request'\nimport { TestWsRequest } from './ws/test-ws-request'\n\n/**\n * TestingModule\n *\n * Provides access to the test application, container, HTTP client, and utilities.\n *\n * @example\n * ```typescript\n * const module = await Test.createTestingModule({\n * modules: [RegistrationModule],\n * }).compile()\n *\n * // Make HTTP requests\n * const response = await module.http\n * .post('/api/v1/register')\n * .withBody({ ... })\n * .send()\n *\n * // Access services\n * const service = module.get(REGISTRATION_TOKENS.RegistrationService)\n *\n * // Database utilities\n * await module.truncateDb()\n * await module.seed(UserSeeder)\n * await module.assertDatabaseHas('user', { email: 'test@example.com' })\n *\n * // Cleanup\n * await module.close()\n * ```\n */\n/**\n * Identifies a per-file database created for test isolation, so it can be\n * dropped on teardown. Produced by the testing module builder.\n */\nexport interface IsolatedDatabase {\n /** Name of the cloned database. */\n name: string\n /** Admin (maintenance-database) connection string used to drop it. */\n adminConnectionString: string\n}\n\nexport class TestingModule {\n private _http: TestHttpClient | null = null\n private readonly _requestContainer: Container\n\n constructor(\n private readonly app: Application,\n private readonly env: StratalEnv,\n private readonly ctx: StratalExecutionContext,\n private readonly isolatedDatabase: IsolatedDatabase | null = null,\n ) {\n const mockContext = this.app.createMockRouterContext()\n this._requestContainer = this.app.container.createRequestScope(mockContext)\n }\n\n /**\n * Resolve a service from the container\n */\n get<T>(token: InjectionToken<T>): T {\n return this._requestContainer.resolve(token)\n }\n\n /**\n * Get HTTP test client for making requests\n */\n get http(): TestHttpClient {\n this._http ??= new TestHttpClient(this)\n return this._http\n }\n\n\n /**\n * Get Inertia test client for making Inertia requests\n */\n get inertia(): TestHttpClient {\n return this.http.withHeaders({ 'X-Inertia': 'true', 'X-Inertia-Version': '1' })\n }\n\n /**\n * Get fake storage service for assertions\n */\n get storage(): FakeStorageService {\n return this.get<FakeStorageService>(STORAGE_TOKENS.StorageService)\n }\n\n /**\n * Get the fake feature-flag service to configure flags in tests\n * (e.g. `module.featureFlags.set('my-flag', true)`).\n */\n get featureFlags(): FakeFeatureFlagService {\n return this.get<FakeFeatureFlagService>(FEATURE_FLAG_SERVICE_TOKEN)\n }\n\n /**\n * Create a WebSocket test request builder for the given path\n */\n ws(path: string): TestWsRequest {\n return new TestWsRequest(path, this)\n }\n\n /**\n * Create an SSE test request builder for the given path\n */\n sse(path: string): TestSseRequest {\n return new TestSseRequest(path, this)\n }\n\n /**\n * Create a Quarry command test request builder\n */\n quarry(name: string): TestCommandRequest {\n return new TestCommandRequest(name, this)\n }\n\n /**\n * Get Application instance\n */\n get application(): Application {\n return this.app\n }\n\n /**\n * Get DI Container (request-scoped)\n */\n get container(): Container {\n return this._requestContainer\n }\n\n /**\n * Execute an HTTP request through HonoApp\n */\n async fetch(request: Request): Promise<Response> {\n const hono = await this.app.ensureHono()\n return hono.fetch(request, this.env, this.ctx as ExecutionContext)\n }\n\n /**\n * Run callback in request scope (for DB operations, service access)\n */\n async runInRequestScope<T>(callback: (container: Container) => T | Promise<T>): Promise<T> {\n const mockContext = this.app.createMockRouterContext()\n return this.app.container.runInRequestScope(mockContext, callback)\n }\n\n /**\n * Get database service instance (resolved in request scope)\n */\n getDb(): DatabaseService\n getDb<K extends ConnectionName>(name: K): DatabaseService<K>\n getDb(name?: string): unknown {\n const token = name ? connectionSymbol(name) : DI_TOKENS.Database\n return this._requestContainer.resolve(token)\n }\n\n /**\n * Truncate all non-prisma tables in the database\n */\n async truncateDb(name?: ConnectionName): Promise<void> {\n const db = this.getDb(name!)\n const tables = await db.$queryRaw<{ tablename: string }[]>`\n SELECT tablename::text as tablename FROM pg_tables\n WHERE schemaname = current_schema()\n AND tablename NOT LIKE '_prisma%'\n `\n if (tables.length === 0) return\n const tableList = tables.map((t) => `\"${t.tablename}\"`).join(', ')\n await db.$executeRawUnsafe(`TRUNCATE ${tableList} RESTART IDENTITY CASCADE`)\n }\n\n /**\n * Run seeders by class constructor in the request-scoped container\n */\n async seed(...SeederClasses: Constructor<Seeder>[]): Promise<void> {\n const registry = this._requestContainer.resolve<SeederRegistry>(SEEDER_TOKENS.SeederRegistry)\n for (const SeederClass of SeederClasses) {\n if (!registry.has(SeederClass)) {\n throw new SeederError(`Seeder \"${SeederClass.name}\" is not registered`)\n }\n await registry.run(SeederClass, { container: this._requestContainer })\n }\n }\n\n /**\n * Assert that a record exists in the database\n */\n async assertDatabaseHas(table: string, data: Record<string, unknown>, name?: ConnectionName): Promise<void> {\n const db = this.getDb(name!)\n const model = (db as unknown as Record<string, unknown>)[table] as { findFirst: (opts: unknown) => Promise<unknown> }\n const result = await model.findFirst({ where: data })\n expect(result, `Expected ${table} with ${JSON.stringify(data)}`).not.toBeNull()\n }\n\n /**\n * Assert that a record does not exist in the database\n */\n async assertDatabaseMissing(table: string, data: Record<string, unknown>, name?: ConnectionName): Promise<void> {\n const db = this.getDb(name!)\n const model = (db as unknown as Record<string, unknown>)[table] as { findFirst: (opts: unknown) => Promise<unknown> }\n const result = await model.findFirst({ where: data })\n expect(result, `Expected ${table} NOT to have ${JSON.stringify(data)}`).toBeNull()\n }\n\n /**\n * Assert the number of records in a table\n */\n async assertDatabaseCount(table: string, expected: number, name?: ConnectionName): Promise<void> {\n const db = this.getDb(name!)\n const model = (db as unknown as Record<string, unknown>)[table] as { count: () => Promise<number> }\n const actual = await model.count()\n expect(actual, `Expected ${table} count ${expected}, got ${actual}`).toBe(expected)\n }\n\n /**\n * Cleanup - call in afterAll\n */\n async close(): Promise<void> {\n this._requestContainer.dispose()\n try {\n await this.app.shutdown()\n }\n finally {\n // Drop the per-file database AFTER shutdown so the app's pool connection is\n // released; `WITH (FORCE)` evicts any that lingers. Runs even if shutdown\n // throws, otherwise a failed shutdown would leak the database until the\n // next run's stale-database sweep.\n //\n // A drop failure here is non-fatal: the next run's connection-guarded sweep\n // reclaims the database. Swallow it (warn only) so a passing suite isn't\n // marked failed by a teardown hiccup.\n if (this.isolatedDatabase) {\n try {\n await dropDatabase(this.isolatedDatabase.adminConnectionString, this.isolatedDatabase.name)\n } catch (error) {\n console.warn(\n `[stratal-testing] Failed to drop isolated test database \"${this.isolatedDatabase.name}\"; ` +\n 'it will be reclaimed by the next run\\'s stale-database sweep.',\n error,\n )\n }\n }\n }\n }\n}\n","import {\n Application,\n type ApplicationConfig,\n type Constructor,\n type StratalEnv,\n type StratalExecutionContext,\n} from 'stratal'\nimport type { ExceptionHandler } from 'stratal/errors'\nimport { type Container } from 'stratal/di'\nimport { LogLevel } from 'stratal/logger'\nimport { type InjectionToken, Module, type ModuleClass, type ModuleOptions } from 'stratal/module'\nimport { STORAGE_TOKENS } from 'stratal/storage'\nimport {\n BINDING_ENV_VAR,\n buildConnectionString,\n createDatabaseFromTemplate,\n DEFAULT_DB_BINDING,\n deriveAdminConnectionString,\n deriveDbName,\n deriveTemplateName,\n dropDatabase,\n ISOLATION_ENV_VAR,\n normalizeIsolation,\n} from '../database'\nimport { FakeStorageService } from '../storage'\nimport { FEATURE_FLAG_SERVICE_TOKEN, FakeFeatureFlagService } from '../feature-flags'\nimport { ProviderOverrideBuilder, type ProviderOverrideConfig } from './override'\nimport { Test } from './test'\nimport { TestingModule, type IsolatedDatabase } from './testing-module'\n\n/**\n * Configuration for creating a testing module\n *\n * Extends ModuleOptions to support all module properties like NestJS.\n *\n * @example\n * ```typescript\n * const module = await Test.createTestingModule({\n * imports: [RegistrationModule, GeoModule],\n * providers: [{ provide: MOCK_TOKEN, useValue: mockValue }],\n * controllers: [TestController],\n * }).compile()\n * ```\n */\nexport interface TestingModuleConfig extends ModuleOptions {\n /** Optional environment overrides */\n env?: Partial<StratalEnv>\n /** Logging configuration. Defaults: level=ERROR, formatter='json' */\n logging?: ApplicationConfig['logging']\n /**\n * Custom exception handler. Mirrors `ApplicationConfig.exceptionHandler`.\n * Must be passed at compile time — `overrideProvider(DI_TOKENS.ExceptionHandler)`\n * cannot replace it because the framework resolves the handler during\n * `initialize()` (before overrides apply).\n */\n exceptionHandler?: Constructor<ExceptionHandler>\n}\n\n/**\n * Builder for creating test modules with provider overrides\n */\nexport class TestingModuleBuilder {\n private overrides: ProviderOverrideConfig<object>[] = []\n\n constructor(private config: TestingModuleConfig) { }\n\n /**\n * Override a provider with a custom implementation\n */\n overrideProvider<T>(token: InjectionToken<T>): ProviderOverrideBuilder<T> {\n return new ProviderOverrideBuilder(this, token)\n }\n\n /**\n * Add a provider override (internal use by ProviderOverrideBuilder)\n *\n * @internal\n */\n addProviderOverride<T>(override: ProviderOverrideConfig<T>): this {\n this.overrides.push(override as ProviderOverrideConfig<object>)\n return this\n }\n\n /**\n * Merge additional environment bindings\n */\n withEnv(env: Partial<StratalEnv>): this {\n this.config.env = { ...this.config.env, ...env }\n return this\n }\n\n private async getCloudflareWorkers() {\n try {\n return await import('cloudflare:workers')\n } catch {\n return null\n }\n }\n\n /**\n * Compile the testing module\n *\n * Creates the Application, applies overrides, initializes, and returns TestingModule.\n */\n async compile(): Promise<TestingModule> {\n const cf = await this.getCloudflareWorkers()\n\n const env = { ...cf?.env, ...this.config.env } as StratalEnv\n const ctx: StratalExecutionContext = {\n waitUntil: cf ? cf.waitUntil : (p) => {\n p.catch(() => {\n //\n })\n },\n }\n\n // When database isolation is enabled, clone the migrated template into a\n // fresh per-file database and point this app's DB binding at it. The clone\n // is dropped in TestingModule.close(). No-op in 'shared' mode or when the\n // app has no DB binding.\n const isolatedDb = await this.setupIsolatedDatabase(env)\n\n // Everything between the clone above and returning the TestingModule (which\n // owns the drop via close()) can throw — module init, overrides, etc. If it\n // does, nothing would ever drop the freshly cloned database. Drop it (FORCE)\n // on failure before rethrowing so a compile() error doesn't leak a database.\n try {\n // Build root module from config\n const baseModules = Test.getBaseModules()\n const allImports = [...baseModules, ...(this.config.imports ?? [])]\n\n const rootModule = this.createTestRootModule({\n imports: allImports,\n providers: this.config.providers,\n controllers: this.config.controllers,\n consumers: this.config.consumers,\n jobs: this.config.jobs,\n })\n\n const app = new Application({\n module: rootModule,\n logging: {\n level: this.config.logging?.level ?? LogLevel.ERROR,\n formatter: this.config.logging?.formatter ?? 'pretty',\n },\n env,\n ctx,\n exceptionHandler: this.config.exceptionHandler,\n })\n\n await app.initialize()\n\n // Auto-register FakeStorageService after initialize so it replaces module-registered StorageService\n app.container.registerSingleton(STORAGE_TOKENS.StorageService, FakeStorageService)\n\n // Auto-register FakeFeatureFlagService so feature-gated code resolves without a\n // real Cloudflare Flagship binding. Inert for apps that don't use feature flags.\n app.container.registerSingleton(FEATURE_FLAG_SERVICE_TOKEN, FakeFeatureFlagService)\n\n // Apply user overrides AFTER initialize so they replace module-registered providers\n for (const override of this.overrides) {\n switch (override.type) {\n case 'value':\n app.container.registerValue(override.token, override.implementation)\n break\n case 'class':\n app.container.registerSingleton(\n override.token,\n override.implementation as Constructor\n )\n break\n case 'factory':\n app.container.registerFactory(\n override.token,\n override.implementation as (c: Container) => object\n )\n break\n case 'existing':\n app.container.registerExisting(\n override.token,\n override.implementation as InjectionToken<object>\n )\n break\n }\n }\n\n return new TestingModule(app, env, ctx, isolatedDb)\n } catch (error) {\n if (isolatedDb) {\n await dropDatabase(isolatedDb.adminConnectionString, isolatedDb.name).catch(() => {\n // Best-effort cleanup; the next run's stale-database sweep is the backstop.\n })\n }\n throw error\n }\n }\n\n /**\n * When `STRATAL_TEST_DB_ISOLATION=database`, create a fresh database cloned\n * from the migrated template and rewrite the Hyperdrive binding's\n * `connectionString` to target it. The binding name is configurable via\n * `stratalTest({ database: { binding } })` (defaults to `DB`). Returns the\n * created name + admin connection so `close()` can drop it, or `null` when\n * isolation is off. Throws if isolation is on but the binding is missing — a\n * misconfiguration, not a case to skip.\n */\n private async setupIsolatedDatabase(env: StratalEnv): Promise<IsolatedDatabase | null> {\n const bindings = env as unknown as Record<string, unknown>\n const isolation = normalizeIsolation(bindings[ISOLATION_ENV_VAR] as string | undefined)\n if (isolation !== 'database') return null\n\n const bindingName = (bindings[BINDING_ENV_VAR] as string | undefined) ?? DEFAULT_DB_BINDING\n const db = bindings[bindingName] as { connectionString?: string } | undefined\n const base = db?.connectionString\n if (!base) {\n throw new Error(\n `Database isolation is 'database' but no \\`${bindingName}\\` Hyperdrive binding with a connectionString was found. ` +\n `Provide it via \\`stratalTest({ miniflare: { hyperdrives: { ${bindingName} } } })\\`, or set isolation to 'shared'.`,\n )\n }\n\n const adminConnectionString = deriveAdminConnectionString(base)\n const name = deriveDbName(base)\n await createDatabaseFromTemplate(adminConnectionString, name, deriveTemplateName(base))\n\n // Point the binding at the per-file database by overriding only its\n // connectionString. Clone with the original prototype + descriptors so the\n // Hyperdrive binding's methods (e.g. `connect()`) and other fields survive,\n // and never mutate the shared cloudflare env object.\n const isolated = Object.create(\n Object.getPrototypeOf(db) as object | null,\n Object.getOwnPropertyDescriptors(db),\n ) as Record<string, unknown>\n Object.defineProperty(isolated, 'connectionString', {\n value: buildConnectionString(base, name),\n enumerable: true,\n configurable: true,\n writable: true,\n })\n bindings[bindingName] = isolated\n\n return { name, adminConnectionString }\n }\n\n /**\n * Create a test root module with the given options\n */\n private createTestRootModule(options: ModuleOptions): ModuleClass {\n @Module(options)\n class TestRootModule { }\n return TestRootModule\n }\n}\n","import { type DynamicModule, type ModuleClass } from 'stratal/module'\nimport { TestingModuleBuilder, type TestingModuleConfig } from './testing-module-builder'\n\n/**\n * Test\n *\n * Static class for creating testing modules.\n * Provides a NestJS-style API for configuring test modules.\n *\n * @example\n * ```typescript\n * // In vitest.setup.ts:\n * Test.setBaseModules([CoreModule])\n *\n * // In test files:\n * const module = await Test.createTestingModule({\n * imports: [RegistrationModule, GeoModule],\n * })\n * .overrideProvider(EMAIL_TOKENS.EmailService)\n * .useValue(mockEmailService)\n * .compile()\n * ```\n */\nexport class Test {\n /**\n * Base modules to include in all test modules\n * Set once in vitest.setup.ts\n */\n private static baseModules: (ModuleClass | DynamicModule)[] = []\n\n /**\n * Set base modules to include in all test modules\n * Should be called once in vitest.setup.ts\n *\n * @param modules - Modules to include before test-specific modules (e.g., CoreModule)\n */\n static setBaseModules(modules: (ModuleClass | DynamicModule)[]): void {\n this.baseModules = modules\n }\n\n /**\n * Get base modules\n */\n static getBaseModules(): (ModuleClass | DynamicModule)[] {\n return this.baseModules\n }\n\n /**\n * Create a testing module builder\n *\n * @param config - Configuration with modules and optional env overrides\n * @returns TestingModuleBuilder for configuring and compiling the module\n */\n static createTestingModule(config: TestingModuleConfig): TestingModuleBuilder {\n return new TestingModuleBuilder(config)\n }\n}\n","import { http, HttpResponse, type RequestHandler } from 'msw'\nimport { setupServer, type SetupServer } from 'msw/node'\nimport type { MockErrorOptions, MockJsonOptions } from './fetch-mock.types'\n\ntype HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' | 'head' | 'options'\n\n/**\n * MSW-based fetch mock for declarative HTTP mocking in tests.\n *\n * Replaces the old Cloudflare `fetchMock` (undici MockAgent) with MSW's `setupServer`.\n * Works in both Node.js and workerd test environments.\n *\n * @example\n * ```typescript\n * import { createMockFetch } from '@stratal/testing'\n *\n * const mock = createMockFetch()\n *\n * beforeAll(() => mock.listen())\n * afterEach(() => mock.reset())\n * afterAll(() => mock.close())\n *\n * it('should mock external API', async () => {\n * mock.mockJsonResponse('https://api.example.com/data', { success: true })\n *\n * const response = await fetch('https://api.example.com/data')\n * const json = await response.json()\n *\n * expect(json.success).toBe(true)\n * })\n * ```\n */\nexport class MockFetch {\n private server: SetupServer\n\n constructor(handlers: RequestHandler[] = []) {\n this.server = setupServer(...handlers)\n }\n\n /** Start intercepting. Call in beforeAll/beforeEach. */\n listen() {\n this.server.listen({ onUnhandledRequest: 'error' })\n }\n\n /** Reset runtime handlers. Call in afterEach. */\n reset() {\n this.server.resetHandlers()\n }\n\n /** Stop intercepting. Call in afterAll. */\n close() {\n this.server.close()\n }\n\n /** Add runtime handler(s) for a single test. */\n use(...handlers: RequestHandler[]) {\n this.server.use(...handlers)\n }\n\n /**\n * Mock a JSON response.\n *\n * @param url - Full URL to mock (e.g., 'https://api.example.com/users')\n * @param data - JSON data to return\n * @param options - HTTP method, status code, headers\n *\n * @example\n * ```typescript\n * mock.mockJsonResponse('https://api.example.com/users', { users: [] })\n * mock.mockJsonResponse('https://api.example.com/users', { created: true }, { method: 'POST', status: 201 })\n * ```\n */\n mockJsonResponse(url: string, data: Record<string, unknown> | unknown[], options: MockJsonOptions = {}) {\n const method = (options.method ?? 'GET').toLowerCase() as HttpMethod\n const handler = http[method](url, () =>\n HttpResponse.json(data, {\n status: options.status ?? 200,\n headers: options.headers,\n }),\n )\n this.server.use(handler)\n }\n\n /**\n * Mock an error response.\n *\n * @param url - Full URL to mock\n * @param status - HTTP error status code\n * @param message - Optional error message\n * @param options - HTTP method, headers\n *\n * @example\n * ```typescript\n * mock.mockError('https://api.example.com/fail', 401, 'Unauthorized')\n * mock.mockError('https://api.example.com/fail', 500, 'Server Error', { method: 'POST' })\n * ```\n */\n mockError(url: string, status: number, message?: string, options: MockErrorOptions = {}) {\n const method = (options.method ?? 'GET').toLowerCase() as HttpMethod\n const body = message ? { error: message } : undefined\n this.server.use(\n http[method](url, () =>\n HttpResponse.json(body, { status, headers: options.headers }),\n ),\n )\n }\n}\n\n/**\n * Factory function to create a new MockFetch instance\n *\n * @param handlers - Optional initial MSW request handlers\n * @returns A new MockFetch instance\n *\n * @example\n * ```typescript\n * import { createMockFetch } from '@stratal/testing'\n *\n * const mock = createMockFetch()\n *\n * beforeAll(() => mock.listen())\n * afterEach(() => mock.reset())\n * afterAll(() => mock.close())\n * ```\n */\nexport function createMockFetch(handlers?: RequestHandler[]): MockFetch {\n return new MockFetch(handlers)\n}\n","/**\n * Base error class for all test framework errors.\n * Extends from Error and allows easy identification via `instanceof`.\n */\nexport class TestError extends Error {\n constructor(\n message: string,\n public readonly cause?: Error\n ) {\n super(message)\n this.name = 'TestError'\n\n // Maintain proper stack trace\n Error.captureStackTrace(this, this.constructor)\n }\n}\n","import { TestError } from './test-error'\n\n/**\n * Error thrown when test setup fails.\n * Examples: schema creation failure, migration failure, application bootstrap failure.\n */\nexport class TestSetupError extends TestError {\n constructor(message: string, cause?: Error) {\n super(`Test setup failed: ${message}`, cause)\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["http","HttpResponse"],"sources":["../src/core/override/provider-override-builder.ts","../src/core/http/locale-helper.ts","../src/auth/acting-as.ts","../src/core/http/path-utils.ts","../src/core/http/test-response.ts","../src/core/http/test-http-request.ts","../src/core/http/test-http-client.ts","../src/core/quarry/test-command-result.ts","../src/core/quarry/test-command-request.ts","../src/core/sse/test-sse-connection.ts","../src/core/sse/test-sse-request.ts","../src/core/ws/test-ws-connection.ts","../src/core/ws/test-ws-request.ts","../src/core/testing-module.ts","../src/core/testing-module-builder.ts","../src/core/test.ts","../src/core/http/mock-fetch.ts","../src/errors/test-error.ts","../src/errors/setup-error.ts"],"sourcesContent":["import { type Container } from 'stratal/di'\nimport { type InjectionToken } from 'stratal/module'\nimport type { TestingModuleBuilder } from '../testing-module-builder'\n\n/**\n * Provider override configuration\n */\nexport interface ProviderOverrideConfig<T = unknown> {\n token: InjectionToken<T>\n type: 'value' | 'class' | 'factory' | 'existing'\n implementation: T | (new (...args: unknown[]) => T) | ((container: Container) => T) | InjectionToken<T>\n}\n\n/**\n * Fluent builder for provider overrides\n *\n * Provides a NestJS-style API for overriding providers in tests.\n *\n * @example\n * ```typescript\n * const module = await Test.createTestingModule({\n * imports: [RegistrationModule],\n * })\n * .overrideProvider(EMAIL_TOKENS.EmailService)\n * .useValue(mockEmailService)\n * .compile()\n * ```\n */\nexport class ProviderOverrideBuilder<T> {\n constructor(\n private readonly parent: TestingModuleBuilder,\n private readonly token: InjectionToken<T>\n ) { }\n\n /**\n * Use a static value as the provider\n *\n * The value will be registered directly in the container.\n *\n * @param value - The value to use as the provider\n * @returns The parent TestingModuleBuilder for chaining\n */\n useValue(value: T): TestingModuleBuilder {\n return this.parent.addProviderOverride({\n token: this.token,\n type: 'value',\n implementation: value,\n })\n }\n\n /**\n * Use a class as the provider\n *\n * The class will be registered as a singleton.\n *\n * @param cls - The class constructor to use as the provider\n * @returns The parent TestingModuleBuilder for chaining\n */\n useClass(cls: new (...args: unknown[]) => T): TestingModuleBuilder {\n return this.parent.addProviderOverride({\n token: this.token,\n type: 'class',\n implementation: cls,\n })\n }\n\n /**\n * Use a factory function as the provider\n *\n * The factory receives the container and should return the provider instance.\n *\n * @param factory - Factory function that creates the provider\n * @returns The parent TestingModuleBuilder for chaining\n */\n useFactory(factory: (container: Container) => T): TestingModuleBuilder {\n return this.parent.addProviderOverride({\n token: this.token,\n type: 'factory',\n implementation: factory,\n })\n }\n\n /**\n * Use an existing token as the provider (alias)\n *\n * The override token will resolve to the same instance as the target token.\n *\n * @param existingToken - The token to alias\n * @returns The parent TestingModuleBuilder for chaining\n *\n * @example\n * ```typescript\n * const module = await Test.createTestingModule({\n * imports: [MyModule],\n * })\n * .overrideProvider(ABSTRACT_TOKEN)\n * .useExisting(ConcreteService)\n * .compile()\n * ```\n */\n useExisting(existingToken: InjectionToken<T>): TestingModuleBuilder {\n return this.parent.addProviderOverride({\n token: this.token,\n type: 'existing',\n implementation: existingToken,\n })\n }\n}\n","import type { DetectionStrategy, I18nModuleOptions } from 'stratal/i18n'\nimport { I18N_TOKENS } from 'stratal/i18n'\nimport type { TestingModule } from '../testing-module'\n\n/**\n * Resolve the configured detection strategy from the testing module's DI container.\n * Falls back to 'cookie' if I18n is not configured.\n */\nexport function resolveLocaleStrategy(module: TestingModule): DetectionStrategy {\n try {\n const options = module.get<I18nModuleOptions>(I18N_TOKENS.Options)\n const detection = options.detection\n if (detection && 'strategy' in detection && detection.strategy) {\n return detection.strategy\n }\n return 'cookie'\n } catch {\n return 'cookie'\n }\n}\n\n/**\n * Apply locale to request headers based on detection strategy.\n */\nexport function applyLocaleToHeaders(\n headers: Headers,\n locale: string,\n strategy: DetectionStrategy,\n): void {\n switch (strategy) {\n case 'cookie':\n headers.set('Cookie', `locale=${locale}`)\n break\n case 'header':\n headers.set('Accept-Language', locale)\n break\n }\n}\n\n/**\n * Apply locale to URL based on detection strategy.\n */\nexport function applyLocaleToUrl(\n url: URL,\n locale: string,\n strategy: DetectionStrategy,\n): void {\n if (strategy === 'querystring') {\n url.searchParams.set('locale', locale)\n }\n}\n","import { runWithEndpointContext, type AuthEndpointContext } from '@better-auth/core/context'\nimport type { AuthService } from '@stratal/framework/auth'\nimport { type GenericEndpointContext } from 'better-auth'\nimport { setSessionCookie } from 'better-auth/cookies'\nimport { convertSetCookieToCookie } from 'better-auth/test'\n\nasync function makeSignature(value: string, secret: string): Promise<string> {\n const algorithm = { name: 'HMAC', hash: 'SHA-256' }\n const secretBuf = new TextEncoder().encode(secret)\n const key = await crypto.subtle.importKey('raw', secretBuf, algorithm, false, ['sign'])\n const signature = await crypto.subtle.sign(algorithm.name, key, new TextEncoder().encode(value))\n return btoa(String.fromCharCode(...new Uint8Array(signature)))\n}\n\nfunction buildCookieString(name: string, value: string, options: Record<string, unknown> = {}): string {\n const encodedValue = encodeURIComponent(value)\n let str = `${name}=${encodedValue}`\n if (options.path) str += `; Path=${options.path as string}`\n if (options.httpOnly) str += '; HttpOnly'\n if (options.secure) str += '; Secure'\n if (options.sameSite) str += `; SameSite=${options.sameSite as string}`\n if (options.maxAge !== undefined) str += `; Max-Age=${Math.floor(options.maxAge as number)}`\n return str\n}\n\n/**\n * ActingAs\n *\n * Creates authentication sessions for testing.\n * Uses Better Auth's internalAdapter to create real database sessions.\n *\n * @example\n * ```typescript\n * const actingAs = new ActingAs(authService)\n * const headers = await actingAs.createSessionForUser({ id: 'user-123' })\n * ```\n */\nexport class ActingAs {\n constructor(private readonly authService: AuthService) { }\n\n async createSessionForUser(user: { id: string }): Promise<Headers> {\n const auth = this.authService.auth\n const ctx = await auth.$context\n\n const secret = ctx.secret\n\n // Mirror production: session writes always happen inside a Better Auth\n // endpoint, so database hooks receive the endpoint context (e.g. for\n // internalAdapter lookups). The cast bridges better-auth's own generic\n // variance — `$context` is typed with the instance's concrete plugin\n // registry while AuthEndpointContext expects the default generics (same\n // impedance as the GenericEndpointContext cast below).\n const session = await runWithEndpointContext(\n { context: ctx } as unknown as AuthEndpointContext,\n () =>\n ctx.internalAdapter.createSession(\n user.id,\n undefined,\n { ipAddress: '127.0.0.1', userAgent: 'test-client' }\n )\n )\n\n const dbUser = await ctx.internalAdapter.findUserById(user.id)\n if (!dbUser) {\n throw new Error(`User not found: ${user.id}`)\n }\n\n const responseHeaders = new Headers()\n const mockCtx = {\n context: ctx,\n getSignedCookie: () => null,\n setSignedCookie: async (name: string, value: string, _secret: string, options: Record<string, unknown> = {}) => {\n const signature = await makeSignature(value, secret)\n const signedValue = `${value}.${signature}`\n responseHeaders.append('Set-Cookie', buildCookieString(name, signedValue, options))\n },\n setCookie: (name: string, value: string, options: Record<string, unknown> = {}) => {\n responseHeaders.append('Set-Cookie', buildCookieString(name, value, options))\n },\n }\n\n await setSessionCookie(mockCtx as unknown as GenericEndpointContext, { session, user: dbUser }, false)\n return convertSetCookieToCookie(responseHeaders)\n }\n}\n","/**\n * Get value at dot-notation path.\n */\nexport function getValueAtPath(obj: unknown, path: string): unknown {\n const parts = path.split('.')\n let current: unknown = obj\n\n for (const part of parts) {\n if (current === null || current === undefined) {\n return undefined\n }\n current = (current as Record<string, unknown>)[part]\n }\n\n return current\n}\n\n/**\n * Check if a path exists in the object (even if value is null/undefined).\n */\nexport function hasValueAtPath(obj: unknown, path: string): boolean {\n const parts = path.split('.')\n let current: unknown = obj\n\n for (const part of parts) {\n if (current === null || current === undefined) {\n return false\n }\n\n if (typeof current !== 'object') {\n return false\n }\n\n const record = current as Record<string, unknown>\n\n if (!(part in record)) {\n return false\n }\n\n current = record[part]\n }\n\n return true\n}\n","import { Macroable } from 'stratal/macroable'\nimport { expect } from 'vitest'\nimport { getValueAtPath, hasValueAtPath } from './path-utils'\n\n/**\n * TestResponse\n *\n * Wrapper around Response with assertion methods.\n *\n * @example\n * ```typescript\n * response\n * .assertOk()\n * .assertStatus(200)\n * .assertJsonPath('data.id', userId)\n * ```\n */\nexport class TestResponse extends Macroable {\n private jsonData: unknown = null\n private textData: string | null = null\n\n constructor(private readonly response: Response) {\n super()\n }\n\n /**\n * Get the raw Response object.\n */\n get raw(): Response {\n return this.response\n }\n\n /**\n * Get the response status code.\n */\n get status(): number {\n return this.response.status\n }\n\n /**\n * Get response headers.\n */\n get headers(): Headers {\n return this.response.headers\n }\n\n /**\n * Get the response body as JSON.\n */\n async json<T = unknown>(): Promise<T> {\n if (this.jsonData === null) {\n this.jsonData = await this.response.clone().json()\n }\n return this.jsonData as T\n }\n\n /**\n * Get the response body as text.\n */\n async text(): Promise<string> {\n this.textData ??= await this.response.clone().text()\n return this.textData\n }\n\n // ============================================================\n // Status Assertions\n // ============================================================\n\n /**\n * Assert response status is 200 OK.\n */\n assertOk(): this {\n return this.assertStatus(200)\n }\n\n /**\n * Assert response status is 201 Created.\n */\n assertCreated(): this {\n return this.assertStatus(201)\n }\n\n /**\n * Assert response status is 204 No Content.\n */\n assertNoContent(): this {\n return this.assertStatus(204)\n }\n\n /**\n * Assert response status is 400 Bad Request.\n */\n assertBadRequest(): this {\n return this.assertStatus(400)\n }\n\n /**\n * Assert response status is 401 Unauthorized.\n */\n assertUnauthorized(): this {\n return this.assertStatus(401)\n }\n\n /**\n * Assert response status is 403 Forbidden.\n */\n assertForbidden(): this {\n return this.assertStatus(403)\n }\n\n /**\n * Assert response status is 404 Not Found.\n */\n assertNotFound(): this {\n return this.assertStatus(404)\n }\n\n /**\n * Assert response status is 422 Unprocessable Entity.\n */\n assertUnprocessable(): this {\n return this.assertStatus(422)\n }\n\n /**\n * Assert response status is 500 Internal Server Error.\n */\n assertServerError(): this {\n return this.assertStatus(500)\n }\n\n /**\n * Assert response has the given status code.\n */\n assertStatus(expected: number): this {\n expect(\n this.response.status,\n `Expected status ${expected}, got ${this.response.status}`\n ).toBe(expected)\n return this\n }\n\n /**\n * Assert response status is in the 2xx success range.\n */\n assertSuccessful(): this {\n expect(\n this.response.status >= 200 && this.response.status < 300,\n `Expected successful status (2xx), got ${this.response.status}`\n ).toBe(true)\n return this\n }\n\n // ============================================================\n // JSON Assertions\n // ============================================================\n\n /**\n * Assert JSON response contains the given data.\n */\n async assertJson(expected: Record<string, unknown>): Promise<this> {\n const actual = await this.json<Record<string, unknown>>()\n\n for (const [key, value] of Object.entries(expected)) {\n expect(\n actual[key],\n `Expected JSON key \"${key}\" to be ${JSON.stringify(value)}, got ${JSON.stringify(actual[key])}`\n ).toStrictEqual(value)\n }\n\n return this\n }\n\n /**\n * Assert JSON response has value at the given path.\n *\n * @param path - Dot-notation path (e.g., 'data.user.id')\n * @param expected - Expected value at path\n */\n async assertJsonPath(path: string, expected: unknown): Promise<this> {\n const json = await this.json()\n const actual = getValueAtPath(json, path)\n\n expect(\n actual,\n `Expected JSON path \"${path}\" to be ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`\n ).toStrictEqual(expected)\n\n return this\n }\n\n /**\n * Assert JSON response structure matches the given schema.\n */\n async assertJsonStructure(structure: string[]): Promise<this> {\n const json = await this.json<Record<string, unknown>>()\n\n for (const key of structure) {\n expect(\n key in json,\n `Expected JSON to have key \"${key}\", got keys: ${JSON.stringify(Object.keys(json))}`\n ).toBe(true)\n }\n\n return this\n }\n\n /**\n * Assert a JSON path exists (value can be anything, including null).\n *\n * @param path - Dot-notation path (e.g., 'data.user.id')\n */\n async assertJsonPathExists(path: string): Promise<this> {\n const json = await this.json()\n const exists = hasValueAtPath(json, path)\n\n expect(\n exists,\n `Expected JSON path \"${path}\" to exist`\n ).toBe(true)\n\n return this\n }\n\n /**\n * Assert a JSON path does not exist.\n *\n * @param path - Dot-notation path (e.g., 'data.user.deletedAt')\n */\n async assertJsonPathMissing(path: string): Promise<this> {\n const json = await this.json()\n const exists = hasValueAtPath(json, path)\n\n expect(\n exists,\n `Expected JSON path \"${path}\" to not exist`\n ).toBe(false)\n\n return this\n }\n\n /**\n * Assert JSON value at path matches a custom predicate.\n *\n * @param path - Dot-notation path (e.g., 'data.items')\n * @param matcher - Predicate function to validate the value\n */\n async assertJsonPathMatches(\n path: string,\n matcher: (value: unknown) => boolean\n ): Promise<this> {\n const json = await this.json()\n const value = getValueAtPath(json, path)\n\n expect(\n matcher(value),\n `Expected JSON path \"${path}\" to match predicate, got ${JSON.stringify(value)}`\n ).toBe(true)\n\n return this\n }\n\n /**\n * Assert string value at path contains a substring.\n *\n * @param path - Dot-notation path (e.g., 'data.message')\n * @param substring - Substring to search for\n */\n async assertJsonPathContains(path: string, substring: string): Promise<this> {\n const json = await this.json()\n const value = getValueAtPath(json, path)\n\n expect(\n typeof value === 'string',\n `Expected JSON path \"${path}\" to be a string, got ${typeof value}`\n ).toBe(true)\n\n expect(\n (value as string).includes(substring),\n `Expected JSON path \"${path}\" to contain \"${substring}\", got \"${String(value)}\"`\n ).toBe(true)\n\n return this\n }\n\n /**\n * Assert array value at path includes a specific item.\n *\n * @param path - Dot-notation path (e.g., 'data.tags')\n * @param item - Item to search for in the array\n */\n async assertJsonPathIncludes(path: string, item: unknown): Promise<this> {\n const json = await this.json()\n const value = getValueAtPath(json, path)\n\n expect(\n Array.isArray(value),\n `Expected JSON path \"${path}\" to be an array, got ${typeof value}`\n ).toBe(true)\n\n expect(\n (value as unknown[]).includes(item),\n `Expected JSON path \"${path}\" to include ${JSON.stringify(item)}`\n ).toBe(true)\n\n return this\n }\n\n /**\n * Assert array length at path equals the expected count.\n *\n * @param path - Dot-notation path (e.g., 'data.items')\n * @param count - Expected array length\n */\n async assertJsonPathCount(path: string, count: number): Promise<this> {\n const json = await this.json()\n const value = getValueAtPath(json, path)\n\n expect(\n Array.isArray(value),\n `Expected JSON path \"${path}\" to be an array, got ${typeof value}`\n ).toBe(true)\n\n expect(\n (value as unknown[]).length,\n `Expected JSON path \"${path}\" to have ${count} items, got ${(value as unknown[]).length}`\n ).toBe(count)\n\n return this\n }\n\n /**\n * Assert multiple JSON paths at once (batch assertion).\n *\n * @param expectations - Object mapping paths to expected values\n */\n async assertJsonPaths(expectations: Record<string, unknown>): Promise<this> {\n const json = await this.json()\n\n for (const [path, expected] of Object.entries(expectations)) {\n const actual = getValueAtPath(json, path)\n expect(\n actual,\n `Expected JSON path \"${path}\" to be ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`\n ).toStrictEqual(expected)\n }\n\n return this\n }\n\n // ============================================================\n // Header Assertions\n // ============================================================\n\n /**\n * Assert response has the given header.\n */\n assertHeader(name: string, expected?: string): this {\n const actual = this.response.headers.get(name)\n\n expect(\n actual !== null,\n `Expected header \"${name}\" to be present`\n ).toBe(true)\n\n if (expected !== undefined) {\n expect(\n actual,\n `Expected header \"${name}\" to be \"${expected}\", got \"${actual}\"`\n ).toBe(expected)\n }\n\n return this\n }\n\n /**\n * Assert response does not have the given header.\n */\n assertHeaderMissing(name: string): this {\n const actual = this.response.headers.get(name)\n\n expect(\n actual,\n `Expected header \"${name}\" to be absent, but got \"${actual}\"`\n ).toBeNull()\n\n return this\n }\n\n}\n","import type { AuthService } from '@stratal/framework/auth'\nimport { AUTH_SERVICE } from '@stratal/framework/auth'\nimport type { DetectionStrategy } from 'stratal/i18n'\nimport { Macroable } from 'stratal/macroable'\nimport { ActingAs } from '../../auth'\nimport type { TestingModule } from '../testing-module'\nimport { applyLocaleToHeaders, applyLocaleToUrl, resolveLocaleStrategy } from './locale-helper'\nimport { TestResponse } from './test-response'\n\n/**\n * TestHttpRequest\n *\n * Request builder with fluent API for configuring test HTTP requests.\n *\n * @example\n * ```typescript\n * const response = await module.http\n * .post('/api/v1/register')\n * .withBody({ name: 'Test School' })\n * .withHeaders({ 'X-Custom': 'value' })\n * .send()\n * ```\n *\n * @example Authenticated request\n * ```typescript\n * const response = await module.http\n * .get('/api/v1/profile')\n * .actingAs({ id: user.id })\n * .send()\n * ```\n */\nexport class TestHttpRequest extends Macroable {\n\tprotected body: unknown = null\n\tprotected requestHeaders: Headers\n\tprotected actingAsUser: { id: string } | null = null\n\tprotected authResolver: ((module: TestingModule, user: { id: string }) => Promise<Headers>) | null = null\n\tprotected localeConfig: { locale: string; strategy: DetectionStrategy } | null\n\n\tconstructor(\n\t\tprotected readonly method: string,\n\t\tprotected readonly path: string,\n\t\theaders: Headers,\n\t\tprotected readonly module: TestingModule,\n\t\tprotected readonly host: string | null = null,\n\t\tlocaleConfig: { locale: string; strategy: DetectionStrategy } | null = null,\n\t) {\n\t\tsuper()\n\t\tthis.requestHeaders = new Headers(headers)\n\t\tthis.localeConfig = localeConfig\n\t}\n\n\t/**\n\t * Set the request body\n\t */\n\twithBody(data: unknown): this {\n\t\tthis.body = data\n\t\treturn this\n\t}\n\n\t/**\n\t * Add headers to the request\n\t */\n\twithHeaders(headers: Record<string, string>): this {\n\t\tfor (const [key, value] of Object.entries(headers)) {\n\t\t\tthis.requestHeaders.set(key, value)\n\t\t}\n\t\treturn this\n\t}\n\n\t/**\n\t * Set the locale for this request.\n\t * If strategy is not provided, resolves from the module's I18n configuration.\n\t *\n\t * @param locale - Locale code (e.g., 'en', 'fr')\n\t * @param strategy - Detection strategy override\n\t */\n\twithLocale(locale: string, strategy?: DetectionStrategy): this {\n\t\tconst resolved = strategy ?? resolveLocaleStrategy(this.module)\n\t\tthis.localeConfig = { locale, strategy: resolved }\n\t\tapplyLocaleToHeaders(this.requestHeaders, locale, resolved)\n\t\treturn this\n\t}\n\n\t/**\n\t * Set Content-Type to application/json\n\t */\n\tasJson(): this {\n\t\tthis.requestHeaders.set('Content-Type', 'application/json')\n\t\treturn this\n\t}\n\n\t/**\n\t * Authenticate the request as a specific user\n\t */\n\tactingAs(user: { id: string }): this {\n\t\tthis.actingAsUser = user\n\t\tthis.authResolver = null\n\t\treturn this\n\t}\n\n\t/**\n\t * Send the request and return response\n\t *\n\t * Calls module.fetch() - NOT SELF.fetch()\n\t */\n\tasync send(): Promise<TestResponse> {\n\t\tawait this.applyAuthentication()\n\n\t\t// Auto-set Content-Type for body\n\t\tif (this.body && !this.requestHeaders.has('Content-Type')) {\n\t\t\tthis.requestHeaders.set('Content-Type', 'application/json')\n\t\t}\n\n\t\t// Build request\n\t\tconst url = new URL(this.path, `http://${this.host ?? 'localhost'}`)\n\n\t\t// Apply locale to URL for querystring strategy\n\t\tif (this.localeConfig) {\n\t\t\tapplyLocaleToUrl(url, this.localeConfig.locale, this.localeConfig.strategy)\n\t\t}\n\n\t\tconst request = new Request(url.toString(), {\n\t\t\tmethod: this.method,\n\t\t\theaders: this.requestHeaders,\n\t\t\tbody: this.body ? JSON.stringify(this.body) : null,\n\t\t})\n\n\t\t// Call module.fetch() - NO SELF.fetch()\n\t\tconst response = await this.module.fetch(request)\n\t\treturn new TestResponse(response)\n\t}\n\n\tprotected async applyAuthentication(): Promise<void> {\n\t\tif (!this.actingAsUser) return\n\n\t\tif (this.authResolver) {\n\t\t\tconst headers = await this.authResolver(this.module, this.actingAsUser)\n\t\t\tfor (const [key, value] of headers.entries()) {\n\t\t\t\tthis.requestHeaders.set(key, value)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\tawait this.module.runInRequestScope(async () => {\n\t\t\tconst authService = this.module.get<AuthService>(AUTH_SERVICE)\n\t\t\tconst actingAs = new ActingAs(authService)\n\t\t\tconst authHeaders = await actingAs.createSessionForUser(this.actingAsUser!)\n\n\t\t\tfor (const [key, value] of authHeaders.entries()) {\n\t\t\t\tthis.requestHeaders.set(key, value)\n\t\t\t}\n\t\t})\n\t}\n}\n","import type { DetectionStrategy } from 'stratal/i18n'\nimport type { TestingModule } from '../testing-module'\nimport { applyLocaleToHeaders, resolveLocaleStrategy } from './locale-helper'\nimport { TestHttpRequest } from './test-http-request'\n\n/**\n * TestHttpClient\n *\n * Fluent HTTP client for making test requests.\n *\n * @example\n * ```typescript\n * const response = await module.http\n * .forHost('example.com')\n * .post('/api/v1/users')\n * .withBody({ name: 'Test' })\n * .send()\n *\n * response.assertCreated()\n * ```\n */\nexport class TestHttpClient {\n private defaultHeaders: Headers\n private host: string | null\n private localeConfig: { locale: string; strategy: DetectionStrategy } | null\n\n constructor(\n private readonly module: TestingModule,\n host: string | null = null,\n headers: Headers = new Headers(),\n localeConfig: { locale: string; strategy: DetectionStrategy } | null = null,\n ) {\n this.host = host\n this.defaultHeaders = headers\n this.localeConfig = localeConfig\n }\n\n /**\n * Set the host for the request (returns a new client).\n * Also sets the Host header to ensure domain routing works\n * even when the runtime reads the header instead of the URL host.\n */\n forHost(host: string): TestHttpClient {\n const newHeaders = new Headers(this.defaultHeaders)\n newHeaders.set('Host', host)\n return new TestHttpClient(this.module, host, newHeaders, this.localeConfig)\n }\n\n /**\n * Set default headers for all requests (returns a new client)\n */\n withHeaders(headers: Record<string, string>): TestHttpClient {\n const newHeaders = new Headers(this.defaultHeaders)\n for (const [key, value] of Object.entries(headers)) {\n newHeaders.set(key, value)\n }\n return new TestHttpClient(this.module, this.host, newHeaders, this.localeConfig)\n }\n\n /**\n * Set the locale for all requests from this client (returns a new client).\n * If strategy is not provided, resolves from the module's I18n configuration.\n *\n * @param locale - Locale code (e.g., 'en', 'fr')\n * @param strategy - Detection strategy override\n */\n withLocale(locale: string, strategy?: DetectionStrategy): TestHttpClient {\n const resolved = strategy ?? resolveLocaleStrategy(this.module)\n const newHeaders = new Headers(this.defaultHeaders)\n applyLocaleToHeaders(newHeaders, locale, resolved)\n return new TestHttpClient(this.module, this.host, newHeaders, { locale, strategy: resolved })\n }\n\n /**\n * Create a GET request\n */\n get(path: string): TestHttpRequest {\n return this.createRequest('GET', path)\n }\n\n /**\n * Create a POST request\n */\n post(path: string): TestHttpRequest {\n return this.createRequest('POST', path)\n }\n\n /**\n * Create a PUT request\n */\n put(path: string): TestHttpRequest {\n return this.createRequest('PUT', path)\n }\n\n /**\n * Create a PATCH request\n */\n patch(path: string): TestHttpRequest {\n return this.createRequest('PATCH', path)\n }\n\n /**\n * Create a DELETE request\n */\n delete(path: string): TestHttpRequest {\n return this.createRequest('DELETE', path)\n }\n\n private createRequest(method: string, path: string): TestHttpRequest {\n return new TestHttpRequest(method, path, this.defaultHeaders, this.module, this.host, this.localeConfig)\n }\n}\n","import type { CommandResult } from 'stratal/quarry'\nimport { expect } from 'vitest'\n\n/**\n * Fluent assertion wrapper for command results.\n *\n * @example\n * ```typescript\n * const result = await module\n * .quarry('users:create')\n * .withInput({ email: 'test@example.com' })\n * .run()\n *\n * result.assertSuccessful()\n * result.assertOutputContains('User created')\n * ```\n */\nexport class TestCommandResult {\n constructor(private readonly result: CommandResult) {}\n\n get exitCode(): number {\n return this.result.exitCode\n }\n\n get output(): string[] {\n return this.result.output\n }\n\n get errors(): string[] {\n return this.result.errors\n }\n\n assertSuccessful(): this {\n expect(this.result.exitCode, `Expected exit code 0, got ${this.result.exitCode}. Errors: ${this.result.errors.join(', ')}`).toBe(0)\n expect(this.result.errors, 'Expected no errors').toHaveLength(0)\n return this\n }\n\n assertFailed(exitCode?: number): this {\n if (exitCode !== undefined) {\n expect(this.result.exitCode, `Expected exit code ${exitCode}, got ${this.result.exitCode}`).toBe(exitCode)\n } else {\n expect(this.result.exitCode, 'Expected non-zero exit code').not.toBe(0)\n }\n return this\n }\n\n assertExitCode(code: number): this {\n expect(this.result.exitCode, `Expected exit code ${code}, got ${this.result.exitCode}`).toBe(code)\n return this\n }\n\n assertOutputContains(text: string): this {\n const joined = this.result.output.join('\\n')\n expect(joined, `Expected output to contain \"${text}\"`).toContain(text)\n return this\n }\n\n assertOutputMissing(text: string): this {\n const joined = this.result.output.join('\\n')\n expect(joined, `Expected output NOT to contain \"${text}\"`).not.toContain(text)\n return this\n }\n\n assertErrorContains(text: string): this {\n const joined = this.result.errors.join('\\n')\n expect(joined, `Expected errors to contain \"${text}\"`).toContain(text)\n return this\n }\n\n assertErrorMissing(text: string): this {\n const joined = this.result.errors.join('\\n')\n expect(joined, `Expected errors NOT to contain \"${text}\"`).not.toContain(text)\n return this\n }\n}\n","import type { CommandInput } from 'stratal/quarry'\nimport type { TestingModule } from '../testing-module'\nimport { TestCommandResult } from './test-command-result'\n\n/**\n * Fluent builder for testing Quarry commands.\n *\n * @example\n * ```typescript\n * const result = await module\n * .quarry('users:create')\n * .withInput({ email: 'test@example.com', admin: true })\n * .run()\n *\n * result.assertSuccessful()\n * result.assertOutputContains('User created')\n * ```\n */\nexport class TestCommandRequest {\n private _input: CommandInput = {}\n\n constructor(\n private readonly commandName: string,\n private readonly module: TestingModule,\n ) {}\n\n /**\n * Set the flat input for the command.\n */\n withInput(input: CommandInput): this {\n this._input = { ...input }\n return this\n }\n\n /**\n * Execute the command and return a TestCommandResult for assertions.\n */\n async run(): Promise<TestCommandResult> {\n const result = await this.module.application.handleCommand(this.commandName, this._input)\n return new TestCommandResult(result)\n }\n}\n","import { expect } from \"vitest\"\n\n/**\n * Represents a parsed SSE event\n */\nexport interface TestSseEvent {\n\tdata: string\n\tevent?: string\n\tid?: string\n\tretry?: number\n}\n\n/**\n * TestSseConnection\n *\n * Live SSE connection wrapper with assertion helpers for testing.\n *\n * @example\n * ```typescript\n * const sse = await module.sse('/streaming/sse').connect()\n * await sse.assertEvent({ event: 'message', data: 'hello', id: '1' })\n * await sse.waitForEnd()\n * ```\n */\nexport class TestSseConnection {\n\tprivate readonly eventQueue: TestSseEvent[] = []\n\tprivate eventWaiters: ((event: TestSseEvent) => void)[] = []\n\tprivate streamEnded = false\n\tprivate endWaiters: (() => void)[] = []\n\n\tconstructor(private readonly response: Response) {\n\t\tthis.startReading()\n\t}\n\n\t/**\n\t * Wait for the next SSE event\n\t */\n\tasync waitForEvent(timeout = 5000): Promise<TestSseEvent> {\n\t\tif (this.eventQueue.length > 0) {\n\t\t\treturn this.eventQueue.shift()!\n\t\t}\n\n\t\tif (this.streamEnded) {\n\t\t\tthrow new Error('SSE: stream has ended, no more events')\n\t\t}\n\n\t\treturn new Promise<TestSseEvent>((resolve, reject) => {\n\t\t\tconst waiter = (event: TestSseEvent) => {\n\t\t\t\tclearTimeout(timer)\n\t\t\t\tresolve(event)\n\t\t\t}\n\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tconst index = this.eventWaiters.indexOf(waiter)\n\t\t\t\tif (index !== -1) this.eventWaiters.splice(index, 1)\n\t\t\t\treject(new Error(`SSE: no event received within ${timeout}ms`))\n\t\t\t}, timeout)\n\n\t\t\tthis.eventWaiters.push(waiter)\n\t\t})\n\t}\n\n\t/**\n\t * Wait for the stream to end\n\t */\n\tasync waitForEnd(timeout = 5000): Promise<void> {\n\t\tif (this.streamEnded) return\n\n\t\treturn new Promise<void>((resolve, reject) => {\n\t\t\tconst waiter = () => {\n\t\t\t\tclearTimeout(timer)\n\t\t\t\tresolve()\n\t\t\t}\n\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tconst index = this.endWaiters.indexOf(waiter)\n\t\t\t\tif (index !== -1) this.endWaiters.splice(index, 1)\n\t\t\t\treject(new Error(`SSE: stream did not end within ${timeout}ms`))\n\t\t\t}, timeout)\n\n\t\t\tthis.endWaiters.push(waiter)\n\t\t})\n\t}\n\n\t/**\n\t * Collect all remaining events until the stream ends\n\t */\n\tasync collectEvents(timeout = 5000): Promise<TestSseEvent[]> {\n\t\tconst events: TestSseEvent[] = []\n\n\t\tif (this.streamEnded) {\n\t\t\treturn [...this.eventQueue.splice(0)]\n\t\t}\n\n\t\treturn new Promise<TestSseEvent[]>((resolve, reject) => {\n\t\t\t// Listen for new events until stream ends\n\t\t\tconst originalDispatch = this.dispatchEvent.bind(this)\n\t\t\tthis.dispatchEvent = (event: TestSseEvent) => {\n\t\t\t\tevents.push(event)\n\t\t\t\toriginalDispatch(event)\n\t\t\t}\n\n\t\t\tconst endWaiter = () => {\n\t\t\t\tclearTimeout(timer)\n\t\t\t\tthis.dispatchEvent = originalDispatch\n\t\t\t\tresolve(events)\n\t\t\t}\n\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tthis.dispatchEvent = originalDispatch\n\t\t\t\tconst index = this.endWaiters.indexOf(endWaiter)\n\t\t\t\tif (index !== -1) this.endWaiters.splice(index, 1)\n\t\t\t\treject(new Error(`SSE: stream did not end within ${timeout}ms`))\n\t\t\t}, timeout)\n\n\t\t\t// Drain any queued events first\n\t\t\tevents.push(...this.eventQueue.splice(0))\n\n\t\t\tthis.endWaiters.push(endWaiter)\n\t\t})\n\t}\n\n\t/**\n\t * Assert that the next event matches the expected partial shape\n\t */\n\tasync assertEvent(expected: Partial<TestSseEvent>, timeout = 5000): Promise<void> {\n\t\tconst event = await this.waitForEvent(timeout)\n\t\texpect(event).toMatchObject(expected)\n\t}\n\n\t/**\n\t * Assert that the next event's data matches the expected string\n\t */\n\tasync assertEventData(expected: string, timeout = 5000): Promise<void> {\n\t\tconst event = await this.waitForEvent(timeout)\n\t\texpect(event.data, `Expected SSE data \"${expected}\", got \"${event.data}\"`).toBe(expected)\n\t}\n\n\t/**\n\t * Assert that the next event's data is JSON matching the expected value\n\t */\n\tasync assertJsonEventData<T>(expected: T, timeout = 5000): Promise<void> {\n\t\tconst event = await this.waitForEvent(timeout)\n\t\tconst parsed = JSON.parse(event.data) as unknown\n\t\texpect(parsed).toEqual(expected)\n\t}\n\n\t/**\n\t * Access the raw Response\n\t */\n\tget raw(): Response {\n\t\treturn this.response\n\t}\n\n\tprivate startReading(): void {\n\t\tconst body = this.response.body\n\t\tif (!body) {\n\t\t\tthis.streamEnded = true\n\t\t\treturn\n\t\t}\n\n\t\tconst reader = body.getReader() as ReadableStreamDefaultReader<Uint8Array>\n\t\tconst decoder = new TextDecoder()\n\t\tlet buffer = ''\n\n\t\tconst read = async (): Promise<void> => {\n\t\t\ttry {\n\t\t\t\t// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n\t\t\t\twhile (true) {\n\t\t\t\t\tconst { done, value } = await reader.read()\n\n\t\t\t\t\tif (done) {\n\t\t\t\t\t\t// Parse any remaining buffered data\n\t\t\t\t\t\tif (buffer.trim()) {\n\t\t\t\t\t\t\tconst event = this.parseEvent(buffer)\n\t\t\t\t\t\t\tif (event) this.dispatchEvent(event)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthis.streamEnded = true\n\t\t\t\t\t\tfor (const waiter of this.endWaiters) {\n\t\t\t\t\t\t\twaiter()\n\t\t\t\t\t\t}\n\t\t\t\t\t\tthis.endWaiters = []\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\tbuffer += decoder.decode(value, { stream: true })\n\n\t\t\t\t\t// Split on double newlines (SSE event boundary)\n\t\t\t\t\tconst parts = buffer.split('\\n\\n')\n\t\t\t\t\t// Last part may be incomplete, keep it in the buffer\n\t\t\t\t\tbuffer = parts.pop()!\n\n\t\t\t\t\tfor (const part of parts) {\n\t\t\t\t\t\tif (!part.trim()) continue\n\t\t\t\t\t\tconst event = this.parseEvent(part)\n\t\t\t\t\t\tif (event) this.dispatchEvent(event)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\tthis.streamEnded = true\n\t\t\t\tfor (const waiter of this.endWaiters) {\n\t\t\t\t\twaiter()\n\t\t\t\t}\n\t\t\t\tthis.endWaiters = []\n\t\t\t}\n\t\t}\n\n\t\tvoid read()\n\t}\n\n\tprivate parseEvent(raw: string): TestSseEvent | null {\n\t\tconst lines = raw.split('\\n')\n\t\tconst dataLines: string[] = []\n\t\tlet event: string | undefined\n\t\tlet id: string | undefined\n\t\tlet retry: number | undefined\n\n\t\tfor (const line of lines) {\n\t\t\tif (line.startsWith(':')) continue // comment line\n\n\t\t\tconst colonIndex = line.indexOf(':')\n\t\t\tif (colonIndex === -1) continue\n\n\t\t\tconst field = line.slice(0, colonIndex)\n\t\t\t// Strip optional leading space after colon\n\t\t\tconst value = line[colonIndex + 1] === ' ' ? line.slice(colonIndex + 2) : line.slice(colonIndex + 1)\n\n\t\t\tswitch (field) {\n\t\t\t\tcase 'data':\n\t\t\t\t\tdataLines.push(value)\n\t\t\t\t\tbreak\n\t\t\t\tcase 'event':\n\t\t\t\t\tevent = value\n\t\t\t\t\tbreak\n\t\t\t\tcase 'id':\n\t\t\t\t\tid = value\n\t\t\t\t\tbreak\n\t\t\t\tcase 'retry': {\n\t\t\t\t\tconst parsed = parseInt(value, 10)\n\t\t\t\t\tif (!isNaN(parsed)) retry = parsed\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (dataLines.length === 0) return null\n\n\t\tconst result: TestSseEvent = { data: dataLines.join('\\n') }\n\t\tif (event !== undefined) result.event = event\n\t\tif (id !== undefined) result.id = id\n\t\tif (retry !== undefined) result.retry = retry\n\n\t\treturn result\n\t}\n\n\tprivate dispatchEvent(event: TestSseEvent): void {\n\t\tif (this.eventWaiters.length > 0) {\n\t\t\tthis.eventWaiters.shift()!(event)\n\t\t} else {\n\t\t\tthis.eventQueue.push(event)\n\t\t}\n\t}\n}\n","import type { AuthService } from '@stratal/framework/auth'\nimport { AUTH_SERVICE } from '@stratal/framework/auth'\nimport type { DetectionStrategy } from 'stratal/i18n'\nimport { expect } from 'vitest'\nimport { ActingAs } from '../../auth'\nimport type { TestingModule } from '../testing-module'\nimport { applyLocaleToHeaders, resolveLocaleStrategy } from '../http/locale-helper'\nimport { TestSseConnection } from './test-sse-connection'\n\n/**\n * TestSseRequest\n *\n * Builder for SSE connection requests. Follows the TestWsRequest pattern.\n *\n * @example\n * ```typescript\n * const sse = await module.sse('/streaming/sse').connect()\n * await sse.assertEvent({ event: 'message', data: 'hello' })\n * await sse.waitForEnd()\n * ```\n *\n * @example Authenticated SSE\n * ```typescript\n * const sse = await module.sse('/streaming/sse').actingAs({ id: user.id }).connect()\n * ```\n */\nexport class TestSseRequest {\n\tprivate requestHeaders: Headers = new Headers()\n\tprivate actingAsUser: { id: string } | null = null\n\n\tconstructor(\n\t\tprivate readonly path: string,\n\t\tprivate readonly module: TestingModule,\n\t) { }\n\n\t/**\n\t * Add custom headers to the request\n\t */\n\twithHeaders(headers: Record<string, string>): this {\n\t\tfor (const [key, value] of Object.entries(headers)) {\n\t\t\tthis.requestHeaders.set(key, value)\n\t\t}\n\t\treturn this\n\t}\n\n\t/**\n\t * Set the locale for this SSE connection.\n\t * If strategy is not provided, resolves from the module's I18n configuration.\n\t */\n\twithLocale(locale: string, strategy?: DetectionStrategy): this {\n\t\tconst resolved = strategy ?? resolveLocaleStrategy(this.module)\n\t\tapplyLocaleToHeaders(this.requestHeaders, locale, resolved)\n\t\treturn this\n\t}\n\n\t/**\n\t * Authenticate the SSE connection as a specific user\n\t */\n\tactingAs(user: { id: string }): this {\n\t\tthis.actingAsUser = user\n\t\treturn this\n\t}\n\n\t/**\n\t * Send the request and return a live SSE connection\n\t */\n\tasync connect(): Promise<TestSseConnection> {\n\t\tawait this.applyAuthentication()\n\n\t\tthis.requestHeaders.set('Accept', 'text/event-stream')\n\n\t\tconst url = new URL(this.path, 'http://localhost')\n\t\tconst request = new Request(url.toString(), {\n\t\t\theaders: this.requestHeaders,\n\t\t})\n\n\t\tconst response = await this.module.fetch(request)\n\n\t\texpect(\n\t\t\tresponse.status,\n\t\t\t`Expected status 200, got ${response.status}`,\n\t\t).toBe(200)\n\n\t\tconst contentType = response.headers.get('content-type') ?? ''\n\t\texpect(\n\t\t\tcontentType.includes('text/event-stream'),\n\t\t\t`Expected content-type \"text/event-stream\", got \"${contentType}\"`,\n\t\t).toBe(true)\n\n\t\treturn new TestSseConnection(response)\n\t}\n\n\tprivate async applyAuthentication(): Promise<void> {\n\t\tif (!this.actingAsUser) return\n\n\t\tawait this.module.runInRequestScope(async () => {\n\t\t\tconst authService = this.module.get<AuthService>(AUTH_SERVICE)\n\t\t\tconst actingAs = new ActingAs(authService)\n\t\t\tconst authHeaders = this.actingAsUser ? await actingAs.createSessionForUser(this.actingAsUser) : new Headers()\n\n\t\t\tfor (const [key, value] of authHeaders.entries()) {\n\t\t\t\tthis.requestHeaders.set(key, value)\n\t\t\t}\n\t\t})\n\t}\n}\n","import { expect } from \"vitest\";\n\n/**\n * TestWsConnection\n *\n * Live WebSocket connection wrapper with assertion helpers for testing.\n *\n * @example\n * ```typescript\n * const ws = await module.ws('/ws/chat').connect()\n * ws.send('hello')\n * await ws.assertMessage('echo:hello')\n * ws.close()\n * await ws.waitForClose()\n * ```\n */\nexport class TestWsConnection {\n\tprivate readonly messageQueue: (string | ArrayBuffer)[] = []\n\tprivate messageWaiters: ((data: string | ArrayBuffer) => void)[] = []\n\tprivate closeEvent: { code?: number; reason?: string } | null = null\n\tprivate closeWaiters: ((event: { code?: number; reason?: string }) => void)[] = []\n\n\tconstructor(private readonly ws: WebSocket) {\n\t\tthis.ws.addEventListener('message', (event: MessageEvent) => {\n\t\t\tconst data = event.data as string | ArrayBuffer\n\t\t\tif (this.messageWaiters.length > 0) {\n\t\t\t\tthis.messageWaiters.shift()!(data)\n\t\t\t} else {\n\t\t\t\tthis.messageQueue.push(data)\n\t\t\t}\n\t\t})\n\n\t\tthis.ws.addEventListener('close', (event: CloseEvent) => {\n\t\t\tthis.closeEvent = { code: event.code, reason: event.reason }\n\t\t\tfor (const waiter of this.closeWaiters) {\n\t\t\t\twaiter(this.closeEvent)\n\t\t\t}\n\t\t\tthis.closeWaiters = []\n\t\t})\n\t}\n\n\t/**\n\t * Send a message through the WebSocket\n\t */\n\tsend(data: string | ArrayBuffer | Uint8Array): void {\n\t\tthis.ws.send(data)\n\t}\n\n\t/**\n\t * Close the WebSocket connection\n\t */\n\tclose(code?: number, reason?: string): void {\n\t\tthis.ws.close(code, reason)\n\t}\n\n\t/**\n\t * Wait for the next message, returning its data\n\t */\n\tasync waitForMessage(timeout = 5000): Promise<string | ArrayBuffer> {\n\t\tif (this.messageQueue.length > 0) {\n\t\t\treturn this.messageQueue.shift()!\n\t\t}\n\n\t\treturn new Promise<string | ArrayBuffer>((resolve, reject) => {\n\t\t\tconst waiter = (data: string | ArrayBuffer) => {\n\t\t\t\tclearTimeout(timer)\n\t\t\t\tresolve(data)\n\t\t\t}\n\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tconst index = this.messageWaiters.indexOf(waiter)\n\t\t\t\tif (index !== -1) this.messageWaiters.splice(index, 1)\n\t\t\t\treject(new Error(`WebSocket: no message received within ${timeout}ms`))\n\t\t\t}, timeout)\n\n\t\t\tthis.messageWaiters.push(waiter)\n\t\t})\n\t}\n\n\t/**\n\t * Wait for the connection to close\n\t */\n\tasync waitForClose(timeout = 5000): Promise<{ code?: number; reason?: string }> {\n\t\tif (this.closeEvent) {\n\t\t\treturn this.closeEvent\n\t\t}\n\n\t\treturn new Promise<{ code?: number; reason?: string }>((resolve, reject) => {\n\t\t\tconst waiter = (event: { code?: number; reason?: string }) => {\n\t\t\t\tclearTimeout(timer)\n\t\t\t\tresolve(event)\n\t\t\t}\n\n\t\t\tconst timer = setTimeout(() => {\n\t\t\t\tconst index = this.closeWaiters.indexOf(waiter)\n\t\t\t\tif (index !== -1) this.closeWaiters.splice(index, 1)\n\t\t\t\treject(new Error(`WebSocket: connection did not close within ${timeout}ms`))\n\t\t\t}, timeout)\n\n\t\t\tthis.closeWaiters.push(waiter)\n\t\t})\n\t}\n\n\t/**\n\t * Assert that the next message equals the expected value\n\t */\n\tasync assertMessage(expected: string, timeout = 5000): Promise<void> {\n\t\tconst data = await this.waitForMessage(timeout)\n\t\tconst message = typeof data === 'string' ? data : '[ArrayBuffer]'\n\t\texpect(message, `Expected WebSocket message \"${expected}\", got \"${message}\"`).toBe(expected)\n\t}\n\n\t/**\n\t * Assert that the connection closes, optionally with an expected code\n\t */\n\tasync assertClosed(expectedCode?: number, timeout = 5000): Promise<void> {\n\t\tconst event = await this.waitForClose(timeout)\n\t\tif (expectedCode !== undefined) {\n\t\t\texpect(event.code, `Expected close code ${expectedCode}, got ${event.code}`).toBe(expectedCode)\n\t\t}\n\t}\n\n\t/**\n\t * Access the raw Cloudflare WebSocket\n\t */\n\tget raw(): WebSocket {\n\t\treturn this.ws\n\t}\n}\n","import type { AuthService } from '@stratal/framework/auth'\nimport { AUTH_SERVICE } from '@stratal/framework/auth'\nimport type { DetectionStrategy } from 'stratal/i18n'\nimport { expect } from 'vitest'\nimport { ActingAs } from '../../auth'\nimport type { TestingModule } from '../testing-module'\nimport { applyLocaleToHeaders, resolveLocaleStrategy } from '../http/locale-helper'\nimport { TestWsConnection } from './test-ws-connection'\n\n/**\n * TestWsRequest\n *\n * Builder for WebSocket upgrade requests. Follows the TestHttpRequest pattern.\n *\n * @example\n * ```typescript\n * const ws = await module.ws('/ws/chat').connect()\n * ws.send('hello')\n * await ws.assertMessage('echo:hello')\n * ws.close()\n * ```\n *\n * @example Authenticated WebSocket\n * ```typescript\n * const ws = await module.ws('/ws/chat').actingAs({ id: user.id }).connect()\n * ```\n */\nexport class TestWsRequest {\n\tprivate requestHeaders: Headers = new Headers()\n\tprivate actingAsUser: { id: string } | null = null\n\n\tconstructor(\n\t\tprivate readonly path: string,\n\t\tprivate readonly module: TestingModule,\n\t) { }\n\n\t/**\n\t * Add custom headers to the upgrade request\n\t */\n\twithHeaders(headers: Record<string, string>): this {\n\t\tfor (const [key, value] of Object.entries(headers)) {\n\t\t\tthis.requestHeaders.set(key, value)\n\t\t}\n\t\treturn this\n\t}\n\n\t/**\n\t * Set the locale for this WebSocket connection.\n\t * If strategy is not provided, resolves from the module's I18n configuration.\n\t */\n\twithLocale(locale: string, strategy?: DetectionStrategy): this {\n\t\tconst resolved = strategy ?? resolveLocaleStrategy(this.module)\n\t\tapplyLocaleToHeaders(this.requestHeaders, locale, resolved)\n\t\treturn this\n\t}\n\n\t/**\n\t * Authenticate the WebSocket connection as a specific user\n\t */\n\tactingAs(user: { id: string }): this {\n\t\tthis.actingAsUser = user\n\t\treturn this\n\t}\n\n\t/**\n\t * Send the upgrade request and return a live WebSocket connection\n\t */\n\tasync connect(): Promise<TestWsConnection> {\n\t\tawait this.applyAuthentication()\n\n\t\tthis.requestHeaders.set('Upgrade', 'websocket')\n\t\tthis.requestHeaders.set('Connection', 'Upgrade')\n\t\tthis.requestHeaders.set('Sec-WebSocket-Key', 'dGhlIHNhbXBsZSBub25jZQ==')\n\t\tthis.requestHeaders.set('Sec-WebSocket-Version', '13')\n\n\t\tconst url = new URL(this.path, 'http://localhost')\n\t\tconst request = new Request(url.toString(), {\n\t\t\theaders: this.requestHeaders,\n\t\t})\n\n\t\tconst response = await this.module.fetch(request)\n\n\t\texpect(\n\t\t\tresponse.status,\n\t\t\t`Expected status 101 (Switching Protocols), got ${response.status}`,\n\t\t).toBe(101)\n\n\t\tconst ws = (response as Response & { webSocket: WebSocket | null }).webSocket\n\t\tif (!ws) {\n\t\t\tthrow new Error('Response did not include a WebSocket connection')\n\t\t}\n\n\t\tws.accept()\n\n\t\treturn new TestWsConnection(ws)\n\t}\n\n\tprivate async applyAuthentication(): Promise<void> {\n\t\tif (!this.actingAsUser) return\n\n\t\tawait this.module.runInRequestScope(async () => {\n\t\t\tconst authService = this.module.get<AuthService>(AUTH_SERVICE)\n\t\t\tconst actingAs = new ActingAs(authService)\n\t\t\tconst authHeaders = this.actingAsUser ? await actingAs.createSessionForUser(this.actingAsUser) : new Headers()\n\n\t\t\tfor (const [key, value] of authHeaders.entries()) {\n\t\t\t\tthis.requestHeaders.set(key, value)\n\t\t\t}\n\t\t})\n\t}\n}\n","import type { ConnectionName, DatabaseService } from '@stratal/framework/database'\nimport { connectionSymbol } from '@stratal/framework/database'\nimport type { Application, Constructor, StratalEnv, StratalExecutionContext } from 'stratal'\nimport { DI_TOKENS, type Container } from 'stratal/di'\nimport type { ResolvedEmailMessage } from 'stratal/email'\nimport { type InjectionToken } from 'stratal/module'\nimport { SEEDER_TOKENS, SeederError, type Seeder, type SeederRegistry } from 'stratal/seeder'\nimport { STORAGE_TOKENS } from 'stratal/storage'\nimport { expect } from 'vitest'\nimport { dropDatabase } from '../database'\nimport { FEATURE_FLAG_SERVICE_TOKEN, type FakeFeatureFlagService } from '../feature-flags'\nimport type { FakeStorageService } from '../storage'\nimport type { TestEmailProvider } from '../mocks/test-email-provider'\nimport { TestHttpClient } from './http/test-http-client'\nimport { TestCommandRequest } from './quarry/test-command-request'\nimport { TestSseRequest } from './sse/test-sse-request'\nimport { TestWsRequest } from './ws/test-ws-request'\n\n/**\n * TestingModule\n *\n * Provides access to the test application, container, HTTP client, and utilities.\n *\n * @example\n * ```typescript\n * const module = await Test.createTestingModule({\n * modules: [RegistrationModule],\n * }).compile()\n *\n * // Make HTTP requests\n * const response = await module.http\n * .post('/api/v1/register')\n * .withBody({ ... })\n * .send()\n *\n * // Access services\n * const service = module.get(REGISTRATION_TOKENS.RegistrationService)\n *\n * // Database utilities\n * await module.truncateDb()\n * await module.seed(UserSeeder)\n * await module.assertDatabaseHas('user', { email: 'test@example.com' })\n *\n * // Cleanup\n * await module.close()\n * ```\n */\n/**\n * Identifies a per-file database created for test isolation, so it can be\n * dropped on teardown. Produced by the testing module builder.\n */\nexport interface IsolatedDatabase {\n /** Name of the cloned database. */\n name: string\n /** Admin (maintenance-database) connection string used to drop it. */\n adminConnectionString: string\n}\n\nexport class TestingModule {\n private _http: TestHttpClient | null = null\n private readonly _requestContainer: Container\n\n constructor(\n private readonly app: Application,\n private readonly env: StratalEnv,\n private readonly ctx: StratalExecutionContext,\n private readonly isolatedDatabase: IsolatedDatabase | null = null,\n private readonly testEmailProvider: TestEmailProvider | null = null,\n ) {\n const mockContext = this.app.createMockRouterContext()\n this._requestContainer = this.app.container.createRequestScope(mockContext)\n }\n\n /**\n * Emails recorded by the default {@link TestEmailProvider}, in send order.\n * Empty when the email provider factory was overridden.\n */\n get sentEmails(): ResolvedEmailMessage[] {\n return this.testEmailProvider?.sent ?? []\n }\n\n /**\n * Resolve a service from the container\n */\n get<T>(token: InjectionToken<T>): T {\n return this._requestContainer.resolve(token)\n }\n\n /**\n * Get HTTP test client for making requests\n */\n get http(): TestHttpClient {\n this._http ??= new TestHttpClient(this)\n return this._http\n }\n\n\n /**\n * Get Inertia test client for making Inertia requests\n */\n get inertia(): TestHttpClient {\n return this.http.withHeaders({ 'X-Inertia': 'true', 'X-Inertia-Version': '1' })\n }\n\n /**\n * Get fake storage service for assertions\n */\n get storage(): FakeStorageService {\n return this.get<FakeStorageService>(STORAGE_TOKENS.StorageService)\n }\n\n /**\n * Get the fake feature-flag service to configure flags in tests\n * (e.g. `module.featureFlags.set('my-flag', true)`).\n */\n get featureFlags(): FakeFeatureFlagService {\n return this.get<FakeFeatureFlagService>(FEATURE_FLAG_SERVICE_TOKEN)\n }\n\n /**\n * Create a WebSocket test request builder for the given path\n */\n ws(path: string): TestWsRequest {\n return new TestWsRequest(path, this)\n }\n\n /**\n * Create an SSE test request builder for the given path\n */\n sse(path: string): TestSseRequest {\n return new TestSseRequest(path, this)\n }\n\n /**\n * Create a Quarry command test request builder\n */\n quarry(name: string): TestCommandRequest {\n return new TestCommandRequest(name, this)\n }\n\n /**\n * Get Application instance\n */\n get application(): Application {\n return this.app\n }\n\n /**\n * Get DI Container (request-scoped)\n */\n get container(): Container {\n return this._requestContainer\n }\n\n /**\n * Execute an HTTP request through HonoApp\n */\n async fetch(request: Request): Promise<Response> {\n const hono = await this.app.ensureHono()\n return hono.fetch(request, this.env, this.ctx as ExecutionContext)\n }\n\n /**\n * Run callback in request scope (for DB operations, service access)\n */\n async runInRequestScope<T>(callback: (container: Container) => T | Promise<T>): Promise<T> {\n const mockContext = this.app.createMockRouterContext()\n return this.app.container.runInRequestScope(mockContext, callback)\n }\n\n /**\n * Get database service instance (resolved in request scope)\n */\n getDb(): DatabaseService\n getDb<K extends ConnectionName>(name: K): DatabaseService<K>\n getDb(name?: string): unknown {\n const token = name ? connectionSymbol(name) : DI_TOKENS.Database\n return this._requestContainer.resolve(token)\n }\n\n /**\n * Truncate all non-prisma tables in the database\n */\n async truncateDb(name?: ConnectionName): Promise<void> {\n const db = this.getDb(name!)\n const tables = await db.$queryRaw<{ tablename: string }[]>`\n SELECT tablename::text as tablename FROM pg_tables\n WHERE schemaname = current_schema()\n AND tablename NOT LIKE '_prisma%'\n `\n if (tables.length === 0) return\n const tableList = tables.map((t) => `\"${t.tablename}\"`).join(', ')\n await db.$executeRawUnsafe(`TRUNCATE ${tableList} RESTART IDENTITY CASCADE`)\n }\n\n /**\n * Run seeders by class constructor in the request-scoped container\n */\n async seed(...SeederClasses: Constructor<Seeder>[]): Promise<void> {\n const registry = this._requestContainer.resolve<SeederRegistry>(SEEDER_TOKENS.SeederRegistry)\n for (const SeederClass of SeederClasses) {\n if (!registry.has(SeederClass)) {\n throw new SeederError(`Seeder \"${SeederClass.name}\" is not registered`)\n }\n await registry.run(SeederClass, { container: this._requestContainer })\n }\n }\n\n /**\n * Assert that a record exists in the database\n */\n async assertDatabaseHas(table: string, data: Record<string, unknown>, name?: ConnectionName): Promise<void> {\n const db = this.getDb(name!)\n const model = (db as unknown as Record<string, unknown>)[table] as { findFirst: (opts: unknown) => Promise<unknown> }\n const result = await model.findFirst({ where: data })\n expect(result, `Expected ${table} with ${JSON.stringify(data)}`).not.toBeNull()\n }\n\n /**\n * Assert that a record does not exist in the database\n */\n async assertDatabaseMissing(table: string, data: Record<string, unknown>, name?: ConnectionName): Promise<void> {\n const db = this.getDb(name!)\n const model = (db as unknown as Record<string, unknown>)[table] as { findFirst: (opts: unknown) => Promise<unknown> }\n const result = await model.findFirst({ where: data })\n expect(result, `Expected ${table} NOT to have ${JSON.stringify(data)}`).toBeNull()\n }\n\n /**\n * Assert the number of records in a table\n */\n async assertDatabaseCount(table: string, expected: number, name?: ConnectionName): Promise<void> {\n const db = this.getDb(name!)\n const model = (db as unknown as Record<string, unknown>)[table] as { count: () => Promise<number> }\n const actual = await model.count()\n expect(actual, `Expected ${table} count ${expected}, got ${actual}`).toBe(expected)\n }\n\n /**\n * Cleanup - call in afterAll\n */\n async close(): Promise<void> {\n await this._requestContainer.dispose()\n try {\n await this.app.shutdown()\n }\n finally {\n // Drop the per-file database AFTER shutdown so the app's pool connection is\n // released; `WITH (FORCE)` evicts any that lingers. Runs even if shutdown\n // throws, otherwise a failed shutdown would leak the database until the\n // next run's stale-database sweep.\n //\n // A drop failure here is non-fatal: the next run's connection-guarded sweep\n // reclaims the database. Swallow it (warn only) so a passing suite isn't\n // marked failed by a teardown hiccup.\n if (this.isolatedDatabase) {\n try {\n await dropDatabase(this.isolatedDatabase.adminConnectionString, this.isolatedDatabase.name)\n } catch (error) {\n console.warn(\n `[stratal-testing] Failed to drop isolated test database \"${this.isolatedDatabase.name}\"; ` +\n 'it will be reclaimed by the next run\\'s stale-database sweep.',\n error,\n )\n }\n }\n }\n }\n}\n","import {\n Application,\n type ApplicationConfig,\n type Constructor,\n type StratalEnv,\n type StratalExecutionContext,\n} from 'stratal'\nimport type { ExceptionHandler } from 'stratal/errors'\nimport { type Container } from 'stratal/di'\nimport { LogLevel } from 'stratal/logger'\nimport { type InjectionToken, Module, type ModuleClass, type ModuleOptions } from 'stratal/module'\nimport { EMAIL_TOKENS } from 'stratal/email'\nimport { RATE_LIMITER_TOKENS } from 'stratal/rate-limiter'\nimport { STORAGE_TOKENS } from 'stratal/storage'\nimport {\n BINDING_ENV_VAR,\n buildConnectionString,\n createDatabaseFromTemplate,\n DEFAULT_DB_BINDING,\n deriveAdminConnectionString,\n deriveDbName,\n deriveTemplateName,\n dropDatabase,\n ISOLATION_ENV_VAR,\n normalizeIsolation,\n} from '../database'\nimport { FakeStorageService } from '../storage'\nimport { NoopRateLimiterStore } from '../mocks/noop-rate-limiter-store'\nimport { TestEmailProvider } from '../mocks/test-email-provider'\nimport { FEATURE_FLAG_SERVICE_TOKEN, FakeFeatureFlagService } from '../feature-flags'\nimport { ProviderOverrideBuilder, type ProviderOverrideConfig } from './override'\nimport { Test } from './test'\nimport { TestingModule, type IsolatedDatabase } from './testing-module'\n\n/**\n * Configuration for creating a testing module\n *\n * Extends ModuleOptions to support all module properties like NestJS.\n *\n * @example\n * ```typescript\n * const module = await Test.createTestingModule({\n * imports: [RegistrationModule, GeoModule],\n * providers: [{ provide: MOCK_TOKEN, useValue: mockValue }],\n * controllers: [TestController],\n * }).compile()\n * ```\n */\nexport interface TestingModuleConfig extends ModuleOptions {\n /** Optional environment overrides */\n env?: Partial<StratalEnv>\n /** Logging configuration. Defaults: level=ERROR, formatter='json' */\n logging?: ApplicationConfig['logging']\n /**\n * Custom exception handler. Mirrors `ApplicationConfig.exceptionHandler`.\n * Must be passed at compile time — `overrideProvider(DI_TOKENS.ExceptionHandler)`\n * cannot replace it because the framework resolves the handler during\n * `initialize()` (before overrides apply).\n */\n exceptionHandler?: Constructor<ExceptionHandler>\n}\n\n/**\n * Builder for creating test modules with provider overrides\n */\nexport class TestingModuleBuilder {\n private overrides: ProviderOverrideConfig<object>[] = []\n\n constructor(private config: TestingModuleConfig) { }\n\n /**\n * Override a provider with a custom implementation\n */\n overrideProvider<T>(token: InjectionToken<T>): ProviderOverrideBuilder<T> {\n return new ProviderOverrideBuilder(this, token)\n }\n\n /**\n * Add a provider override (internal use by ProviderOverrideBuilder)\n *\n * @internal\n */\n addProviderOverride<T>(override: ProviderOverrideConfig<T>): this {\n this.overrides.push(override as ProviderOverrideConfig<object>)\n return this\n }\n\n /**\n * Merge additional environment bindings\n */\n withEnv(env: Partial<StratalEnv>): this {\n this.config.env = { ...this.config.env, ...env }\n return this\n }\n\n private async getCloudflareWorkers() {\n try {\n return await import('cloudflare:workers')\n } catch {\n return null\n }\n }\n\n /**\n * Compile the testing module\n *\n * Creates the Application, applies overrides, initializes, and returns TestingModule.\n */\n async compile(): Promise<TestingModule> {\n const cf = await this.getCloudflareWorkers()\n\n const env = { ...cf?.env, ...this.config.env } as StratalEnv\n const ctx: StratalExecutionContext = {\n waitUntil: cf ? cf.waitUntil : (p) => {\n p.catch(() => {\n //\n })\n },\n }\n\n // When database isolation is enabled, clone the migrated template into a\n // fresh per-file database and point this app's DB binding at it. The clone\n // is dropped in TestingModule.close(). No-op in 'shared' mode or when the\n // app has no DB binding.\n const isolatedDb = await this.setupIsolatedDatabase(env)\n\n // Everything between the clone above and returning the TestingModule (which\n // owns the drop via close()) can throw — module init, overrides, etc. If it\n // does, nothing would ever drop the freshly cloned database. Drop it (FORCE)\n // on failure before rethrowing so a compile() error doesn't leak a database.\n let app: Application | null = null\n try {\n // Build root module from config\n const baseModules = Test.getBaseModules()\n const allImports = [...baseModules, ...(this.config.imports ?? [])]\n\n const rootModule = this.createTestRootModule({\n imports: allImports,\n providers: this.config.providers,\n controllers: this.config.controllers,\n consumers: this.config.consumers,\n jobs: this.config.jobs,\n })\n\n app = new Application({\n module: rootModule,\n logging: {\n level: this.config.logging?.level ?? LogLevel.ERROR,\n formatter: this.config.logging?.formatter ?? 'pretty',\n },\n env,\n ctx,\n exceptionHandler: this.config.exceptionHandler,\n })\n\n await app.initialize()\n\n // Auto-register FakeStorageService after initialize so it replaces module-registered StorageService\n app.container.registerSingleton(STORAGE_TOKENS.StorageService, FakeStorageService)\n\n // Auto-register FakeFeatureFlagService so feature-gated code resolves without a\n // real Cloudflare Flagship binding. Inert for apps that don't use feature flags.\n app.container.registerSingleton(FEATURE_FLAG_SERVICE_TOKEN, FakeFeatureFlagService)\n\n // Disable rate limiting: suites fire many requests from one \"IP\" in\n // seconds and would trip production limiter budgets (and Better Auth's\n // built-in per-path limits, which share this store). Override the token\n // back to a real store to test limiting behavior explicitly.\n app.container.registerSingleton(RATE_LIMITER_TOKENS.Store, NoopRateLimiterStore)\n\n // Auto-register TestEmailProvider: the sync queue provider runs\n // EmailConsumer inline on dispatch, which would otherwise open a real\n // SMTP connection from the test worker. Recorded messages are exposed\n // via `module.sentEmails`; the user overrides below still replace this.\n const testEmailProvider = new TestEmailProvider()\n app.container.registerValue(EMAIL_TOKENS.EmailProviderFactory, {\n create: () => testEmailProvider,\n })\n\n // Apply user overrides AFTER initialize so they replace module-registered providers\n for (const override of this.overrides) {\n switch (override.type) {\n case 'value':\n app.container.registerValue(override.token, override.implementation)\n break\n case 'class':\n app.container.registerSingleton(\n override.token,\n override.implementation as Constructor\n )\n break\n case 'factory':\n app.container.registerFactory(\n override.token,\n override.implementation as (c: Container) => object\n )\n break\n case 'existing':\n app.container.registerExisting(\n override.token,\n override.implementation as InjectionToken<object>\n )\n break\n }\n }\n\n // Routing init is lazy in production (first fetch keeps cold starts\n // lean), but tests may resolve request-scoped router services before\n // any fetch — e.g. ActingAs minting a session resolves AUTH_OPTIONS,\n // whose factory can inject ROUTER_TOKENS.Uri. Initialize eagerly so\n // test ordering can never matter. Runs last: singleton resolution\n // caches by token without invalidation, so anything resolved here\n // would permanently shadow auto-mocks and user overrides.\n await app.ensureHono()\n\n return new TestingModule(app, env, ctx, isolatedDb, testEmailProvider)\n } catch (error) {\n // Tear down the partially built Application so module-held resources\n // (DB pools, timers) don't outlive a failed compile().\n if (app) {\n await app.shutdown().catch(() => {\n // Best-effort cleanup for partially initialized apps.\n })\n }\n if (isolatedDb) {\n await dropDatabase(isolatedDb.adminConnectionString, isolatedDb.name).catch(() => {\n // Best-effort cleanup; the next run's stale-database sweep is the backstop.\n })\n }\n throw error\n }\n }\n\n /**\n * When `STRATAL_TEST_DB_ISOLATION=database`, create a fresh database cloned\n * from the migrated template and rewrite the Hyperdrive binding's\n * `connectionString` to target it. The binding name is configurable via\n * `stratalTest({ database: { binding } })` (defaults to `DB`). Returns the\n * created name + admin connection so `close()` can drop it, or `null` when\n * isolation is off. Throws if isolation is on but the binding is missing — a\n * misconfiguration, not a case to skip.\n */\n private async setupIsolatedDatabase(env: StratalEnv): Promise<IsolatedDatabase | null> {\n const bindings = env as unknown as Record<string, unknown>\n const isolation = normalizeIsolation(bindings[ISOLATION_ENV_VAR] as string | undefined)\n if (isolation !== 'database') return null\n\n const bindingName = (bindings[BINDING_ENV_VAR] as string | undefined) ?? DEFAULT_DB_BINDING\n const db = bindings[bindingName] as { connectionString?: string } | undefined\n const base = db?.connectionString\n if (!base) {\n throw new Error(\n `Database isolation is 'database' but no \\`${bindingName}\\` Hyperdrive binding with a connectionString was found. ` +\n `Provide it via \\`stratalTest({ miniflare: { hyperdrives: { ${bindingName} } } })\\`, or set isolation to 'shared'.`,\n )\n }\n\n const adminConnectionString = deriveAdminConnectionString(base)\n const name = deriveDbName(base)\n await createDatabaseFromTemplate(adminConnectionString, name, deriveTemplateName(base))\n\n // Point the binding at the per-file database by overriding only its\n // connectionString. Clone with the original prototype + descriptors so the\n // Hyperdrive binding's methods (e.g. `connect()`) and other fields survive,\n // and never mutate the shared cloudflare env object.\n const isolated = Object.create(\n Object.getPrototypeOf(db) as object | null,\n Object.getOwnPropertyDescriptors(db),\n ) as Record<string, unknown>\n Object.defineProperty(isolated, 'connectionString', {\n value: buildConnectionString(base, name),\n enumerable: true,\n configurable: true,\n writable: true,\n })\n bindings[bindingName] = isolated\n\n return { name, adminConnectionString }\n }\n\n /**\n * Create a test root module with the given options\n */\n private createTestRootModule(options: ModuleOptions): ModuleClass {\n @Module(options)\n class TestRootModule { }\n return TestRootModule\n }\n}\n","import { type DynamicModule, type ModuleClass } from 'stratal/module'\nimport { TestingModuleBuilder, type TestingModuleConfig } from './testing-module-builder'\n\n/**\n * Test\n *\n * Static class for creating testing modules.\n * Provides a NestJS-style API for configuring test modules.\n *\n * @example\n * ```typescript\n * // In vitest.setup.ts:\n * Test.setBaseModules([CoreModule])\n *\n * // In test files:\n * const module = await Test.createTestingModule({\n * imports: [RegistrationModule, GeoModule],\n * })\n * .overrideProvider(EMAIL_TOKENS.EmailService)\n * .useValue(mockEmailService)\n * .compile()\n * ```\n */\nexport class Test {\n /**\n * Base modules to include in all test modules\n * Set once in vitest.setup.ts\n */\n private static baseModules: (ModuleClass | DynamicModule)[] = []\n\n /**\n * Set base modules to include in all test modules\n * Should be called once in vitest.setup.ts\n *\n * @param modules - Modules to include before test-specific modules (e.g., CoreModule)\n */\n static setBaseModules(modules: (ModuleClass | DynamicModule)[]): void {\n this.baseModules = modules\n }\n\n /**\n * Get base modules\n */\n static getBaseModules(): (ModuleClass | DynamicModule)[] {\n return this.baseModules\n }\n\n /**\n * Create a testing module builder\n *\n * @param config - Configuration with modules and optional env overrides\n * @returns TestingModuleBuilder for configuring and compiling the module\n */\n static createTestingModule(config: TestingModuleConfig): TestingModuleBuilder {\n return new TestingModuleBuilder(config)\n }\n}\n","import { http, HttpResponse, type RequestHandler } from 'msw'\nimport { setupServer, type SetupServer } from 'msw/node'\nimport type { MockErrorOptions, MockJsonOptions } from './fetch-mock.types'\n\ntype HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'delete' | 'head' | 'options'\n\n/**\n * MSW-based fetch mock for declarative HTTP mocking in tests.\n *\n * Replaces the old Cloudflare `fetchMock` (undici MockAgent) with MSW's `setupServer`.\n * Works in both Node.js and workerd test environments.\n *\n * @example\n * ```typescript\n * import { createMockFetch } from '@stratal/testing'\n *\n * const mock = createMockFetch()\n *\n * beforeAll(() => mock.listen())\n * afterEach(() => mock.reset())\n * afterAll(() => mock.close())\n *\n * it('should mock external API', async () => {\n * mock.mockJsonResponse('https://api.example.com/data', { success: true })\n *\n * const response = await fetch('https://api.example.com/data')\n * const json = await response.json()\n *\n * expect(json.success).toBe(true)\n * })\n * ```\n */\nexport class MockFetch {\n private server: SetupServer\n\n constructor(handlers: RequestHandler[] = []) {\n this.server = setupServer(...handlers)\n }\n\n /** Start intercepting. Call in beforeAll/beforeEach. */\n listen() {\n this.server.listen({ onUnhandledRequest: 'error' })\n }\n\n /** Reset runtime handlers. Call in afterEach. */\n reset() {\n this.server.resetHandlers()\n }\n\n /** Stop intercepting. Call in afterAll. */\n close() {\n this.server.close()\n }\n\n /** Add runtime handler(s) for a single test. */\n use(...handlers: RequestHandler[]) {\n this.server.use(...handlers)\n }\n\n /**\n * Mock a JSON response.\n *\n * @param url - Full URL to mock (e.g., 'https://api.example.com/users')\n * @param data - JSON data to return\n * @param options - HTTP method, status code, headers\n *\n * @example\n * ```typescript\n * mock.mockJsonResponse('https://api.example.com/users', { users: [] })\n * mock.mockJsonResponse('https://api.example.com/users', { created: true }, { method: 'POST', status: 201 })\n * ```\n */\n mockJsonResponse(url: string, data: Record<string, unknown> | unknown[], options: MockJsonOptions = {}) {\n const method = (options.method ?? 'GET').toLowerCase() as HttpMethod\n const handler = http[method](url, () =>\n HttpResponse.json(data, {\n status: options.status ?? 200,\n headers: options.headers,\n }),\n )\n this.server.use(handler)\n }\n\n /**\n * Mock an error response.\n *\n * @param url - Full URL to mock\n * @param status - HTTP error status code\n * @param message - Optional error message\n * @param options - HTTP method, headers\n *\n * @example\n * ```typescript\n * mock.mockError('https://api.example.com/fail', 401, 'Unauthorized')\n * mock.mockError('https://api.example.com/fail', 500, 'Server Error', { method: 'POST' })\n * ```\n */\n mockError(url: string, status: number, message?: string, options: MockErrorOptions = {}) {\n const method = (options.method ?? 'GET').toLowerCase() as HttpMethod\n const body = message ? { error: message } : undefined\n this.server.use(\n http[method](url, () =>\n HttpResponse.json(body, { status, headers: options.headers }),\n ),\n )\n }\n}\n\n/**\n * Factory function to create a new MockFetch instance\n *\n * @param handlers - Optional initial MSW request handlers\n * @returns A new MockFetch instance\n *\n * @example\n * ```typescript\n * import { createMockFetch } from '@stratal/testing'\n *\n * const mock = createMockFetch()\n *\n * beforeAll(() => mock.listen())\n * afterEach(() => mock.reset())\n * afterAll(() => mock.close())\n * ```\n */\nexport function createMockFetch(handlers?: RequestHandler[]): MockFetch {\n return new MockFetch(handlers)\n}\n","/**\n * Base error class for all test framework errors.\n * Extends from Error and allows easy identification via `instanceof`.\n */\nexport class TestError extends Error {\n constructor(\n message: string,\n public readonly cause?: Error\n ) {\n super(message)\n this.name = 'TestError'\n\n // Maintain proper stack trace\n Error.captureStackTrace(this, this.constructor)\n }\n}\n","import { TestError } from './test-error'\n\n/**\n * Error thrown when test setup fails.\n * Examples: schema creation failure, migration failure, application bootstrap failure.\n */\nexport class TestSetupError extends TestError {\n constructor(message: string, cause?: Error) {\n super(`Test setup failed: ${message}`, cause)\n }\n}\n"],"mappings":"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"}
|
package/dist/mocks/index.d.mts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
+
import { t as TestEmailProvider } from "../test-email-provider-B7cjj97-.mjs";
|
|
1
2
|
import { DeepMocked, PartialFuncReturn, createMock } from "@golevelup/ts-vitest";
|
|
2
|
-
export { type DeepMocked, type PartialFuncReturn, createMock };
|
|
3
|
+
export { type DeepMocked, type PartialFuncReturn, TestEmailProvider, createMock };
|
package/dist/mocks/index.mjs
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { IRateLimiterStore } from "stratal/rate-limiter";
|
|
2
|
+
|
|
3
|
+
//#region src/mocks/noop-rate-limiter-store.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Rate-limit store that never persists, so every read misses and every
|
|
6
|
+
* counter restarts at 1 — rate limits are effectively disabled.
|
|
7
|
+
*
|
|
8
|
+
* Auto-registered by the testing module builder: integration suites fire
|
|
9
|
+
* many requests from one "IP" in seconds, which would trip production
|
|
10
|
+
* limiter budgets (and Better Auth's built-in per-path limits, which share
|
|
11
|
+
* this store via the framework bridge). Suites that want to test limiting
|
|
12
|
+
* behavior can override `RATE_LIMITER_TOKENS.Store` back to a real store.
|
|
13
|
+
*/
|
|
14
|
+
declare class NoopRateLimiterStore implements IRateLimiterStore {
|
|
15
|
+
get<T = unknown>(_key: string): Promise<T | null>;
|
|
16
|
+
set<T = unknown>(_key: string, _value: T, _ttlSeconds: number): Promise<void>;
|
|
17
|
+
delete(_key: string): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
export { NoopRateLimiterStore };
|
|
21
|
+
//# sourceMappingURL=noop-rate-limiter-store.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"noop-rate-limiter-store.d.mts","names":[],"sources":["../../src/mocks/noop-rate-limiter-store.ts"],"mappings":";;;;;AAYA;;;;;;;;cAAa,oBAAA,YAAgC,iBAAA;EAC3C,GAAA,cAAiB,IAAA,WAAe,OAAA,CAAQ,CAAA;EAIxC,GAAA,cAAiB,IAAA,UAAc,MAAA,EAAQ,CAAA,EAAG,WAAA,WAAsB,OAAA;EAKhE,MAAA,CAAO,IAAA,WAAe,OAAA;AAAA"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//#region src/mocks/noop-rate-limiter-store.ts
|
|
2
|
+
/**
|
|
3
|
+
* Rate-limit store that never persists, so every read misses and every
|
|
4
|
+
* counter restarts at 1 — rate limits are effectively disabled.
|
|
5
|
+
*
|
|
6
|
+
* Auto-registered by the testing module builder: integration suites fire
|
|
7
|
+
* many requests from one "IP" in seconds, which would trip production
|
|
8
|
+
* limiter budgets (and Better Auth's built-in per-path limits, which share
|
|
9
|
+
* this store via the framework bridge). Suites that want to test limiting
|
|
10
|
+
* behavior can override `RATE_LIMITER_TOKENS.Store` back to a real store.
|
|
11
|
+
*/
|
|
12
|
+
var NoopRateLimiterStore = class {
|
|
13
|
+
get(_key) {
|
|
14
|
+
return Promise.resolve(null);
|
|
15
|
+
}
|
|
16
|
+
set(_key, _value, _ttlSeconds) {
|
|
17
|
+
return Promise.resolve();
|
|
18
|
+
}
|
|
19
|
+
delete(_key) {
|
|
20
|
+
return Promise.resolve();
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
//#endregion
|
|
24
|
+
export { NoopRateLimiterStore };
|
|
25
|
+
|
|
26
|
+
//# sourceMappingURL=noop-rate-limiter-store.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"noop-rate-limiter-store.mjs","names":[],"sources":["../../src/mocks/noop-rate-limiter-store.ts"],"sourcesContent":["import type { IRateLimiterStore } from 'stratal/rate-limiter'\n\n/**\n * Rate-limit store that never persists, so every read misses and every\n * counter restarts at 1 — rate limits are effectively disabled.\n *\n * Auto-registered by the testing module builder: integration suites fire\n * many requests from one \"IP\" in seconds, which would trip production\n * limiter budgets (and Better Auth's built-in per-path limits, which share\n * this store via the framework bridge). Suites that want to test limiting\n * behavior can override `RATE_LIMITER_TOKENS.Store` back to a real store.\n */\nexport class NoopRateLimiterStore implements IRateLimiterStore {\n get<T = unknown>(_key: string): Promise<T | null> {\n return Promise.resolve(null)\n }\n\n set<T = unknown>(_key: string, _value: T, _ttlSeconds: number): Promise<void> {\n // intentionally dropped\n return Promise.resolve()\n }\n\n delete(_key: string): Promise<void> {\n // nothing persisted, nothing to delete\n return Promise.resolve()\n }\n}\n"],"mappings":";;;;;;;;;;;AAYA,IAAa,uBAAb,MAA+D;CAC7D,IAAiB,MAAiC;EAChD,OAAO,QAAQ,QAAQ,IAAI;CAC7B;CAEA,IAAiB,MAAc,QAAW,aAAoC;EAE5E,OAAO,QAAQ,QAAQ;CACzB;CAEA,OAAO,MAA6B;EAElC,OAAO,QAAQ,QAAQ;CACzB;AACF"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { EmailBatchSendResult, EmailSendResult, IEmailProvider, ResolvedEmailMessage } from "stratal/email";
|
|
2
|
+
|
|
3
|
+
//#region src/mocks/test-email-provider.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* In-memory email provider for tests.
|
|
6
|
+
*
|
|
7
|
+
* The sync queue provider runs `EmailConsumer` inline on dispatch, which would
|
|
8
|
+
* otherwise open a real SMTP connection from the test worker. The testing
|
|
9
|
+
* module builder installs this provider by default (overridable via
|
|
10
|
+
* `overrideProvider(EMAIL_TOKENS.EmailProviderFactory)`), recording every
|
|
11
|
+
* message so tests can assert on what was sent.
|
|
12
|
+
*/
|
|
13
|
+
declare class TestEmailProvider implements IEmailProvider {
|
|
14
|
+
/** Every message handed to the provider, in send order. */
|
|
15
|
+
readonly sent: ResolvedEmailMessage[];
|
|
16
|
+
send(message: ResolvedEmailMessage): Promise<EmailSendResult>;
|
|
17
|
+
sendBatch(messages: ResolvedEmailMessage[]): Promise<EmailBatchSendResult>;
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
export { TestEmailProvider as t };
|
|
21
|
+
//# sourceMappingURL=test-email-provider-B7cjj97-.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-email-provider-B7cjj97-.d.mts","names":[],"sources":["../src/mocks/test-email-provider.ts"],"mappings":";;;;;AAgBA;;;;;;;cAAa,iBAAA,YAA6B,cAAA;EASkB;EAAA,SAPlD,IAAA,EAAM,oBAAA;EAEf,IAAA,CAAK,OAAA,EAAS,oBAAA,GAAuB,OAAA,CAAQ,eAAA;EAKvC,SAAA,CAAU,QAAA,EAAU,oBAAA,KAAyB,OAAA,CAAQ,oBAAA;AAAA"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
//#region src/mocks/test-email-provider.ts
|
|
2
|
+
/**
|
|
3
|
+
* In-memory email provider for tests.
|
|
4
|
+
*
|
|
5
|
+
* The sync queue provider runs `EmailConsumer` inline on dispatch, which would
|
|
6
|
+
* otherwise open a real SMTP connection from the test worker. The testing
|
|
7
|
+
* module builder installs this provider by default (overridable via
|
|
8
|
+
* `overrideProvider(EMAIL_TOKENS.EmailProviderFactory)`), recording every
|
|
9
|
+
* message so tests can assert on what was sent.
|
|
10
|
+
*/
|
|
11
|
+
var TestEmailProvider = class {
|
|
12
|
+
/** Every message handed to the provider, in send order. */
|
|
13
|
+
sent = [];
|
|
14
|
+
send(message) {
|
|
15
|
+
this.sent.push(message);
|
|
16
|
+
return Promise.resolve({
|
|
17
|
+
messageId: `test-${this.sent.length}`,
|
|
18
|
+
accepted: true
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
async sendBatch(messages) {
|
|
22
|
+
const results = await Promise.all(messages.map((message) => this.send(message)));
|
|
23
|
+
return {
|
|
24
|
+
total: results.length,
|
|
25
|
+
successful: results.length,
|
|
26
|
+
failed: 0,
|
|
27
|
+
results
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
//#endregion
|
|
32
|
+
export { TestEmailProvider as t };
|
|
33
|
+
|
|
34
|
+
//# sourceMappingURL=test-email-provider-Dr-nhE0x.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-email-provider-Dr-nhE0x.mjs","names":[],"sources":["../src/mocks/test-email-provider.ts"],"sourcesContent":["import type {\n\tEmailBatchSendResult,\n\tEmailSendResult,\n\tIEmailProvider,\n\tResolvedEmailMessage,\n} from 'stratal/email'\n\n/**\n * In-memory email provider for tests.\n *\n * The sync queue provider runs `EmailConsumer` inline on dispatch, which would\n * otherwise open a real SMTP connection from the test worker. The testing\n * module builder installs this provider by default (overridable via\n * `overrideProvider(EMAIL_TOKENS.EmailProviderFactory)`), recording every\n * message so tests can assert on what was sent.\n */\nexport class TestEmailProvider implements IEmailProvider {\n\t/** Every message handed to the provider, in send order. */\n\treadonly sent: ResolvedEmailMessage[] = []\n\n\tsend(message: ResolvedEmailMessage): Promise<EmailSendResult> {\n\t\tthis.sent.push(message)\n\t\treturn Promise.resolve({ messageId: `test-${this.sent.length}`, accepted: true })\n\t}\n\n\tasync sendBatch(messages: ResolvedEmailMessage[]): Promise<EmailBatchSendResult> {\n\t\tconst results = await Promise.all(messages.map((message) => this.send(message)))\n\t\treturn {\n\t\t\ttotal: results.length,\n\t\t\tsuccessful: results.length,\n\t\t\tfailed: 0,\n\t\t\tresults,\n\t\t}\n\t}\n}\n"],"mappings":";;;;;;;;;;AAgBA,IAAa,oBAAb,MAAyD;;CAExD,OAAwC,CAAC;CAEzC,KAAK,SAAyD;EAC7D,KAAK,KAAK,KAAK,OAAO;EACtB,OAAO,QAAQ,QAAQ;GAAE,WAAW,QAAQ,KAAK,KAAK;GAAU,UAAU;EAAK,CAAC;CACjF;CAEA,MAAM,UAAU,UAAiE;EAChF,MAAM,UAAU,MAAM,QAAQ,IAAI,SAAS,KAAK,YAAY,KAAK,KAAK,OAAO,CAAC,CAAC;EAC/E,OAAO;GACN,OAAO,QAAQ;GACf,YAAY,QAAQ;GACpB,QAAQ;GACR;EACD;CACD;AACD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stratal/testing",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.27",
|
|
4
4
|
"description": "Testing utilities and mocks for Stratal framework applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -49,6 +49,10 @@
|
|
|
49
49
|
"types": "./dist/mocks/index.d.mts",
|
|
50
50
|
"import": "./dist/mocks/index.mjs"
|
|
51
51
|
},
|
|
52
|
+
"./mocks/noop-rate-limiter-store": {
|
|
53
|
+
"types": "./dist/mocks/noop-rate-limiter-store.d.mts",
|
|
54
|
+
"import": "./dist/mocks/noop-rate-limiter-store.mjs"
|
|
55
|
+
},
|
|
52
56
|
"./mocks/zenstack-language": {
|
|
53
57
|
"types": "./dist/mocks/zenstack-language.d.mts",
|
|
54
58
|
"import": "./dist/mocks/zenstack-language.mjs"
|
|
@@ -76,10 +80,10 @@
|
|
|
76
80
|
},
|
|
77
81
|
"peerDependencies": {
|
|
78
82
|
"@better-auth/core": ">=1.6",
|
|
79
|
-
"@stratal/framework": ">=0.0.
|
|
83
|
+
"@stratal/framework": ">=0.0.27",
|
|
80
84
|
"better-auth": ">=1.4",
|
|
81
85
|
"pg": "^8.0.0",
|
|
82
|
-
"stratal": ">=0.0.
|
|
86
|
+
"stratal": ">=0.0.27",
|
|
83
87
|
"vitest": "^4.1.0"
|
|
84
88
|
},
|
|
85
89
|
"peerDependenciesMeta": {
|