@taicode/common-server 3.8.0 → 4.0.0-beta.10
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/output/app/index.d.ts +18 -15
- package/output/app/index.d.ts.map +1 -1
- package/output/app/index.js +330 -9
- package/output/auth/index.d.ts +73 -0
- package/output/auth/index.d.ts.map +1 -0
- package/output/auth/index.js +196 -0
- package/output/core/assemble.d.ts +64 -0
- package/output/core/assemble.d.ts.map +1 -0
- package/output/core/assemble.js +88 -0
- package/output/core/boot/context.d.ts +109 -0
- package/output/core/boot/context.d.ts.map +1 -0
- package/output/core/boot/context.js +145 -0
- package/output/core/boot/lifecycle.d.ts +206 -0
- package/output/core/boot/lifecycle.d.ts.map +1 -0
- package/output/core/boot/lifecycle.js +255 -0
- package/output/core/boot/shutdown.d.ts +34 -0
- package/output/core/boot/shutdown.d.ts.map +1 -0
- package/output/core/boot/shutdown.js +50 -0
- package/output/core/boot/signal.d.ts +13 -0
- package/output/core/boot/signal.d.ts.map +1 -0
- package/output/{app → core/boot}/signal.js +5 -4
- package/output/core/boot/steps.d.ts +138 -0
- package/output/core/boot/steps.d.ts.map +1 -0
- package/output/core/boot/steps.js +259 -0
- package/output/core/entries.d.ts +151 -0
- package/output/core/entries.d.ts.map +1 -0
- package/output/core/entries.js +242 -0
- package/output/core/entries.test.d.ts +2 -0
- package/output/core/entries.test.d.ts.map +1 -0
- package/output/core/entries.test.js +458 -0
- package/output/core/env.d.ts +83 -0
- package/output/core/env.d.ts.map +1 -0
- package/output/core/env.js +124 -0
- package/output/{app → core}/env.test.d.ts.map +1 -1
- package/output/core/env.test.js +105 -0
- package/output/core/modules/auth-provider/index.d.ts +10 -0
- package/output/core/modules/auth-provider/index.d.ts.map +1 -0
- package/output/core/modules/auth-provider/index.js +9 -0
- package/output/core/modules/auth-provider/types.d.ts +71 -0
- package/output/core/modules/auth-provider/types.d.ts.map +1 -0
- package/output/core/modules/context/index.d.ts +18 -0
- package/output/core/modules/context/index.d.ts.map +1 -0
- package/output/core/modules/context/index.js +23 -0
- package/output/core/modules/context/index.test.d.ts.map +1 -0
- package/output/core/modules/cookie/index.d.ts +21 -0
- package/output/core/modules/cookie/index.d.ts.map +1 -0
- package/output/core/modules/cookie/index.js +17 -0
- package/output/core/modules/identity/index.d.ts +4 -0
- package/output/core/modules/identity/index.d.ts.map +1 -0
- package/output/core/modules/identity/index.js +14 -0
- package/output/core/modules/logging/index.d.ts +5 -0
- package/output/core/modules/logging/index.d.ts.map +1 -0
- package/output/core/modules/logging/index.js +29 -0
- package/output/core/modules/openapi/index.d.ts +7 -0
- package/output/core/modules/openapi/index.d.ts.map +1 -0
- package/output/core/modules/openapi/index.js +41 -0
- package/output/core/modules/permission-operations/index.d.ts +26 -0
- package/output/core/modules/permission-operations/index.d.ts.map +1 -0
- package/output/core/modules/permission-operations/index.js +36 -0
- package/output/core/modules/permission-operations/index.test.d.ts +2 -0
- package/output/core/modules/permission-operations/index.test.d.ts.map +1 -0
- package/output/core/modules/permission-operations/index.test.js +45 -0
- package/output/core/modules/request-id/index.d.ts +33 -0
- package/output/core/modules/request-id/index.d.ts.map +1 -0
- package/output/core/modules/request-id/index.js +66 -0
- package/output/core/modules/response/index.d.ts +50 -0
- package/output/core/modules/response/index.d.ts.map +1 -0
- package/output/core/modules/response/index.js +102 -0
- package/output/{schema/index.d.ts → core/modules/schema/builders.d.ts} +8 -1
- package/output/core/modules/schema/builders.d.ts.map +1 -0
- package/output/{schema/index.js → core/modules/schema/builders.js} +7 -4
- package/output/core/modules/schema/index.d.ts +34 -0
- package/output/core/modules/schema/index.d.ts.map +1 -0
- package/output/core/modules/schema/index.js +62 -0
- package/output/core/modules/schema/index.test.d.ts +2 -0
- package/output/core/modules/schema/index.test.d.ts.map +1 -0
- package/output/core/modules/schema/index.test.js +55 -0
- package/output/core/modules/system-routes/index.d.ts +15 -0
- package/output/core/modules/system-routes/index.d.ts.map +1 -0
- package/output/core/modules/system-routes/index.js +205 -0
- package/output/core/resource.d.ts +56 -0
- package/output/core/resource.d.ts.map +1 -0
- package/output/core/resource.js +56 -0
- package/output/core/test-support/database.d.ts +89 -0
- package/output/core/test-support/database.d.ts.map +1 -0
- package/output/core/test-support/database.js +131 -0
- package/output/core/test-support/database.test.d.ts.map +1 -0
- package/output/core/test-support/database.test.js +111 -0
- package/output/core/test-support/environment.d.ts +38 -0
- package/output/core/test-support/environment.d.ts.map +1 -0
- package/output/core/test-support/environment.js +45 -0
- package/output/core/test-support/service-context.d.ts +64 -0
- package/output/core/test-support/service-context.d.ts.map +1 -0
- package/output/core/test-support/service-context.js +63 -0
- package/output/core/test-support/service-context.test.d.ts +2 -0
- package/output/core/test-support/service-context.test.d.ts.map +1 -0
- package/output/core/test-support/service-context.test.js +55 -0
- package/output/core/test-support/test-server.d.ts +154 -0
- package/output/core/test-support/test-server.d.ts.map +1 -0
- package/output/core/test-support/test-server.js +114 -0
- package/output/core/test-support/test-server.test.d.ts +2 -0
- package/output/core/test-support/test-server.test.d.ts.map +1 -0
- package/output/core/test-support/test-server.test.js +296 -0
- package/output/core/test-support/testdata.d.ts +255 -0
- package/output/core/test-support/testdata.d.ts.map +1 -0
- package/output/core/test-support/testdata.js +271 -0
- package/output/core/test-support/testdata.test.d.ts +2 -0
- package/output/core/test-support/testdata.test.d.ts.map +1 -0
- package/output/core/test-support/testdata.test.js +334 -0
- package/output/core/type-augment.d.ts +34 -0
- package/output/core/type-augment.d.ts.map +1 -0
- package/output/core/type-augment.js +7 -0
- package/output/core/types.d.ts +582 -0
- package/output/core/types.d.ts.map +1 -0
- package/output/core/types.js +2 -0
- package/output/cors/index.d.ts +3 -0
- package/output/cors/index.d.ts.map +1 -0
- package/output/cors/index.js +28 -0
- package/output/index.d.ts +10 -4
- package/output/index.d.ts.map +1 -1
- package/output/index.js +25 -5
- package/output/rate-limit/index.d.ts +23 -0
- package/output/rate-limit/index.d.ts.map +1 -0
- package/output/rate-limit/index.js +43 -0
- package/output/{rate-limiter/rate-limiter.d.ts → rate-limit/limiter.d.ts} +1 -1
- package/output/rate-limit/limiter.d.ts.map +1 -0
- package/output/{rate-limiter → rate-limit}/memory-store.d.ts +6 -0
- package/output/rate-limit/memory-store.d.ts.map +1 -0
- package/output/{rate-limiter → rate-limit}/memory-store.js +8 -0
- package/output/{rate-limiter → rate-limit}/redis-store.d.ts +18 -0
- package/output/rate-limit/redis-store.d.ts.map +1 -0
- package/output/{rate-limiter → rate-limit}/redis-store.js +35 -0
- package/output/rate-limit/redis-store.test.d.ts.map +1 -0
- package/output/{rate-limiter → rate-limit}/redis-store.test.js +2 -2
- package/output/rate-limit/types.d.ts +45 -0
- package/output/rate-limit/types.d.ts.map +1 -0
- package/output/{redis-cache/redis-cache.d.ts → redis/cache.d.ts} +1 -1
- package/output/redis/cache.d.ts.map +1 -0
- package/output/redis/cache.test.d.ts +2 -0
- package/output/redis/cache.test.d.ts.map +1 -0
- package/output/{redis-cache/redis-cache.test.js → redis/cache.test.js} +1 -1
- package/output/redis/index.d.ts +50 -0
- package/output/redis/index.d.ts.map +1 -0
- package/output/redis/index.js +71 -0
- package/output/redis/index.test.d.ts +2 -0
- package/output/{context → redis}/index.test.d.ts.map +1 -1
- package/output/redis/index.test.js +55 -0
- package/output/redis-queue/index.js +1 -1
- package/output/redis-queue/test-helpers.d.ts +1 -1
- package/output/redis-queue/test-helpers.js +1 -1
- package/output/redis-queue/types.d.ts +3 -12
- package/output/redis-queue/types.d.ts.map +1 -1
- package/package.json +11 -7
- package/output/app/assemble.d.ts +0 -16
- package/output/app/assemble.d.ts.map +0 -1
- package/output/app/assemble.js +0 -124
- package/output/app/database.d.ts +0 -36
- package/output/app/database.d.ts.map +0 -1
- package/output/app/database.js +0 -40
- package/output/app/database.test.d.ts.map +0 -1
- package/output/app/database.test.js +0 -66
- package/output/app/default-modules.d.ts +0 -23
- package/output/app/default-modules.d.ts.map +0 -1
- package/output/app/default-modules.js +0 -40
- package/output/app/define-server.d.ts +0 -12
- package/output/app/define-server.d.ts.map +0 -1
- package/output/app/define-server.js +0 -289
- package/output/app/define-server.test.d.ts +0 -2
- package/output/app/define-server.test.d.ts.map +0 -1
- package/output/app/define-server.test.js +0 -299
- package/output/app/env.d.ts +0 -24
- package/output/app/env.d.ts.map +0 -1
- package/output/app/env.js +0 -38
- package/output/app/env.test.js +0 -78
- package/output/app/module.d.ts +0 -88
- package/output/app/module.d.ts.map +0 -1
- package/output/app/module.js +0 -34
- package/output/app/shutdown.d.ts +0 -9
- package/output/app/shutdown.d.ts.map +0 -1
- package/output/app/shutdown.js +0 -15
- package/output/app/shutdown.test.d.ts +0 -2
- package/output/app/shutdown.test.d.ts.map +0 -1
- package/output/app/shutdown.test.js +0 -149
- package/output/app/signal.d.ts +0 -12
- package/output/app/signal.d.ts.map +0 -1
- package/output/app/steps.d.ts +0 -17
- package/output/app/steps.d.ts.map +0 -1
- package/output/app/steps.js +0 -41
- package/output/app/types.d.ts +0 -149
- package/output/app/types.d.ts.map +0 -1
- package/output/context/index.d.ts +0 -6
- package/output/context/index.d.ts.map +0 -1
- package/output/context/index.js +0 -14
- package/output/rate-limiter/index.d.ts +0 -5
- package/output/rate-limiter/index.d.ts.map +0 -1
- package/output/rate-limiter/index.js +0 -4
- package/output/rate-limiter/memory-store.d.ts.map +0 -1
- package/output/rate-limiter/rate-limiter.d.ts.map +0 -1
- package/output/rate-limiter/redis-store.d.ts.map +0 -1
- package/output/rate-limiter/redis-store.test.d.ts.map +0 -1
- package/output/rate-limiter/types.d.ts +0 -27
- package/output/rate-limiter/types.d.ts.map +0 -1
- package/output/redis-cache/index.d.ts +0 -4
- package/output/redis-cache/index.d.ts.map +0 -1
- package/output/redis-cache/index.js +0 -1
- package/output/redis-cache/redis-cache.d.ts.map +0 -1
- package/output/redis-cache/redis-cache.test.d.ts +0 -2
- package/output/redis-cache/redis-cache.test.d.ts.map +0 -1
- package/output/schema/index.d.ts.map +0 -1
- /package/output/{app → core}/env.test.d.ts +0 -0
- /package/output/{app → core/modules/auth-provider}/types.js +0 -0
- /package/output/{context → core/modules/context}/index.test.d.ts +0 -0
- /package/output/{context → core/modules/context}/index.test.js +0 -0
- /package/output/{app → core/test-support}/database.test.d.ts +0 -0
- /package/output/{rate-limiter/rate-limiter.js → rate-limit/limiter.js} +0 -0
- /package/output/{rate-limiter → rate-limit}/redis-store.test.d.ts +0 -0
- /package/output/{rate-limiter → rate-limit}/types.js +0 -0
- /package/output/{redis-cache/redis-cache.js → redis/cache.js} +0 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ConfigEnv } from '../env.js';
|
|
2
|
+
/**
|
|
3
|
+
* **工具进程**的环境出口。
|
|
4
|
+
*
|
|
5
|
+
* `startServer` / `createTestServer` 覆盖不到的进程(`prisma.config.ts`、`scripts/*.ts`、以及被
|
|
6
|
+
* 它们静态 import 的配置模块)只能自己开口要 env:ES 模块顶层求值早于入口函数执行,所以
|
|
7
|
+
* 「入口里加载 env」救不了这些文件。
|
|
8
|
+
*
|
|
9
|
+
* 只加载、不做任何闸门——运行期配置不该带上测试闸门,那会给正常启动平白加一道无关检查。
|
|
10
|
+
* 幂等:重复调用只是重复读 `dotenv`。
|
|
11
|
+
*/
|
|
12
|
+
export declare function loadRuntimeEnvironment(): ConfigEnv;
|
|
13
|
+
/**
|
|
14
|
+
* 结算**测试**数据库目标:加载测试层 → 闸门 → 结算 → 把测试连接串注入返回的 env 快照。
|
|
15
|
+
*
|
|
16
|
+
* **`createTestServer` 的私有步骤,不对外导出。** 导出它等于让调用方自己决定「什么时候跑
|
|
17
|
+
* 闸门、按什么顺序」,而顺序本身就是安全前提。
|
|
18
|
+
*
|
|
19
|
+
* 顺序(固定,不开放重排):
|
|
20
|
+
* ```
|
|
21
|
+
* 加载 .env.dev < .env < .env.local < .env.test < .env.test.local(ENV 未给时兜底 test)
|
|
22
|
+
* → 环境闸门(ENV 必须可清理)
|
|
23
|
+
* → 结算(显式 TEST_DATABASE_URL 优先,否则由 DATABASE_URL 派生 +_test,幂等)
|
|
24
|
+
* → schema 闸门(必须以 _test 结尾)
|
|
25
|
+
* → 把测试连接串写进**返回的 env 快照**(不碰全局 process.env)
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* **为什么不再改写 `process.env.DATABASE_URL`**:那是进程级副作用——同一进程里并行跑多个
|
|
29
|
+
* `createTestServer`(或测试与被测代码共享进程)时会互相覆写,谁最后写谁赢。测试目标应当
|
|
30
|
+
* 只属于**本次装配**:框架把它注入到返回的 `env` 快照里,`resources` 槽位从 `ctx.env.DATABASE_URL`
|
|
31
|
+
* 读取(框架契约:资源只读 `ctx.env`,不读 `process.env`),因此每个实例各连各的,互不干扰。
|
|
32
|
+
*
|
|
33
|
+
* 隔离比较不在这里:返回的 `env.DATABASE_URL` 此时就是测试目标,比对恒等于「自比自」。
|
|
34
|
+
* 防止测试清空应用数据靠的是**派生 + `_test` schema 闸门**——派生只会追加后缀,schema 闸门
|
|
35
|
+
* 只放行 `*_test`,所以测试目标结构上不可能是应用目标。
|
|
36
|
+
*/
|
|
37
|
+
export declare function prepareTestDatabase(): ConfigEnv;
|
|
38
|
+
//# sourceMappingURL=environment.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"environment.d.ts","sourceRoot":"","sources":["../../../source/core/test-support/environment.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAE1C;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,IAAI,SAAS,CAElD;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,mBAAmB,IAAI,SAAS,CAO/C"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { loadEnvironment, resolveConfigEnv } from '../env.js';
|
|
2
|
+
import { resolveTestDatabase } from './database.js';
|
|
3
|
+
/**
|
|
4
|
+
* **工具进程**的环境出口。
|
|
5
|
+
*
|
|
6
|
+
* `startServer` / `createTestServer` 覆盖不到的进程(`prisma.config.ts`、`scripts/*.ts`、以及被
|
|
7
|
+
* 它们静态 import 的配置模块)只能自己开口要 env:ES 模块顶层求值早于入口函数执行,所以
|
|
8
|
+
* 「入口里加载 env」救不了这些文件。
|
|
9
|
+
*
|
|
10
|
+
* 只加载、不做任何闸门——运行期配置不该带上测试闸门,那会给正常启动平白加一道无关检查。
|
|
11
|
+
* 幂等:重复调用只是重复读 `dotenv`。
|
|
12
|
+
*/
|
|
13
|
+
export function loadRuntimeEnvironment() {
|
|
14
|
+
return loadEnvironment();
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* 结算**测试**数据库目标:加载测试层 → 闸门 → 结算 → 把测试连接串注入返回的 env 快照。
|
|
18
|
+
*
|
|
19
|
+
* **`createTestServer` 的私有步骤,不对外导出。** 导出它等于让调用方自己决定「什么时候跑
|
|
20
|
+
* 闸门、按什么顺序」,而顺序本身就是安全前提。
|
|
21
|
+
*
|
|
22
|
+
* 顺序(固定,不开放重排):
|
|
23
|
+
* ```
|
|
24
|
+
* 加载 .env.dev < .env < .env.local < .env.test < .env.test.local(ENV 未给时兜底 test)
|
|
25
|
+
* → 环境闸门(ENV 必须可清理)
|
|
26
|
+
* → 结算(显式 TEST_DATABASE_URL 优先,否则由 DATABASE_URL 派生 +_test,幂等)
|
|
27
|
+
* → schema 闸门(必须以 _test 结尾)
|
|
28
|
+
* → 把测试连接串写进**返回的 env 快照**(不碰全局 process.env)
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* **为什么不再改写 `process.env.DATABASE_URL`**:那是进程级副作用——同一进程里并行跑多个
|
|
32
|
+
* `createTestServer`(或测试与被测代码共享进程)时会互相覆写,谁最后写谁赢。测试目标应当
|
|
33
|
+
* 只属于**本次装配**:框架把它注入到返回的 `env` 快照里,`resources` 槽位从 `ctx.env.DATABASE_URL`
|
|
34
|
+
* 读取(框架契约:资源只读 `ctx.env`,不读 `process.env`),因此每个实例各连各的,互不干扰。
|
|
35
|
+
*
|
|
36
|
+
* 隔离比较不在这里:返回的 `env.DATABASE_URL` 此时就是测试目标,比对恒等于「自比自」。
|
|
37
|
+
* 防止测试清空应用数据靠的是**派生 + `_test` schema 闸门**——派生只会追加后缀,schema 闸门
|
|
38
|
+
* 只放行 `*_test`,所以测试目标结构上不可能是应用目标。
|
|
39
|
+
*/
|
|
40
|
+
export function prepareTestDatabase() {
|
|
41
|
+
const loaded = loadEnvironment({ mode: 'test' });
|
|
42
|
+
const databaseUrl = resolveTestDatabase({ env: loaded.ENV });
|
|
43
|
+
// 只注入到本次装配的 env 快照,**不改写全局 process.env.DATABASE_URL**(避免进程级串扰)
|
|
44
|
+
return { ...resolveConfigEnv(), DATABASE_URL: databaseUrl };
|
|
45
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { ClientsView, LoggerLike, ResourcesView, ServiceContext } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* `createTestServiceContext()` —— **单元级**测试构造 `services` 槽位入参的唯一方式。
|
|
4
|
+
*
|
|
5
|
+
* ## 为什么框架要给这个出口
|
|
6
|
+
*
|
|
7
|
+
* `createTestServer()` 是**集成**测试的唯一入口:它跑全量装配(resources → config →
|
|
8
|
+
* clients → services → routers),代价是「每条用例都要连真库、装全部路由」。只想验一个
|
|
9
|
+
* service 函数的判断逻辑时,这个代价与收益不成比例——于是各仓各自写了一份
|
|
10
|
+
* `createServiceContext(fakeDb, { ... })`,还都带着同一句
|
|
11
|
+
* `as unknown as AccountServiceContext`:**同一个绕过动作被抄了四遍**,说明缺的是
|
|
12
|
+
* 框架能力,不是各仓的疏忽。
|
|
13
|
+
*
|
|
14
|
+
* 这里的取舍是明确的:**不伪造能力,只允许缺省能力**。
|
|
15
|
+
* - 传进来的 `resources` / `clients` 是**真实对象**(真 reader、真 db 客户端、注入式
|
|
16
|
+
* 替身),不是 mock 框架的代理——本函数不生成任何行为,只做装配。
|
|
17
|
+
* - 没传的能力被访问时**抛错**(错误信息点名是哪个键),而不是返回 `undefined` 让调用方
|
|
18
|
+
* 的 `undefined.xxx` 在更远处炸开。这区分了「这个单元不该碰数据库」与「忘了给数据库」
|
|
19
|
+
* 两种完全不同的失败。
|
|
20
|
+
*
|
|
21
|
+
* ## 为什么不干脆把 `ctx` 收窄成「这个服务用到的字段」
|
|
22
|
+
*
|
|
23
|
+
* 那会让「服务工厂第一参数恒为 ctx」这条约定退化:每个工厂的入参类型各不相同,装配层
|
|
24
|
+
* 就无法把一份 ctx 原样传下去(`createServices` 得为每个服务裁一次类型)。约定要留,
|
|
25
|
+
* 代价就由本函数承担——它给出**形状完整、能力可缺省**的 ctx。
|
|
26
|
+
*
|
|
27
|
+
* ## `resources.<key>` 为什么用 Proxy 而不是浅合并空对象
|
|
28
|
+
*
|
|
29
|
+
* 浅合并只能发现问题的一半:`resources.database` 变成了 `undefined`,错误发生在
|
|
30
|
+
* `undefined.user.findMany()` 上,栈里全都是业务代码,看不出根因是「这份 ctx 没给
|
|
31
|
+
* database」。Proxy 让错误在**取字段的那一行**抛出并点名,测试写错时的定位成本从
|
|
32
|
+
* 「翻业务代码」降到「看一行错误」。
|
|
33
|
+
*/
|
|
34
|
+
export interface TestServiceContextOptions<Resources, Config, Clients> {
|
|
35
|
+
/**
|
|
36
|
+
* 提供被测单元**实际读到**的资源能力(其余键访问即抛错)。
|
|
37
|
+
*
|
|
38
|
+
* 传真实实例:真 `PrismaClient`、`createSystemConfigReader(db)`,或对**外部 I/O
|
|
39
|
+
* 边界**(第三方 HTTP、邮件网关)注入的替身。不要替身框架自己的装配。
|
|
40
|
+
*/
|
|
41
|
+
resources?: Partial<ResourcesView<Resources>>;
|
|
42
|
+
/** 提供被测单元**实际读到**的客户端(其余键访问即抛错) */
|
|
43
|
+
clients?: Partial<ClientsView<Clients>>;
|
|
44
|
+
/** `config` 产物;被测单元按契约不读它时省略(读取会抛错,不会拿到 `undefined`) */
|
|
45
|
+
config?: Awaited<Config>;
|
|
46
|
+
/** 日志出口覆盖(缺省为静默 no-op;断言日志时传 `vi.fn()` 组成的实现) */
|
|
47
|
+
log?: Partial<LoggerLike>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 构造 `services` 槽位的测试入参(`ServiceContext`)。
|
|
51
|
+
*
|
|
52
|
+
* ```ts
|
|
53
|
+
* const ctx = createTestServiceContext<AccountResources, AccountConfig>({
|
|
54
|
+
* resources: { systemConfig: fakeReader },
|
|
55
|
+
* log: { warn: warnSpy },
|
|
56
|
+
* })
|
|
57
|
+
* const service = createRuntimeConfigService(ctx)
|
|
58
|
+
* ```
|
|
59
|
+
*
|
|
60
|
+
* 返回的 `disposer` 是**真实** `Disposer`:被测单元登记的释放函数会真的被收集,
|
|
61
|
+
* 测试可以用 `await ctx.disposer.dispose()` 验证「服务自己登记了释放」这条契约。
|
|
62
|
+
*/
|
|
63
|
+
export declare function createTestServiceContext<Resources, Config, Clients = unknown>(options?: TestServiceContextOptions<Resources, Config, Clients>): ServiceContext<Resources, Config, Clients>;
|
|
64
|
+
//# sourceMappingURL=service-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-context.d.ts","sourceRoot":"","sources":["../../../source/core/test-support/service-context.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAEzF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,MAAM,WAAW,yBAAyB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO;IACnE;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC,CAAA;IAC7C,mCAAmC;IACnC,OAAO,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAA;IACvC,wDAAwD;IACxD,MAAM,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,CAAA;IACxB,iDAAiD;IACjD,GAAG,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,CAAA;CAC1B;AAUD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,GAAG,OAAO,EAC3E,OAAO,GAAE,yBAAyB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAM,GAClE,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,CAoB5C"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Disposer } from '@taicode/common-base';
|
|
2
|
+
/** 缺省日志:静默。测试不该因为「服务打了一行 info」而需要准备 logger。 */
|
|
3
|
+
const SILENT_LOG = {
|
|
4
|
+
debug: () => undefined,
|
|
5
|
+
info: () => undefined,
|
|
6
|
+
warn: () => undefined,
|
|
7
|
+
error: () => undefined,
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* 构造 `services` 槽位的测试入参(`ServiceContext`)。
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* const ctx = createTestServiceContext<AccountResources, AccountConfig>({
|
|
14
|
+
* resources: { systemConfig: fakeReader },
|
|
15
|
+
* log: { warn: warnSpy },
|
|
16
|
+
* })
|
|
17
|
+
* const service = createRuntimeConfigService(ctx)
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* 返回的 `disposer` 是**真实** `Disposer`:被测单元登记的释放函数会真的被收集,
|
|
21
|
+
* 测试可以用 `await ctx.disposer.dispose()` 验证「服务自己登记了释放」这条契约。
|
|
22
|
+
*/
|
|
23
|
+
export function createTestServiceContext(options = {}) {
|
|
24
|
+
// 释放器是真实的:登记 / 逆序执行都是被测契约的一部分,不能替身掉
|
|
25
|
+
const disposer = new Disposer({ label: 'test-service-context', log: SILENT_LOG });
|
|
26
|
+
return {
|
|
27
|
+
log: { ...SILENT_LOG, ...options.log },
|
|
28
|
+
disposer,
|
|
29
|
+
resources: requireProvided(options.resources, 'resources'),
|
|
30
|
+
clients: requireProvided(options.clients, 'clients'),
|
|
31
|
+
get config() {
|
|
32
|
+
if (options.config === undefined) {
|
|
33
|
+
throw new Error('[test-service-context] 这份 ctx 没有提供 config:' +
|
|
34
|
+
'被测单元按契约不读 config(运行期配置一律经 resources.systemConfig)。' +
|
|
35
|
+
'若确实要读,请通过 createTestServiceContext({ config }) 显式给出。');
|
|
36
|
+
}
|
|
37
|
+
return options.config;
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 把「部分提供」包装成「形状完整、缺省即抛错」的视图。
|
|
43
|
+
*
|
|
44
|
+
* 只在**读取被缺省的键**时抛错:传了的键行为与真实视图完全一致(同一个对象、同一份
|
|
45
|
+
* 引用),所以注入式替身与真实例都照常工作。
|
|
46
|
+
*/
|
|
47
|
+
function requireProvided(provided, label) {
|
|
48
|
+
const source = provided ?? {};
|
|
49
|
+
return new Proxy(source, {
|
|
50
|
+
get(target, key, receiver) {
|
|
51
|
+
// Symbol(`Symbol.toStringTag`、`then` 探测等)与 `in` 都按普通对象处理
|
|
52
|
+
if (typeof key === 'symbol') {
|
|
53
|
+
return Reflect.get(target, key, receiver);
|
|
54
|
+
}
|
|
55
|
+
if (!(key in target)) {
|
|
56
|
+
throw new Error(`[test-service-context] 这份 ctx 没有提供 ${label}.${key}:` +
|
|
57
|
+
`被测单元读到了它,说明它确实依赖这项能力——请通过 createTestServiceContext({ ${label}: { ${key}: … } }) 给出` +
|
|
58
|
+
'(真实实例或对外部 I/O 边界的替身,不要替身框架自身的装配)。');
|
|
59
|
+
}
|
|
60
|
+
return Reflect.get(target, key, receiver);
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-context.test.d.ts","sourceRoot":"","sources":["../../../source/core/test-support/service-context.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { createTestServiceContext } from './service-context.js';
|
|
3
|
+
describe('createTestServiceContext', () => {
|
|
4
|
+
it('提供的资源与客户端原样可用(同一个引用,不做拷贝)', () => {
|
|
5
|
+
const database = { user: { count: async () => 3 } };
|
|
6
|
+
const cache = { get: () => undefined };
|
|
7
|
+
const ctx = createTestServiceContext({
|
|
8
|
+
resources: { database },
|
|
9
|
+
clients: { cache },
|
|
10
|
+
});
|
|
11
|
+
expect(ctx.resources.database).toBe(database);
|
|
12
|
+
expect(ctx.clients.cache).toBe(cache);
|
|
13
|
+
});
|
|
14
|
+
it('读取被缺省的能力时当场抛错,错误点名是哪个键', () => {
|
|
15
|
+
const ctx = createTestServiceContext({
|
|
16
|
+
resources: { database: { user: { count: async () => 0 } } },
|
|
17
|
+
});
|
|
18
|
+
// 提供的键正常
|
|
19
|
+
expect(ctx.resources.database).toBeDefined();
|
|
20
|
+
// 未提供的键:取字段那一步就抛,而不是返回 undefined 留到业务代码里炸
|
|
21
|
+
expect(() => ctx.resources.systemConfig).toThrowError(/resources\.systemConfig/);
|
|
22
|
+
});
|
|
23
|
+
it('缺省能力抛错信息里给出补法(createTestServiceContext({ resources: { … } }))', () => {
|
|
24
|
+
const ctx = createTestServiceContext();
|
|
25
|
+
expect(() => ctx.clients.anything).toThrowError(/createTestServiceContext\(\{ clients: \{ anything: … \} \}\)/);
|
|
26
|
+
});
|
|
27
|
+
it('未给 config 时读取抛错(不会拿到 undefined)', () => {
|
|
28
|
+
const ctx = createTestServiceContext();
|
|
29
|
+
expect(() => ctx.config).toThrowError(/没有提供 config/);
|
|
30
|
+
});
|
|
31
|
+
it('给了 config 时按传入值返回', () => {
|
|
32
|
+
const ctx = createTestServiceContext({
|
|
33
|
+
config: { featureEnabled: true },
|
|
34
|
+
});
|
|
35
|
+
expect(ctx.config).toEqual({ featureEnabled: true });
|
|
36
|
+
});
|
|
37
|
+
it('disposer 是真实的:登记的清理会被收集并按逆序执行', async () => {
|
|
38
|
+
const ctx = createTestServiceContext();
|
|
39
|
+
const order = [];
|
|
40
|
+
// 模拟「服务在构造期自己登记释放」这条契约
|
|
41
|
+
ctx.disposer.add(() => order.push('first'));
|
|
42
|
+
ctx.disposer.add(() => order.push('second'));
|
|
43
|
+
await ctx.disposer.dispose();
|
|
44
|
+
expect(order).toEqual(['second', 'first']);
|
|
45
|
+
});
|
|
46
|
+
it('日志缺省静默;覆盖后能断言调用', () => {
|
|
47
|
+
const warn = vi.fn();
|
|
48
|
+
const silent = createTestServiceContext();
|
|
49
|
+
const spying = createTestServiceContext({ log: { warn } });
|
|
50
|
+
// 缺省实现不抛错(pino 之外不要强制测试准备 logger)
|
|
51
|
+
expect(() => silent.log.warn({ a: 1 }, 'hello')).not.toThrow();
|
|
52
|
+
spying.log.warn({ a: 1 }, 'hello');
|
|
53
|
+
expect(warn).toHaveBeenCalledWith({ a: 1 }, 'hello');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import type { AppEnv, ClientsView, LoggerLike, ResourcesView, Server } from '../types.js';
|
|
2
|
+
import type { FastifyInstance } from 'fastify';
|
|
3
|
+
/**
|
|
4
|
+
* `createTestServer()` ——**测试对象的唯一构造方式**。
|
|
5
|
+
*
|
|
6
|
+
* ## 它是唯一的一个,不是「又一个」入口
|
|
7
|
+
*
|
|
8
|
+
* 这里有一个硬性约束:**测试必须跑在生产的同一套装配上**。请求链、响应信封、错误码映射、
|
|
9
|
+
* `request.ctx`、关闭流程,只要测试里另有一套实现,它就会在生产之外漂移,而漂移只在线暴露。
|
|
10
|
+
*
|
|
11
|
+
* 因此测试对象的构造只有一个入口,它**不接受任何槽位覆盖**:没有 `resources` 参数、没有
|
|
12
|
+
* `services` 参数、没有 `overrideXxx`。覆盖面一开,测试就能绕开生产接线,这个入口随即失去
|
|
13
|
+
* 全部证明力——那还不如直接写个假 app。
|
|
14
|
+
*
|
|
15
|
+
* 需要「只测一个模块」时,**组合定义**,而不是覆盖槽位:
|
|
16
|
+
*
|
|
17
|
+
* ```ts
|
|
18
|
+
* const moduleServer = defineServer({
|
|
19
|
+
* ...server,
|
|
20
|
+
* routers: ({ app }) => app.register(createUserRouter(stub)),
|
|
21
|
+
* })
|
|
22
|
+
* const testServer = await createTestServer(moduleServer)
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* 组合用的是同一个 `defineServer` 产物、同一套装配,只是把另一个模块的接线换掉。
|
|
26
|
+
*
|
|
27
|
+
* ## 与 `startServer` 的唯一差别
|
|
28
|
+
*
|
|
29
|
+
* `startServer` 会 listen 并装信号处理器;测试用 `app.inject()`,且 vitest worker 收不到
|
|
30
|
+
* SIGTERM。除此之外(env 分层 → resources → config → clients → services → `init()` →
|
|
31
|
+
* middlewares → 系统路由 → routers、八步关闭)**逐行相同**。
|
|
32
|
+
*
|
|
33
|
+
* ## 参数就是那个 `Server`,没有第二个参数
|
|
34
|
+
*
|
|
35
|
+
* 连接串结算、闸门、改写 `DATABASE_URL` 全部固定(见 `prepareTestDatabase()`)。
|
|
36
|
+
* 没有「用哪个连接串」这类开关:测试对象的身份就是测试,让它可以被配置成「有点像测试」
|
|
37
|
+
* 是把安全前提变成调用方的责任。
|
|
38
|
+
*
|
|
39
|
+
* ## 返回值:`TestServer` 就是测试要的一切
|
|
40
|
+
*
|
|
41
|
+
* 测试要断言的对象往往不是 HTTP 响应,而是**装配出来的东西本身**:`resources.database` 连的
|
|
42
|
+
* 是不是 `*_test`、某个 service 有没有被 `dispose`、`config` 里那个开关有没有生效。
|
|
43
|
+
* 因此返回装配期**已经构造完成**的各槽位产物,而不是让调用方重新跑一遍工厂——重新构造就等于
|
|
44
|
+
* 测试里又拼了一份,与生产接线脱钩。
|
|
45
|
+
*
|
|
46
|
+
* **不包同义方法**:没有 `close()`、没有 `getDatabase()`。`app` 就是原生 `FastifyInstance`,
|
|
47
|
+
* 关闭就是 `await app.close()`,走同一套八步流程;连接串就是 `env.DATABASE_URL`。多包一层
|
|
48
|
+
* `close()` 只是在已有接口上再加一个同义名,反而让「用哪个」变成需要记的事。
|
|
49
|
+
*
|
|
50
|
+
* **唯一的例外是 `reprovision()`**:它不是现有接口的同义名,生产入口没有对应物——
|
|
51
|
+
* 「用例之间要一份干净基线」只有测试知道。
|
|
52
|
+
*/
|
|
53
|
+
export interface TestServer<Resources, Config, Clients, Services, Env> {
|
|
54
|
+
/** 原生 Fastify 实例(已 `ready()`,用 `inject` 发请求;关闭用 `app.close()`) */
|
|
55
|
+
readonly app: FastifyInstance;
|
|
56
|
+
/**
|
|
57
|
+
* 装配后的 env 快照(框架固定约定 + 应用声明的扩展字段 `Env`,`AppEnv<Env>`)。
|
|
58
|
+
*
|
|
59
|
+
* **测试断言「连的是哪个库」就看这里**:`env.DATABASE_URL` 在装配前已结算成 `*_test`
|
|
60
|
+
* 目标,它就是本次实际连的连接串。不额外给一个 `databaseUrl` 字段——同一份事实出现两次,
|
|
61
|
+
* 迟早会有人读到另一个。应用扩展的 env(如 `REDIS_KEY_PREFIX`)也在这里、且有类型。
|
|
62
|
+
*/
|
|
63
|
+
readonly env: AppEnv<Env>;
|
|
64
|
+
/** `resources` 槽位产物(`false` 键已按 `Live<Value>` 剔除) */
|
|
65
|
+
readonly resources: ResourcesView<Resources>;
|
|
66
|
+
/** `config` 槽位产物(`Awaited<>` 归一) */
|
|
67
|
+
readonly config: Awaited<Config>;
|
|
68
|
+
/** `clients` 槽位产物 */
|
|
69
|
+
readonly clients: ClientsView<Clients>;
|
|
70
|
+
/** `services` 槽位产物(已 `init()` 过;测试直接拿它调方法,不 mock) */
|
|
71
|
+
readonly services: Awaited<Services>;
|
|
72
|
+
/** 日志器(与 `app.log` 同一个;关闭阶段仍可用) */
|
|
73
|
+
readonly log: LoggerLike;
|
|
74
|
+
/**
|
|
75
|
+
* **重新装配一个新的实例**:清掉旧实例的数据 → 关掉旧实例 → 按同一份 `env` 快照重跑一遍装配。
|
|
76
|
+
*
|
|
77
|
+
* 返回的是一份**新对象**,不是原地重启:
|
|
78
|
+
*
|
|
79
|
+
* ```ts
|
|
80
|
+
* let server = await createTestServer(server)
|
|
81
|
+
* beforeEach(async () => {
|
|
82
|
+
* server = await server.reprovision()
|
|
83
|
+
* })
|
|
84
|
+
* ```
|
|
85
|
+
*
|
|
86
|
+
* 旧对象上的一切(`app` / `resources` / `services`)**已经失效**:连接已断、`app` 已 `close()`。
|
|
87
|
+
* 还在引用旧实例的 `describe` 报错时不要奇怪——它的名字就叫“旧装配”。
|
|
88
|
+
*
|
|
89
|
+
* ## 为什么是「重新装配」而不是「复位」
|
|
90
|
+
*
|
|
91
|
+
* 状态有两类,而只有一类能被「复位」:
|
|
92
|
+
*
|
|
93
|
+
* | 状态 | 在哪 | 能不能拨回去 |
|
|
94
|
+
* | --- | --- | --- |
|
|
95
|
+
* | 库里的行 / 队列 key / 临时文件 | 外部 | 能(`clean()`) |
|
|
96
|
+
* | 限流桶、进程内缓存、连接级状态 | 内存 | **不能可靠地拨回** |
|
|
97
|
+
*
|
|
98
|
+
* 第二类覆盖地方、无法可靠拨回(漏清一处就是偶发红),而重新装配不需要「谁记得清」:
|
|
99
|
+
* `init()` 重跑一次,内存态天然就是初始值。
|
|
100
|
+
*
|
|
101
|
+
* 另一层原因在 Fastify:`ready()` 是一次性的,同一实例上无法重跑注册流程。想「不换实例而重跑
|
|
102
|
+
* 装配」在结构上就不成立。
|
|
103
|
+
*
|
|
104
|
+
* ## 四个步骤(顺序不可换)
|
|
105
|
+
*
|
|
106
|
+
* 1. **闸门**:`ENV` 必须可清理(`development` / `test`),否则直接抛错;
|
|
107
|
+
* 2. **`clean()`**:由 boot 层的 `cleanServerUnits()` 编排——资源声明序逐项清(数据库那个
|
|
108
|
+
* 必须在最前,它持有 `TRUNCATE`),再服务声明序逐项清;**必须在 `app.close()` 之前**——
|
|
109
|
+
* 关完之后连接就没了;
|
|
110
|
+
* 3. **`app.close()`**:走完整八步关闭(逐单元 `dispose()` 会在这里顺带把缓冲落盘);
|
|
111
|
+
* 4. **重新装配**:复用同一份 `env` 快照(**不重跑** `prepareTestDatabase()`),重建
|
|
112
|
+
* `resources` → `config` → `clients` → `services` → `init()` → 路由 → `ready()` → `loadTestData()`。
|
|
113
|
+
*
|
|
114
|
+
* ## 失败语义
|
|
115
|
+
*
|
|
116
|
+
* - `clean()` 失败**冒泡**:没清干净就去装配,后面全是伪绿;
|
|
117
|
+
* - `app.close()` 失败**不阻断**(八步关闭本来就是逐步骤聚合、失败只 `log.error`):
|
|
118
|
+
* 旧实例释放不完美不应该阻止下一条用例。
|
|
119
|
+
*
|
|
120
|
+
* ## 耗时
|
|
121
|
+
*
|
|
122
|
+
* 一次完整装配 = 一次 `setup()` + 一次 `init()`。这是**故意的约束**:`setup()` 必须保持在
|
|
123
|
+
* 几十毫秒量级(只做连接 / 预执 / 极小的结构检查),跑 `prisma migrate` 这类事归 npm script——
|
|
124
|
+
* `reprovision()` 是**逐用例**跑的,装配慢就等于整个测试套件慢。
|
|
125
|
+
*/
|
|
126
|
+
reprovision(): Promise<TestServer<Resources, Config, Clients, Services, Env>>;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* 构造测试对象。
|
|
130
|
+
*
|
|
131
|
+
* 固定顺序(安全闸门全部在装配之前,`resources` 一行代码都还没跑):
|
|
132
|
+
* ```
|
|
133
|
+
* prepareTestDatabase():
|
|
134
|
+
* 加载 env(强制叠加 .env.test / .env.test.local,ENV 未给时兜底 test)
|
|
135
|
+
* → 环境闸门(ENV 必须可清理)
|
|
136
|
+
* → 结算测试连接串(显式 TEST_DATABASE_URL 优先,否则派生 +_test,幂等)
|
|
137
|
+
* → schema 闸门(必须以 _test 结尾)
|
|
138
|
+
* → 回写 process.env.DATABASE_URL
|
|
139
|
+
* → resources → core → config → clients → services → init() → middlewares
|
|
140
|
+
* → 系统路由 → routers → 关闭钩子 → ready()
|
|
141
|
+
* → 装载 testdata/ 脚本并整体 load(ctx)
|
|
142
|
+
* ```
|
|
143
|
+
*
|
|
144
|
+
* `testdata/` 的 `load()` 排在 `ready()` 之后:脚本拿到的是装配好的 `ctx`(`env` + `resources` +
|
|
145
|
+
* `log`),所以它写数据用的就是被测服务读数据的那份连接——两套连接写/读同一张表而看不到对方
|
|
146
|
+
* 写入(缓存 / 连接级状态)这类问题从结构上不会发生。排在最后也让失败语义最干净:装载失败时
|
|
147
|
+
* 装配已完成、可正常 `app.close()` 释放。
|
|
148
|
+
*
|
|
149
|
+
* 不会再拿应用目标跟测试目标比对:本进程读到的 `DATABASE_URL` 此时已经是测试目标,比对恒等于
|
|
150
|
+
* 「自比自」。真正拦住「测试清空应用库」的是派生 + schema 闸门——派生只会给 schema 追加
|
|
151
|
+
* `_test`,schema 闸门只放行 `*_test`,所以测试目标结构上不可能是应用目标。
|
|
152
|
+
*/
|
|
153
|
+
export declare function createTestServer<Resources, Config, Clients, Services, Env>(server: Server<Resources, Config, Clients, Services, Env>): Promise<TestServer<Resources, Config, Clients, Services, Env>>;
|
|
154
|
+
//# sourceMappingURL=test-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-server.d.ts","sourceRoot":"","sources":["../../../source/core/test-support/test-server.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEzF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAA;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AACH,MAAM,WAAW,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG;IACnE,kEAAkE;IAClE,QAAQ,CAAC,GAAG,EAAE,eAAe,CAAA;IAC7B;;;;;;OAMG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,CAAA;IACzB,qDAAqD;IACrD,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC,SAAS,CAAC,CAAA;IAC5C,oCAAoC;IACpC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,CAAA;IAChC,qBAAqB;IACrB,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,OAAO,CAAC,CAAA;IACtC,qDAAqD;IACrD,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAA;IACpC,mCAAmC;IACnC,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAA;IACxB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAmDG;IACH,WAAW,IAAI,OAAO,CAAC,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAA;CAC9E;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAsB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,CAAC,CAAC,CAO3M"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { cleanServerUnits, initServiceUnits } from '../boot/lifecycle.js';
|
|
2
|
+
import { buildClients, buildConfig, buildResources, buildServices, createBootContext, installShutdownHook, registerCoreStep, registerMiddlewares, registerRouters, registerSystemRoutesStep, resolveAppEnv, setupResources, } from '../boot/steps.js';
|
|
3
|
+
import { assertCleanableEnvironment } from '../env.js';
|
|
4
|
+
import { maskDatabaseUrl } from './database.js';
|
|
5
|
+
import { prepareTestDatabase } from './environment.js';
|
|
6
|
+
import { discoverTestDataModules, loadTestData } from './testdata.js';
|
|
7
|
+
/**
|
|
8
|
+
* 构造测试对象。
|
|
9
|
+
*
|
|
10
|
+
* 固定顺序(安全闸门全部在装配之前,`resources` 一行代码都还没跑):
|
|
11
|
+
* ```
|
|
12
|
+
* prepareTestDatabase():
|
|
13
|
+
* 加载 env(强制叠加 .env.test / .env.test.local,ENV 未给时兜底 test)
|
|
14
|
+
* → 环境闸门(ENV 必须可清理)
|
|
15
|
+
* → 结算测试连接串(显式 TEST_DATABASE_URL 优先,否则派生 +_test,幂等)
|
|
16
|
+
* → schema 闸门(必须以 _test 结尾)
|
|
17
|
+
* → 回写 process.env.DATABASE_URL
|
|
18
|
+
* → resources → core → config → clients → services → init() → middlewares
|
|
19
|
+
* → 系统路由 → routers → 关闭钩子 → ready()
|
|
20
|
+
* → 装载 testdata/ 脚本并整体 load(ctx)
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* `testdata/` 的 `load()` 排在 `ready()` 之后:脚本拿到的是装配好的 `ctx`(`env` + `resources` +
|
|
24
|
+
* `log`),所以它写数据用的就是被测服务读数据的那份连接——两套连接写/读同一张表而看不到对方
|
|
25
|
+
* 写入(缓存 / 连接级状态)这类问题从结构上不会发生。排在最后也让失败语义最干净:装载失败时
|
|
26
|
+
* 装配已完成、可正常 `app.close()` 释放。
|
|
27
|
+
*
|
|
28
|
+
* 不会再拿应用目标跟测试目标比对:本进程读到的 `DATABASE_URL` 此时已经是测试目标,比对恒等于
|
|
29
|
+
* 「自比自」。真正拦住「测试清空应用库」的是派生 + schema 闸门——派生只会给 schema 追加
|
|
30
|
+
* `_test`,schema 闸门只放行 `*_test`,所以测试目标结构上不可能是应用目标。
|
|
31
|
+
*/
|
|
32
|
+
export async function createTestServer(server) {
|
|
33
|
+
// 闸门 + 结算测试连接串全在框架内部,顺序不由调用方决定;**不改写全局 process.env**(见
|
|
34
|
+
// `prepareTestDatabase`)——测试目标只注入到本次装配的 `env` 快照里,`resources` 从
|
|
35
|
+
// `ctx.env.DATABASE_URL` 读取,同进程并行多个实例互不污染。
|
|
36
|
+
const env = resolveAppEnv(prepareTestDatabase());
|
|
37
|
+
return assemble(server, env);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 一次完整装配(`createTestServer` 与 `reprovision()` 共用)。
|
|
41
|
+
*
|
|
42
|
+
* 共用是刻意的:`reprovision()` 走的是**同一条**装配路径,不是「简化版重启」。两条路径一旦
|
|
43
|
+
* 分叉,测试里跑的就和第一次装配不再是同一件事,`reprovision()` 之后的用例就失去了证明力。
|
|
44
|
+
*
|
|
45
|
+
* 入参 `env` 是**已结算好**的快照:`reprovision()` 复用它、不重跑 `prepareTestDatabase()`——
|
|
46
|
+
* 连接串 / ENV 在一次测试生命周期里不该变,重算只会引入「第二次结算结果不同」的怪异失败。
|
|
47
|
+
*
|
|
48
|
+
* 返回的**只有** `TestServer`:装配期单元(`ResourceSetupUnit[]` 之类)是 boot 层的产物,
|
|
49
|
+
* 只在本函数的闭包里给 `reprovision()` 用,不往测试入口的外面漏。
|
|
50
|
+
*/
|
|
51
|
+
async function assemble(server, env) {
|
|
52
|
+
// 以下步序与 `startServer` 逐行对应,只去掉 listen 与信号处理器
|
|
53
|
+
const ctx = createBootContext(server, env);
|
|
54
|
+
const { resources, units: resourceUnits, classifyResourceError } = await buildResources(ctx);
|
|
55
|
+
await setupResources(ctx, resourceUnits);
|
|
56
|
+
await registerCoreStep(ctx, classifyResourceError);
|
|
57
|
+
await buildConfig(ctx, resources);
|
|
58
|
+
const clients = await buildClients(ctx, resources);
|
|
59
|
+
const services = await buildServices(ctx, resources, clients);
|
|
60
|
+
await initServiceUnits({
|
|
61
|
+
service: ctx.service,
|
|
62
|
+
log: ctx.log,
|
|
63
|
+
services: services,
|
|
64
|
+
});
|
|
65
|
+
await registerMiddlewares(ctx, services, clients);
|
|
66
|
+
registerSystemRoutesStep(ctx, services);
|
|
67
|
+
await registerRouters(ctx, services, clients);
|
|
68
|
+
installShutdownHook(ctx, services, resourceUnits);
|
|
69
|
+
await ctx.app.ready();
|
|
70
|
+
ctx.log.info({ service: ctx.service, database: maskDatabaseUrl(env.DATABASE_URL), env: env.ENV }, '[test-server] ready(测试实例不会监听端口)');
|
|
71
|
+
// `buildConfig` 一定跑过(本路径不跳过 config),缺了就按「没有 config」的语义
|
|
72
|
+
// 如实报错(`read()` 抛 `CONFIG_UNAVAILABLE`),而不是拿 `undefined` 冒充产物。
|
|
73
|
+
if (!ctx.configAccess.available) {
|
|
74
|
+
throw new Error('[test-server] config 槽位没有产出:装配顺序被改坏了(应当紧跟 resources 之后)');
|
|
75
|
+
}
|
|
76
|
+
// 装载`testdata/` 脚本并整体 `load(ctx)`:闸门(ENV 必须可清理)在 `loadTestData` 内部,
|
|
77
|
+
// 这里不重复判断,也不降级成「非测试就不算」——失败直接冒泡。
|
|
78
|
+
//
|
|
79
|
+
// `ctx` 与 `resources` 槽位同一个口径:脚本写数据 / 清缓存用的就是被测服务的那份实例。
|
|
80
|
+
const testDataCtx = {
|
|
81
|
+
env,
|
|
82
|
+
resources,
|
|
83
|
+
config: ctx.configAccess.read(),
|
|
84
|
+
clients,
|
|
85
|
+
services,
|
|
86
|
+
log: ctx.log,
|
|
87
|
+
};
|
|
88
|
+
// 脚本名单只在本函数的闭包里用:装配时 `load()` 一次就够了。用例之间的复位是
|
|
89
|
+
// `reprovision()` 的职责(重跑装配 = 重新发现 + 重新 `load`),返回值不需要把它挂在外面。
|
|
90
|
+
const testData = await discoverTestDataModules();
|
|
91
|
+
await loadTestData({ ctx: testDataCtx, units: testData, service: ctx.service });
|
|
92
|
+
return {
|
|
93
|
+
app: ctx.app,
|
|
94
|
+
env,
|
|
95
|
+
resources,
|
|
96
|
+
config: ctx.configAccess.read(),
|
|
97
|
+
clients,
|
|
98
|
+
services,
|
|
99
|
+
log: ctx.log,
|
|
100
|
+
reprovision: async () => {
|
|
101
|
+
// 1. 闸门先于一切:非可清理环境下连 `clean()` 都不该跑
|
|
102
|
+
assertCleanableEnvironment(env.ENV);
|
|
103
|
+
// 2. 清数据**必须在 `close()` 之前**:连接一断就清不了了。顺序由 boot 层编排
|
|
104
|
+
// (资源在前、服务在后),本入口只说「给我一份干净基线」。失败冒泡——
|
|
105
|
+
// 没清干净就重装,后面全是伪绿。
|
|
106
|
+
await cleanServerUnits({ service: ctx.service, log: ctx.log, resources: resourceUnits, services: services });
|
|
107
|
+
// 3. 关掉旧实例:八步关闭自带逐单元 `dispose()`(缓冲落盘的归属就是它),
|
|
108
|
+
// 失败只 `log.error`、不阻断——旧实例释放不完美不该拦住下一条用例。
|
|
109
|
+
await ctx.app.close();
|
|
110
|
+
// 4. 用同一份 `env` 快照重新装配(内部照常 `loadTestData`)
|
|
111
|
+
return assemble(server, env);
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-server.test.d.ts","sourceRoot":"","sources":["../../../source/core/test-support/test-server.test.ts"],"names":[],"mappings":""}
|