@stratal/testing 0.0.27 → 0.1.1
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/CHANGELOG.md +570 -0
- package/README.md +137 -36
- package/dist/database/index.d.mts +2 -2
- package/dist/database/index.mjs +2 -2
- package/dist/database-Rojs69r3.mjs +453 -0
- package/dist/database-Rojs69r3.mjs.map +1 -0
- package/dist/{decorate-B7nr7eBl.mjs → decorate-RQD1h28J.mjs} +1 -1
- package/dist/feature-flags/index.mjs +1 -1
- package/dist/{feature-flags-BiLhfSGh.mjs → feature-flags-CZ1a4M2g.mjs} +2 -2
- package/dist/{feature-flags-BiLhfSGh.mjs.map → feature-flags-CZ1a4M2g.mjs.map} +1 -1
- package/dist/index-B8Mw1Dxk.d.mts +175 -0
- package/dist/index-B8Mw1Dxk.d.mts.map +1 -0
- package/dist/{index-CrHzUDKX.d.mts → index-D7FM6EKp.d.mts} +21 -3
- package/dist/index-D7FM6EKp.d.mts.map +1 -0
- package/dist/index-qgWNJRdC.d.mts.map +1 -1
- package/dist/index.d.mts +105 -46
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +218 -108
- package/dist/index.mjs.map +1 -1
- package/dist/mocks/cloudflare-workers.d.mts +24 -0
- package/dist/mocks/cloudflare-workers.d.mts.map +1 -0
- package/dist/mocks/cloudflare-workers.mjs +28 -0
- package/dist/mocks/cloudflare-workers.mjs.map +1 -0
- package/dist/mocks/index.d.mts +2 -2
- package/dist/mocks/index.mjs +2 -2
- package/dist/mocks/noop-rate-limiter-store.d.mts +1 -3
- package/dist/mocks/noop-rate-limiter-store.d.mts.map +1 -1
- package/dist/mocks/zenstack-language.d.mts +26 -29
- package/dist/mocks/zenstack-language.d.mts.map +1 -1
- package/dist/mocks/zenstack-language.mjs +1 -1
- package/dist/mocks/zenstack-language.mjs.map +1 -1
- package/dist/storage/index.d.mts +1 -1
- package/dist/storage/index.mjs +1 -1
- package/dist/{storage-DhoxWqyF.mjs → storage-BiGamMA8.mjs} +73 -6
- package/dist/storage-BiGamMA8.mjs.map +1 -0
- package/dist/test-worker-exports-dCRARYEc.d.mts +144 -0
- package/dist/test-worker-exports-dCRARYEc.d.mts.map +1 -0
- package/dist/test-workers-cache-D02Pt_dE.mjs +183 -0
- package/dist/test-workers-cache-D02Pt_dE.mjs.map +1 -0
- package/dist/vitest-plugin/index.d.mts +14 -28
- package/dist/vitest-plugin/index.d.mts.map +1 -1
- package/dist/vitest-plugin/index.mjs +32 -20
- package/dist/vitest-plugin/index.mjs.map +1 -1
- package/package.json +30 -21
- package/dist/database-B02eYKhE.mjs +0 -334
- package/dist/database-B02eYKhE.mjs.map +0 -1
- package/dist/index-BIr5nLof.d.mts +0 -122
- package/dist/index-BIr5nLof.d.mts.map +0 -1
- package/dist/index-CrHzUDKX.d.mts.map +0 -1
- package/dist/storage-DhoxWqyF.mjs.map +0 -1
- package/dist/test-email-provider-B7cjj97-.d.mts +0 -21
- package/dist/test-email-provider-B7cjj97-.d.mts.map +0 -1
- package/dist/test-email-provider-Dr-nhE0x.mjs +0 -34
- package/dist/test-email-provider-Dr-nhE0x.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as FakeStorageService } from "./storage-
|
|
3
|
-
import { t as __decorate } from "./decorate-
|
|
1
|
+
import { i as BINDING_ENV_VAR, n as buildTruncateSql, p as leaseWorkerDatabase, t as buildTableDiscoverySql } from "./database-Rojs69r3.mjs";
|
|
2
|
+
import { t as FakeStorageService } from "./storage-BiGamMA8.mjs";
|
|
3
|
+
import { t as __decorate } from "./decorate-RQD1h28J.mjs";
|
|
4
4
|
import { NoopRateLimiterStore } from "./mocks/noop-rate-limiter-store.mjs";
|
|
5
|
-
import {
|
|
6
|
-
import { n as FakeFeatureFlagService, t as FEATURE_FLAG_SERVICE_TOKEN } from "./feature-flags-
|
|
5
|
+
import { n as TestWorkerExports, r as TestEmailProvider, t as TestWorkersCache } from "./test-workers-cache-D02Pt_dE.mjs";
|
|
6
|
+
import { n as FakeFeatureFlagService, t as FEATURE_FLAG_SERVICE_TOKEN } from "./feature-flags-CZ1a4M2g.mjs";
|
|
7
7
|
import { Application } from "stratal";
|
|
8
8
|
import { LogLevel } from "stratal/logger";
|
|
9
|
+
import { markGatewayMode } from "stratal/response-cache";
|
|
9
10
|
import { Module } from "stratal/module";
|
|
10
11
|
import { EMAIL_TOKENS } from "stratal/email";
|
|
11
12
|
import { RATE_LIMITER_TOKENS } from "stratal/rate-limiter";
|
|
@@ -117,6 +118,32 @@ var ProviderOverrideBuilder = class {
|
|
|
117
118
|
}
|
|
118
119
|
};
|
|
119
120
|
//#endregion
|
|
121
|
+
//#region src/errors/test-error.ts
|
|
122
|
+
/**
|
|
123
|
+
* Base error class for all test framework errors.
|
|
124
|
+
* Extends from Error and allows easy identification via `instanceof`.
|
|
125
|
+
*/
|
|
126
|
+
var TestError = class extends Error {
|
|
127
|
+
cause;
|
|
128
|
+
constructor(message, cause) {
|
|
129
|
+
super(message);
|
|
130
|
+
this.cause = cause;
|
|
131
|
+
this.name = "TestError";
|
|
132
|
+
Error.captureStackTrace(this, this.constructor);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/errors/setup-error.ts
|
|
137
|
+
/**
|
|
138
|
+
* Error thrown when test setup fails.
|
|
139
|
+
* Examples: schema creation failure, migration failure, application bootstrap failure.
|
|
140
|
+
*/
|
|
141
|
+
var TestSetupError = class extends TestError {
|
|
142
|
+
constructor(message, cause) {
|
|
143
|
+
super(`Test setup failed: ${message}`, cause);
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
//#endregion
|
|
120
147
|
//#region src/core/http/locale-helper.ts
|
|
121
148
|
/**
|
|
122
149
|
* Resolve the configured detection strategy from the testing module's DI container.
|
|
@@ -139,9 +166,7 @@ function applyLocaleToHeaders(headers, locale, strategy) {
|
|
|
139
166
|
case "cookie":
|
|
140
167
|
headers.set("Cookie", `locale=${locale}`);
|
|
141
168
|
break;
|
|
142
|
-
case "header":
|
|
143
|
-
headers.set("Accept-Language", locale);
|
|
144
|
-
break;
|
|
169
|
+
case "header": headers.set("Accept-Language", locale);
|
|
145
170
|
}
|
|
146
171
|
}
|
|
147
172
|
/**
|
|
@@ -400,7 +425,8 @@ var TestResponse = class extends Macroable {
|
|
|
400
425
|
* @param path - Dot-notation path (e.g., 'data.user.id')
|
|
401
426
|
*/
|
|
402
427
|
async assertJsonPathExists(path) {
|
|
403
|
-
|
|
428
|
+
const exists = hasValueAtPath(await this.json(), path);
|
|
429
|
+
expect(exists, `Expected JSON path "${path}" to exist`).toBe(true);
|
|
404
430
|
return this;
|
|
405
431
|
}
|
|
406
432
|
/**
|
|
@@ -409,7 +435,8 @@ var TestResponse = class extends Macroable {
|
|
|
409
435
|
* @param path - Dot-notation path (e.g., 'data.user.deletedAt')
|
|
410
436
|
*/
|
|
411
437
|
async assertJsonPathMissing(path) {
|
|
412
|
-
|
|
438
|
+
const exists = hasValueAtPath(await this.json(), path);
|
|
439
|
+
expect(exists, `Expected JSON path "${path}" to not exist`).toBe(false);
|
|
413
440
|
return this;
|
|
414
441
|
}
|
|
415
442
|
/**
|
|
@@ -750,19 +777,23 @@ var TestCommandResult = class {
|
|
|
750
777
|
return this;
|
|
751
778
|
}
|
|
752
779
|
assertOutputContains(text) {
|
|
753
|
-
|
|
780
|
+
const joined = this.result.output.join("\n");
|
|
781
|
+
expect(joined, `Expected output to contain "${text}"`).toContain(text);
|
|
754
782
|
return this;
|
|
755
783
|
}
|
|
756
784
|
assertOutputMissing(text) {
|
|
757
|
-
|
|
785
|
+
const joined = this.result.output.join("\n");
|
|
786
|
+
expect(joined, `Expected output NOT to contain "${text}"`).not.toContain(text);
|
|
758
787
|
return this;
|
|
759
788
|
}
|
|
760
789
|
assertErrorContains(text) {
|
|
761
|
-
|
|
790
|
+
const joined = this.result.errors.join("\n");
|
|
791
|
+
expect(joined, `Expected errors to contain "${text}"`).toContain(text);
|
|
762
792
|
return this;
|
|
763
793
|
}
|
|
764
794
|
assertErrorMissing(text) {
|
|
765
|
-
|
|
795
|
+
const joined = this.result.errors.join("\n");
|
|
796
|
+
expect(joined, `Expected errors NOT to contain "${text}"`).not.toContain(text);
|
|
766
797
|
return this;
|
|
767
798
|
}
|
|
768
799
|
};
|
|
@@ -896,7 +927,8 @@ var TestSseConnection = class {
|
|
|
896
927
|
* Assert that the next event matches the expected partial shape
|
|
897
928
|
*/
|
|
898
929
|
async assertEvent(expected, timeout = 5e3) {
|
|
899
|
-
|
|
930
|
+
const event = await this.waitForEvent(timeout);
|
|
931
|
+
expect(event).toMatchObject(expected);
|
|
900
932
|
}
|
|
901
933
|
/**
|
|
902
934
|
* Assert that the next event's data matches the expected string
|
|
@@ -910,7 +942,8 @@ var TestSseConnection = class {
|
|
|
910
942
|
*/
|
|
911
943
|
async assertJsonEventData(expected, timeout = 5e3) {
|
|
912
944
|
const event = await this.waitForEvent(timeout);
|
|
913
|
-
|
|
945
|
+
const parsed = JSON.parse(event.data);
|
|
946
|
+
expect(parsed).toEqual(expected);
|
|
914
947
|
}
|
|
915
948
|
/**
|
|
916
949
|
* Access the raw Response
|
|
@@ -1262,20 +1295,53 @@ var TestWsRequest = class {
|
|
|
1262
1295
|
};
|
|
1263
1296
|
//#endregion
|
|
1264
1297
|
//#region src/core/testing-module.ts
|
|
1298
|
+
/**
|
|
1299
|
+
* TestingModule
|
|
1300
|
+
*
|
|
1301
|
+
* Provides access to the test application, container, HTTP client, and utilities.
|
|
1302
|
+
*
|
|
1303
|
+
* @example
|
|
1304
|
+
* ```typescript
|
|
1305
|
+
* const module = await Test.createTestingModule({
|
|
1306
|
+
* modules: [RegistrationModule],
|
|
1307
|
+
* }).compile()
|
|
1308
|
+
*
|
|
1309
|
+
* // Make HTTP requests
|
|
1310
|
+
* const response = await module.http
|
|
1311
|
+
* .post('/api/v1/register')
|
|
1312
|
+
* .withBody({ ... })
|
|
1313
|
+
* .send()
|
|
1314
|
+
*
|
|
1315
|
+
* // Access services
|
|
1316
|
+
* const service = module.get(REGISTRATION_TOKENS.RegistrationService)
|
|
1317
|
+
*
|
|
1318
|
+
* // Database utilities
|
|
1319
|
+
* await module.truncateDb()
|
|
1320
|
+
* await module.seed(UserSeeder)
|
|
1321
|
+
* await module.assertDatabaseHas('user', { email: 'test@example.com' })
|
|
1322
|
+
*
|
|
1323
|
+
* // Cleanup
|
|
1324
|
+
* await module.close()
|
|
1325
|
+
* ```
|
|
1326
|
+
*/
|
|
1265
1327
|
var TestingModule = class {
|
|
1266
1328
|
app;
|
|
1267
1329
|
env;
|
|
1268
1330
|
ctx;
|
|
1269
|
-
isolatedDatabase;
|
|
1270
1331
|
testEmailProvider;
|
|
1332
|
+
pendingTasks;
|
|
1333
|
+
testWorkersCache;
|
|
1334
|
+
testWorkerExports;
|
|
1271
1335
|
_http = null;
|
|
1272
1336
|
_requestContainer;
|
|
1273
|
-
constructor(app, env, ctx,
|
|
1337
|
+
constructor(app, env, ctx, testEmailProvider = null, pendingTasks = [], testWorkersCache = void 0, testWorkerExports = void 0) {
|
|
1274
1338
|
this.app = app;
|
|
1275
1339
|
this.env = env;
|
|
1276
1340
|
this.ctx = ctx;
|
|
1277
|
-
this.isolatedDatabase = isolatedDatabase;
|
|
1278
1341
|
this.testEmailProvider = testEmailProvider;
|
|
1342
|
+
this.pendingTasks = pendingTasks;
|
|
1343
|
+
this.testWorkersCache = testWorkersCache;
|
|
1344
|
+
this.testWorkerExports = testWorkerExports;
|
|
1279
1345
|
const mockContext = this.app.createMockRouterContext();
|
|
1280
1346
|
this._requestContainer = this.app.container.createRequestScope(mockContext);
|
|
1281
1347
|
}
|
|
@@ -1301,12 +1367,18 @@ var TestingModule = class {
|
|
|
1301
1367
|
}
|
|
1302
1368
|
/**
|
|
1303
1369
|
* Get Inertia test client for making Inertia requests
|
|
1370
|
+
*
|
|
1371
|
+
* Sends no `X-Inertia-Version`. It used to send `'1'`, which meant any app configuring a real
|
|
1372
|
+
* asset version had every request from this client read as a stale client and answered with a
|
|
1373
|
+
* version-mismatch 409 — an entire Inertia suite failing on a value the tests never chose. The
|
|
1374
|
+
* check needs a version from BOTH sides, so omitting it here leaves the app under test in charge
|
|
1375
|
+
* rather than this header.
|
|
1376
|
+
*
|
|
1377
|
+
* A test that wants the mismatch path asks for it:
|
|
1378
|
+
* `module.inertia.withHeaders({ 'X-Inertia-Version': 'stale' })`.
|
|
1304
1379
|
*/
|
|
1305
1380
|
get inertia() {
|
|
1306
|
-
return this.http.withHeaders({
|
|
1307
|
-
"X-Inertia": "true",
|
|
1308
|
-
"X-Inertia-Version": "1"
|
|
1309
|
-
});
|
|
1381
|
+
return this.http.withHeaders({ "X-Inertia": "true" });
|
|
1310
1382
|
}
|
|
1311
1383
|
/**
|
|
1312
1384
|
* Get fake storage service for assertions
|
|
@@ -1322,6 +1394,36 @@ var TestingModule = class {
|
|
|
1322
1394
|
return this.get(FEATURE_FLAG_SERVICE_TOKEN);
|
|
1323
1395
|
}
|
|
1324
1396
|
/**
|
|
1397
|
+
* The default `ctx.cache` stub — present unless this module was compiled
|
|
1398
|
+
* with `cache: false`. Assert `@PurgesCache` routes purged what they should
|
|
1399
|
+
* via `module.cache.purges`, in call order.
|
|
1400
|
+
*
|
|
1401
|
+
* @throws {TestSetupError} This module was compiled with `cache: false`,
|
|
1402
|
+
* so there is no stub to inspect.
|
|
1403
|
+
*/
|
|
1404
|
+
get cache() {
|
|
1405
|
+
if (!this.testWorkersCache) throw new TestSetupError("module.cache is unavailable because this module was compiled with `cache: false`. Remove that option to use the default ctx.cache stub.");
|
|
1406
|
+
return this.testWorkersCache;
|
|
1407
|
+
}
|
|
1408
|
+
/**
|
|
1409
|
+
* The default `ctx.exports` stub — present unless this module was compiled
|
|
1410
|
+
* with `cache: false`.
|
|
1411
|
+
*
|
|
1412
|
+
* Assert which requests the response-cache gateway forwarded to the cached
|
|
1413
|
+
* entrypoint, and with which resolved partitions, via
|
|
1414
|
+
* `module.gateway.loopbacks` — in call order, mirroring
|
|
1415
|
+
* `module.cache.purges`. An empty `loopbacks` is the assertion for every
|
|
1416
|
+
* fail-closed case: an unresolved partition, a non-GET, or a `@Cacheable`
|
|
1417
|
+
* route with no `partitionBy` all run inline and never appear here.
|
|
1418
|
+
*
|
|
1419
|
+
* @throws {TestSetupError} This module was compiled with `cache: false`,
|
|
1420
|
+
* so there is no stub to inspect.
|
|
1421
|
+
*/
|
|
1422
|
+
get gateway() {
|
|
1423
|
+
if (!this.testWorkerExports) throw new TestSetupError("module.gateway is unavailable because this module was compiled with `cache: false`. Remove that option to use the default ctx.exports stub.");
|
|
1424
|
+
return this.testWorkerExports;
|
|
1425
|
+
}
|
|
1426
|
+
/**
|
|
1325
1427
|
* Create a WebSocket test request builder for the given path
|
|
1326
1428
|
*/
|
|
1327
1429
|
ws(path) {
|
|
@@ -1355,7 +1457,20 @@ var TestingModule = class {
|
|
|
1355
1457
|
* Execute an HTTP request through HonoApp
|
|
1356
1458
|
*/
|
|
1357
1459
|
async fetch(request) {
|
|
1358
|
-
|
|
1460
|
+
const response = await (await this.app.ensureHono()).fetch(request, this.env, this.ctx);
|
|
1461
|
+
await this.flushDeferredWork();
|
|
1462
|
+
return response;
|
|
1463
|
+
}
|
|
1464
|
+
/**
|
|
1465
|
+
* Drain background work the most recent request(s) deferred via `ctx.waitUntil`, including any
|
|
1466
|
+
* follow-up tasks those tasks enqueue. Settles each (errors are surfaced by the work itself, not
|
|
1467
|
+
* here) so deferred side-effects don't outlive the request that triggered them.
|
|
1468
|
+
*/
|
|
1469
|
+
async flushDeferredWork() {
|
|
1470
|
+
while (this.pendingTasks.length > 0) {
|
|
1471
|
+
const batch = this.pendingTasks.splice(0);
|
|
1472
|
+
await Promise.allSettled(batch);
|
|
1473
|
+
}
|
|
1359
1474
|
}
|
|
1360
1475
|
/**
|
|
1361
1476
|
* Run callback in request scope (for DB operations, service access)
|
|
@@ -1369,18 +1484,17 @@ var TestingModule = class {
|
|
|
1369
1484
|
return this._requestContainer.resolve(token);
|
|
1370
1485
|
}
|
|
1371
1486
|
/**
|
|
1372
|
-
* Truncate
|
|
1487
|
+
* Truncate mutable tables in the connection's schema(s), honoring a preserve-list.
|
|
1488
|
+
* Migration bookkeeping (`_prisma%`) is always preserved.
|
|
1373
1489
|
*/
|
|
1374
|
-
async truncateDb(name) {
|
|
1490
|
+
async truncateDb(name, opts = {}) {
|
|
1375
1491
|
const db = this.getDb(name);
|
|
1376
|
-
const
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
if (
|
|
1382
|
-
const tableList = tables.map((t) => `"${t.tablename}"`).join(", ");
|
|
1383
|
-
await db.$executeRawUnsafe(`TRUNCATE ${tableList} RESTART IDENTITY CASCADE`);
|
|
1492
|
+
const rows = await db.$queryRawUnsafe(buildTableDiscoverySql(opts.schemas ?? [], opts.preserve ?? []));
|
|
1493
|
+
const sql = buildTruncateSql(rows.map((t) => ({
|
|
1494
|
+
schema: t.schemaname,
|
|
1495
|
+
table: t.tablename
|
|
1496
|
+
})));
|
|
1497
|
+
if (sql) await db.$executeRawUnsafe(sql);
|
|
1384
1498
|
}
|
|
1385
1499
|
/**
|
|
1386
1500
|
* Run seeders by class constructor in the request-scoped container
|
|
@@ -1396,13 +1510,15 @@ var TestingModule = class {
|
|
|
1396
1510
|
* Assert that a record exists in the database
|
|
1397
1511
|
*/
|
|
1398
1512
|
async assertDatabaseHas(table, data, name) {
|
|
1399
|
-
|
|
1513
|
+
const result = await this.getDb(name)[table].findFirst({ where: data });
|
|
1514
|
+
expect(result, `Expected ${table} with ${JSON.stringify(data)}`).not.toBeNull();
|
|
1400
1515
|
}
|
|
1401
1516
|
/**
|
|
1402
1517
|
* Assert that a record does not exist in the database
|
|
1403
1518
|
*/
|
|
1404
1519
|
async assertDatabaseMissing(table, data, name) {
|
|
1405
|
-
|
|
1520
|
+
const result = await this.getDb(name)[table].findFirst({ where: data });
|
|
1521
|
+
expect(result, `Expected ${table} NOT to have ${JSON.stringify(data)}`).toBeNull();
|
|
1406
1522
|
}
|
|
1407
1523
|
/**
|
|
1408
1524
|
* Assert the number of records in a table
|
|
@@ -1415,21 +1531,21 @@ var TestingModule = class {
|
|
|
1415
1531
|
* Cleanup - call in afterAll
|
|
1416
1532
|
*/
|
|
1417
1533
|
async close() {
|
|
1534
|
+
await this.flushDeferredWork();
|
|
1418
1535
|
await this._requestContainer.dispose();
|
|
1419
|
-
|
|
1420
|
-
await this.app.shutdown();
|
|
1421
|
-
} finally {
|
|
1422
|
-
if (this.isolatedDatabase) try {
|
|
1423
|
-
await dropDatabase(this.isolatedDatabase.adminConnectionString, this.isolatedDatabase.name);
|
|
1424
|
-
} catch (error) {
|
|
1425
|
-
console.warn(`[stratal-testing] Failed to drop isolated test database "${this.isolatedDatabase.name}"; it will be reclaimed by the next run's stale-database sweep.`, error);
|
|
1426
|
-
}
|
|
1427
|
-
}
|
|
1536
|
+
await this.app.shutdown();
|
|
1428
1537
|
}
|
|
1429
1538
|
};
|
|
1430
1539
|
//#endregion
|
|
1431
1540
|
//#region src/core/testing-module-builder.ts
|
|
1432
1541
|
/**
|
|
1542
|
+
* This isolate's worker-database lease, per base connection string. The pool
|
|
1543
|
+
* runs each test file in its own isolate (`isolate: true`), so the first
|
|
1544
|
+
* `compile()` in a file leases and clones a database and later compiles in the
|
|
1545
|
+
* same file reuse it. The lease ends when the pool disposes the isolate.
|
|
1546
|
+
*/
|
|
1547
|
+
const workerDatabases = /* @__PURE__ */ new Map();
|
|
1548
|
+
/**
|
|
1433
1549
|
* Builder for creating test modules with provider overrides
|
|
1434
1550
|
*/
|
|
1435
1551
|
var TestingModuleBuilder = class {
|
|
@@ -1481,28 +1597,40 @@ var TestingModuleBuilder = class {
|
|
|
1481
1597
|
...cf?.env,
|
|
1482
1598
|
...this.config.env
|
|
1483
1599
|
};
|
|
1484
|
-
const
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
const
|
|
1600
|
+
const pendingTasks = [];
|
|
1601
|
+
const testWorkersCache = this.config.cache === false ? void 0 : new TestWorkersCache();
|
|
1602
|
+
const testWorkerExports = this.config.cache === false ? void 0 : new TestWorkerExports(testWorkersCache);
|
|
1603
|
+
const ctx = {
|
|
1604
|
+
waitUntil: (promise) => {
|
|
1605
|
+
pendingTasks.push(Promise.resolve(promise).catch(() => void 0));
|
|
1606
|
+
cf?.waitUntil?.(promise);
|
|
1607
|
+
},
|
|
1608
|
+
cache: testWorkersCache,
|
|
1609
|
+
exports: testWorkerExports?.context
|
|
1610
|
+
};
|
|
1611
|
+
markGatewayMode(ctx);
|
|
1612
|
+
await this.attachWorkerDatabase(env);
|
|
1488
1613
|
let app = null;
|
|
1489
1614
|
try {
|
|
1490
1615
|
const allImports = [...Test.getBaseModules(), ...this.config.imports ?? []];
|
|
1616
|
+
const rootModule = this.createTestRootModule({
|
|
1617
|
+
imports: allImports,
|
|
1618
|
+
providers: this.config.providers,
|
|
1619
|
+
controllers: this.config.controllers,
|
|
1620
|
+
consumers: this.config.consumers,
|
|
1621
|
+
jobs: this.config.jobs
|
|
1622
|
+
});
|
|
1491
1623
|
app = new Application({
|
|
1492
|
-
module:
|
|
1493
|
-
imports: allImports,
|
|
1494
|
-
providers: this.config.providers,
|
|
1495
|
-
controllers: this.config.controllers,
|
|
1496
|
-
consumers: this.config.consumers,
|
|
1497
|
-
jobs: this.config.jobs
|
|
1498
|
-
}),
|
|
1624
|
+
module: rootModule,
|
|
1499
1625
|
logging: {
|
|
1500
1626
|
level: this.config.logging?.level ?? LogLevel.ERROR,
|
|
1501
1627
|
formatter: this.config.logging?.formatter ?? "pretty"
|
|
1502
1628
|
},
|
|
1503
1629
|
env,
|
|
1504
1630
|
ctx,
|
|
1505
|
-
exceptionHandler: this.config.exceptionHandler
|
|
1631
|
+
exceptionHandler: this.config.exceptionHandler,
|
|
1632
|
+
trailingSlash: this.config.trailingSlash,
|
|
1633
|
+
versioning: this.config.versioning
|
|
1506
1634
|
});
|
|
1507
1635
|
await app.initialize();
|
|
1508
1636
|
app.container.registerSingleton(STORAGE_TOKENS.StorageService, FakeStorageService);
|
|
@@ -1520,49 +1648,56 @@ var TestingModuleBuilder = class {
|
|
|
1520
1648
|
case "factory":
|
|
1521
1649
|
app.container.registerFactory(override.token, override.implementation);
|
|
1522
1650
|
break;
|
|
1523
|
-
case "existing":
|
|
1524
|
-
app.container.registerExisting(override.token, override.implementation);
|
|
1525
|
-
break;
|
|
1651
|
+
case "existing": app.container.registerExisting(override.token, override.implementation);
|
|
1526
1652
|
}
|
|
1527
1653
|
await app.ensureHono();
|
|
1528
|
-
|
|
1654
|
+
testWorkerExports?.setHandler(async (request, props) => {
|
|
1655
|
+
const hono = await app.ensureHono();
|
|
1656
|
+
const cachedCtx = {
|
|
1657
|
+
waitUntil: ctx.waitUntil.bind(ctx),
|
|
1658
|
+
cache: testWorkersCache,
|
|
1659
|
+
exports: ctx.exports,
|
|
1660
|
+
props
|
|
1661
|
+
};
|
|
1662
|
+
return hono.fetch(request, env, cachedCtx);
|
|
1663
|
+
});
|
|
1664
|
+
return new TestingModule(app, env, ctx, testEmailProvider, pendingTasks, testWorkersCache, testWorkerExports);
|
|
1529
1665
|
} catch (error) {
|
|
1530
1666
|
if (app) await app.shutdown().catch(() => {});
|
|
1531
|
-
if (isolatedDb) await dropDatabase(isolatedDb.adminConnectionString, isolatedDb.name).catch(() => {});
|
|
1532
1667
|
throw error;
|
|
1533
1668
|
}
|
|
1534
1669
|
}
|
|
1535
1670
|
/**
|
|
1536
|
-
*
|
|
1537
|
-
*
|
|
1538
|
-
*
|
|
1539
|
-
* `stratalTest({ database
|
|
1540
|
-
*
|
|
1541
|
-
* isolation is off. Throws if isolation is on but the binding is missing — a
|
|
1542
|
-
* misconfiguration, not a case to skip.
|
|
1671
|
+
* Point this app's Hyperdrive binding at the database this file leased,
|
|
1672
|
+
* leasing it on the file's first compile. Returns nothing to clean up — the
|
|
1673
|
+
* lease ends with the isolate. No-op when the consumer didn't opt into
|
|
1674
|
+
* `stratalTest({ database })`; throws if they did but no connection string is
|
|
1675
|
+
* configured (a misconfiguration that would otherwise silently skip isolation).
|
|
1543
1676
|
*/
|
|
1544
|
-
async
|
|
1677
|
+
async attachWorkerDatabase(env) {
|
|
1545
1678
|
const bindings = env;
|
|
1546
|
-
|
|
1547
|
-
|
|
1679
|
+
const bindingName = bindings[BINDING_ENV_VAR];
|
|
1680
|
+
if (!bindingName) return;
|
|
1548
1681
|
const db = bindings[bindingName];
|
|
1549
1682
|
const base = db?.connectionString;
|
|
1550
|
-
if (!base) throw new Error(`
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1683
|
+
if (!base) throw new Error(`[stratal-testing] stratalTest({ database }) is enabled but no \`${bindingName}\` Hyperdrive binding with a connectionString was found. Provide it via miniflare.hyperdrives (e.g. { ${bindingName}: DATABASE_URL }), or drop the \`database\` option.`);
|
|
1684
|
+
let lease = workerDatabases.get(base);
|
|
1685
|
+
if (!lease) {
|
|
1686
|
+
lease = leaseWorkerDatabase(base).catch((error) => {
|
|
1687
|
+
workerDatabases.delete(base);
|
|
1688
|
+
throw error;
|
|
1689
|
+
});
|
|
1690
|
+
workerDatabases.set(base, lease);
|
|
1691
|
+
}
|
|
1692
|
+
const { connectionString } = await lease;
|
|
1554
1693
|
const isolated = Object.create(Object.getPrototypeOf(db), Object.getOwnPropertyDescriptors(db));
|
|
1555
1694
|
Object.defineProperty(isolated, "connectionString", {
|
|
1556
|
-
value:
|
|
1695
|
+
value: connectionString,
|
|
1557
1696
|
enumerable: true,
|
|
1558
1697
|
configurable: true,
|
|
1559
1698
|
writable: true
|
|
1560
1699
|
});
|
|
1561
1700
|
bindings[bindingName] = isolated;
|
|
1562
|
-
return {
|
|
1563
|
-
name,
|
|
1564
|
-
adminConnectionString
|
|
1565
|
-
};
|
|
1566
1701
|
}
|
|
1567
1702
|
/**
|
|
1568
1703
|
* Create a test root module with the given options
|
|
@@ -1689,7 +1824,8 @@ var MockFetch = class {
|
|
|
1689
1824
|
* ```
|
|
1690
1825
|
*/
|
|
1691
1826
|
mockJsonResponse(url, data, options = {}) {
|
|
1692
|
-
const
|
|
1827
|
+
const method = (options.method ?? "GET").toLowerCase();
|
|
1828
|
+
const handler = http$1[method](url, () => HttpResponse$1.json(data, {
|
|
1693
1829
|
status: options.status ?? 200,
|
|
1694
1830
|
headers: options.headers
|
|
1695
1831
|
}));
|
|
@@ -1739,32 +1875,6 @@ function createMockFetch(handlers) {
|
|
|
1739
1875
|
return new MockFetch(handlers);
|
|
1740
1876
|
}
|
|
1741
1877
|
//#endregion
|
|
1742
|
-
//#region src/errors/test-error.ts
|
|
1743
|
-
/**
|
|
1744
|
-
* Base error class for all test framework errors.
|
|
1745
|
-
* Extends from Error and allows easy identification via `instanceof`.
|
|
1746
|
-
*/
|
|
1747
|
-
var TestError = class extends Error {
|
|
1748
|
-
cause;
|
|
1749
|
-
constructor(message, cause) {
|
|
1750
|
-
super(message);
|
|
1751
|
-
this.cause = cause;
|
|
1752
|
-
this.name = "TestError";
|
|
1753
|
-
Error.captureStackTrace(this, this.constructor);
|
|
1754
|
-
}
|
|
1755
|
-
};
|
|
1756
|
-
//#endregion
|
|
1757
|
-
//#region src/errors/setup-error.ts
|
|
1758
|
-
/**
|
|
1759
|
-
* Error thrown when test setup fails.
|
|
1760
|
-
* Examples: schema creation failure, migration failure, application bootstrap failure.
|
|
1761
|
-
*/
|
|
1762
|
-
var TestSetupError = class extends TestError {
|
|
1763
|
-
constructor(message, cause) {
|
|
1764
|
-
super(`Test setup failed: ${message}`, cause);
|
|
1765
|
-
}
|
|
1766
|
-
};
|
|
1767
|
-
//#endregion
|
|
1768
1878
|
export { ActingAs, HttpResponse, MockFetch, ProviderOverrideBuilder, Test, TestCommandRequest, TestCommandResult, TestError, TestHttpClient, TestHttpRequest, TestResponse, TestSetupError, TestSseConnection, TestSseRequest, TestWsConnection, TestWsRequest, TestingModule, TestingModuleBuilder, createMockFetch, getValueAtPath, hasValueAtPath, http };
|
|
1769
1879
|
|
|
1770
1880
|
//# sourceMappingURL=index.mjs.map
|