@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,271 @@
|
|
|
1
|
+
import { readdir } from 'node:fs/promises';
|
|
2
|
+
import { basename, extname, join, resolve } from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { assertCleanableEnvironment } from '../env.js';
|
|
5
|
+
/**
|
|
6
|
+
* 声明一个测试数据脚本(**唯一正当的写法**)。
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* // packages/server/testdata/03-system-config.ts
|
|
10
|
+
* import { defineTestData } from '@taicode/common-server/app'
|
|
11
|
+
* import type { server } from '../source/server' // 纯类型导入,无副作用
|
|
12
|
+
*
|
|
13
|
+
* export default defineTestData<typeof server>({
|
|
14
|
+
* dependsOn: ['01-database'],
|
|
15
|
+
* async load(ctx) { …插入样本数据… },
|
|
16
|
+
* async cleanup(ctx) { …删掉自己插入的数据… },
|
|
17
|
+
* })
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* 泛型写 `typeof server` 而不是逐个槽位:`ctx` 上的 `resources` / `config` / `clients` /
|
|
21
|
+
* `services` / `env` 类型一次到位。只想声明 `resources` 时也可以写
|
|
22
|
+
* `defineTestData<AccountResources>({ … })`(其余槽位退化为 `object`)。
|
|
23
|
+
*
|
|
24
|
+
* ## 它解决什么
|
|
25
|
+
*
|
|
26
|
+
* 命名导出 `load` 是**约定**,写错只在装载期才会暴露(少写 `load` = 启动直接失败)。
|
|
27
|
+
* `defineTestData` 把这份约定变成**定义期**就检查的东西:
|
|
28
|
+
*
|
|
29
|
+
* 1. **缺 `load` 当场报错**(不是等到 `discoverTestDataModules()` 扫描到它);
|
|
30
|
+
* 2. **`dependsOn` 当场校验**(必须是字符串数组、不能有重复项;引用不存在的脚本 / 自依赖 /
|
|
31
|
+
* 环属于跨文件问题,仍在装载期由 `sortByDependencies()` 统一判定);
|
|
32
|
+
* 3. **结果冻结**:脚本之间不会互相改写 `load` / `cleanup`(执行器拿到的就是定义时那份)。
|
|
33
|
+
*
|
|
34
|
+
* `ctx` 的类型由 `S` 一次性带进来:`defineTestData<typeof server>({ load(ctx) {…} })` 里
|
|
35
|
+
* `ctx.services` / `ctx.resources` / `ctx.config` 全部有类型,不需要每个脚本再手写
|
|
36
|
+
* `type Ctx = TestDataContext<…>`。
|
|
37
|
+
*
|
|
38
|
+
* ## 与「命名导出」的关系
|
|
39
|
+
*
|
|
40
|
+
* 两种写法都合法,`defineTestData` 只是把结构显式化:
|
|
41
|
+
*
|
|
42
|
+
* ```ts
|
|
43
|
+
* // 推荐:默认导出一个定义对象
|
|
44
|
+
* export default defineTestData<typeof server>({ load })
|
|
45
|
+
*
|
|
46
|
+
* // 等价:命名导出(旧写法,继续支持)
|
|
47
|
+
* export async function load(ctx: TestDataContext<AccountResources>) { … }
|
|
48
|
+
* export const dependsOn = ['01-database']
|
|
49
|
+
* ```
|
|
50
|
+
*
|
|
51
|
+
* 同一个文件里两者都有时,**默认导出优先**——避免「改了默认导出、命名导出还在生效」的歧义。
|
|
52
|
+
*/
|
|
53
|
+
export function defineTestData(definition) {
|
|
54
|
+
// 类型上 `load` 必填,但脚本是纯 JS 生态:运行期仍要挡一次(写错时在定义期就炸,而不是装载期)
|
|
55
|
+
const script = definition;
|
|
56
|
+
if (typeof script.load !== 'function') {
|
|
57
|
+
throw new Error('[testdata] defineTestData 缺少 load:必须导出 load(ctx)');
|
|
58
|
+
}
|
|
59
|
+
if (script.cleanup !== undefined && typeof script.cleanup !== 'function') {
|
|
60
|
+
throw new Error('[testdata] defineTestData 的 cleanup 必须是函数(不需要清理就不写)');
|
|
61
|
+
}
|
|
62
|
+
const dependsOn = script.dependsOn;
|
|
63
|
+
if (dependsOn === undefined) {
|
|
64
|
+
// `TestDataScriptOf<S, Env>` 是条件类型,裸泛型参数上无法自动归约;这里的结构已经由入参约束过
|
|
65
|
+
return Object.freeze({ load: definition.load, cleanup: definition.cleanup });
|
|
66
|
+
}
|
|
67
|
+
if (!Array.isArray(dependsOn) || dependsOn.some((item) => typeof item !== 'string' || item.length === 0)) {
|
|
68
|
+
throw new Error('[testdata] defineTestData 的 dependsOn 必须是非空字符串数组(引用其它脚本的去扩展名文件名)');
|
|
69
|
+
}
|
|
70
|
+
if (new Set(dependsOn).size !== dependsOn.length) {
|
|
71
|
+
throw new Error(`[testdata] defineTestData 的 dependsOn 有重复项:${dependsOn.join(', ')}`);
|
|
72
|
+
}
|
|
73
|
+
return Object.freeze({
|
|
74
|
+
load: definition.load,
|
|
75
|
+
cleanup: definition.cleanup,
|
|
76
|
+
dependsOn: Object.freeze([...dependsOn]),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/** 默认脚本目录(相对 `process.cwd()`) */
|
|
80
|
+
export function resolveTestDataDirectory() {
|
|
81
|
+
return resolve(process.cwd(), DEFAULT_TESTDATA_DIRECTORY);
|
|
82
|
+
}
|
|
83
|
+
const DEFAULT_TESTDATA_DIRECTORY = 'testdata';
|
|
84
|
+
/** 会被当作脚本的扩展名 */
|
|
85
|
+
const TESTDATA_EXTENSIONS = ['.ts', '.mts', '.cts', '.js', '.mjs', '.cjs'];
|
|
86
|
+
/** `_` 前缀 = 共享 helper,不参与 load / cleanup */
|
|
87
|
+
const HELPER_PREFIX = '_';
|
|
88
|
+
function isTestDataFile(name) {
|
|
89
|
+
if (name.startsWith(HELPER_PREFIX)) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
// `.d.ts` 是纯声明,没有运行期实现
|
|
93
|
+
if (name.endsWith('.d.ts')) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
return TESTDATA_EXTENSIONS.includes(extname(name));
|
|
97
|
+
}
|
|
98
|
+
function isRecord(value) {
|
|
99
|
+
return typeof value === 'object' && value !== null;
|
|
100
|
+
}
|
|
101
|
+
/** 读一个脚本模块的 `dependsOn`(缺省 = 空数组;类型不对直接报错) */
|
|
102
|
+
function readDependsOn(file, script) {
|
|
103
|
+
const value = script.dependsOn;
|
|
104
|
+
if (value === undefined) {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) {
|
|
108
|
+
throw new Error(`[testdata] "${file}" 的 dependsOn 必须是字符串数组(引用其它脚本的去扩展名文件名)`);
|
|
109
|
+
}
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* 从一个已 `import()` 的模块里取出脚本本体。
|
|
114
|
+
*
|
|
115
|
+
* **默认导出优先**:`export default defineTestData({ … })` 是推荐写法;同一个文件里还留着
|
|
116
|
+
* 命名导出时以默认导出为准,避免「改了默认导出、命名导出仍在生效」这种看不见的分裂。
|
|
117
|
+
*/
|
|
118
|
+
function readScript(file, loaded) {
|
|
119
|
+
const candidate = loaded.default;
|
|
120
|
+
if (isRecord(candidate) && typeof candidate.load === 'function') {
|
|
121
|
+
return candidate;
|
|
122
|
+
}
|
|
123
|
+
return loaded;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* 按 `dependsOn` 做**稳定**拓扑排序(Kahn):同层取文件名升序。
|
|
127
|
+
*
|
|
128
|
+
* 三个不变式:
|
|
129
|
+
* - **依赖是权威**:只要 A 声明依赖 B,A 一定排在 B 之后,文件名管不了它。
|
|
130
|
+
* - **同层可复现**:没有依赖关系的脚本按文件名升序,跑多少次都是同一个序。
|
|
131
|
+
* - **坏图直接报错**:未知依赖 / 自依赖 / 循环依赖都在装载期抛错,不猜顺序——
|
|
132
|
+
* 猜错只会变成偶发的外键冲突,比当场失败难查得多。
|
|
133
|
+
*/
|
|
134
|
+
function sortByDependencies(units) {
|
|
135
|
+
const known = new Set(units.map((unit) => unit.name));
|
|
136
|
+
const pending = new Map();
|
|
137
|
+
const dependents = new Map();
|
|
138
|
+
for (const unit of units) {
|
|
139
|
+
const deps = [...new Set(unit.dependsOn ?? [])];
|
|
140
|
+
for (const dep of deps) {
|
|
141
|
+
if (dep === unit.name) {
|
|
142
|
+
throw new Error(`[testdata] "${unit.name}" 依赖了自己:dependsOn 只能引用其它脚本`);
|
|
143
|
+
}
|
|
144
|
+
if (!known.has(dep)) {
|
|
145
|
+
throw new Error(`[testdata] "${unit.name}" 依赖的 "${dep}" 不存在:dependsOn 用去扩展名的文件名`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
pending.set(unit.name, deps.length);
|
|
149
|
+
for (const dep of deps) {
|
|
150
|
+
dependents.set(dep, [...(dependents.get(dep) ?? []), unit.name]);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// 候选池保持升序:同层先跑文件名小的
|
|
154
|
+
const ready = units.filter((unit) => pending.get(unit.name) === 0).map((unit) => unit.name).sort();
|
|
155
|
+
const order = [];
|
|
156
|
+
while (ready.length > 0) {
|
|
157
|
+
const name = ready.shift();
|
|
158
|
+
order.push(name);
|
|
159
|
+
for (const next of dependents.get(name) ?? []) {
|
|
160
|
+
const rest = (pending.get(next) ?? 0) - 1;
|
|
161
|
+
pending.set(next, rest);
|
|
162
|
+
if (rest === 0) {
|
|
163
|
+
ready.push(next);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
ready.sort();
|
|
167
|
+
}
|
|
168
|
+
if (order.length !== units.length) {
|
|
169
|
+
const stuck = units.filter((unit) => !order.includes(unit.name)).map((unit) => unit.name);
|
|
170
|
+
throw new Error(`[testdata] 存在循环依赖:${stuck.join(' → ')}`);
|
|
171
|
+
}
|
|
172
|
+
return order.map((name) => units.find((unit) => unit.name === name));
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* 扫描目录并**装载**全部脚本(`import()` 一次,模块本身由 ESM 缓存复用)。
|
|
176
|
+
*
|
|
177
|
+
* 目录不存在 → 空数组(不是错误:没写测试数据的服务是合法的)。
|
|
178
|
+
* 文件存在但缺 `load` → **抛错**,并指出是哪个文件。
|
|
179
|
+
*
|
|
180
|
+
* 返回的顺序就是**执行序**:`dependsOn` 拓扑序 + 同层文件名升序(`load` 直接正序跑,
|
|
181
|
+
* `cleanup` 直接逆序跑)。
|
|
182
|
+
*/
|
|
183
|
+
export async function discoverTestDataModules(directory = resolveTestDataDirectory()) {
|
|
184
|
+
let entries;
|
|
185
|
+
try {
|
|
186
|
+
entries = (await readdir(directory, { withFileTypes: true }))
|
|
187
|
+
.filter((entry) => entry.isFile())
|
|
188
|
+
.map((entry) => entry.name);
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
if (isRecord(error) && error.code === 'ENOENT') {
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
const files = entries.filter(isTestDataFile).sort();
|
|
197
|
+
const units = [];
|
|
198
|
+
for (const file of files) {
|
|
199
|
+
const name = basename(file, extname(file));
|
|
200
|
+
const moduleUrl = pathToFileURL(join(directory, file)).href;
|
|
201
|
+
let loaded;
|
|
202
|
+
try {
|
|
203
|
+
loaded = await import(moduleUrl);
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
throw new Error(`[testdata] 装载 "${file}" 失败:${error instanceof Error ? error.message : String(error)}`, {
|
|
207
|
+
cause: error,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
if (!isRecord(loaded)) {
|
|
211
|
+
throw new Error(`[testdata] "${file}" 没有导出任何东西:必须导出 load(或用 defineTestData 作为默认导出)`);
|
|
212
|
+
}
|
|
213
|
+
const script = readScript(file, loaded);
|
|
214
|
+
if (typeof script.load !== 'function') {
|
|
215
|
+
throw new Error(`[testdata] "${file}" 缺少 load 导出:必须导出 load(ctx)`);
|
|
216
|
+
}
|
|
217
|
+
if (script.cleanup !== undefined && typeof script.cleanup !== 'function') {
|
|
218
|
+
throw new Error(`[testdata] "${file}" 的 cleanup 必须是函数(不需要清理就删掉这个导出)`);
|
|
219
|
+
}
|
|
220
|
+
units.push({
|
|
221
|
+
name,
|
|
222
|
+
dependsOn: readDependsOn(file, script),
|
|
223
|
+
load: script.load,
|
|
224
|
+
cleanup: script.cleanup,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
return sortByDependencies(units);
|
|
228
|
+
}
|
|
229
|
+
/** 跑一个脚本步骤(统一日志,失败冒泡给调用方决定) */
|
|
230
|
+
async function runTestDataUnit(params, unit, phase) {
|
|
231
|
+
const step = unit[phase];
|
|
232
|
+
if (!step) {
|
|
233
|
+
// `cleanup` 可选:没写的脚本由资源单元的 `clean()` 兜底
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const fields = { service: params.service ?? '', unit: unit.name, stage: 'testdata', phase };
|
|
237
|
+
params.ctx.log.debug(fields, `[testdata] ${phase} "${unit.name}" start`);
|
|
238
|
+
try {
|
|
239
|
+
await step(params.ctx);
|
|
240
|
+
params.ctx.log.debug(fields, `[testdata] ${phase} "${unit.name}" done`);
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
params.ctx.log.error({ ...fields, error }, `[testdata] ${phase} "${unit.name}" failed`);
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* 加载测试数据:按**执行序**逐项 `await load(ctx)`(依赖在前,同层按文件名升序)。
|
|
249
|
+
*
|
|
250
|
+
* 在 `createTestServer()` / `startServer()` 装配完成后自动执行一次;也可以由测试自己再调。
|
|
251
|
+
*/
|
|
252
|
+
export async function loadTestData(params) {
|
|
253
|
+
// 闸门先于一切:非可清理环境下一个脚本都不许跑
|
|
254
|
+
assertCleanableEnvironment(params.ctx.env.ENV);
|
|
255
|
+
for (const unit of sortByDependencies(params.units ?? [])) {
|
|
256
|
+
await runTestDataUnit(params, unit, 'load');
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* 清理测试数据:按执行序**逆序**逐项 `await cleanup()`(后建的先拆)。
|
|
261
|
+
*
|
|
262
|
+
* 依赖不必反向声明:`load` 的拓扑序取逆就是安全的拆除序。没有 `cleanup` 的脚本直接跳过。
|
|
263
|
+
*
|
|
264
|
+
* 注意:它只负责「把脚本插入的东西撤掉」,不是「把整个库清空」——库由资源单元的 `clean()` 清。
|
|
265
|
+
*/
|
|
266
|
+
export async function cleanupTestData(params) {
|
|
267
|
+
assertCleanableEnvironment(params.ctx.env.ENV);
|
|
268
|
+
for (const unit of sortByDependencies(params.units ?? []).reverse()) {
|
|
269
|
+
await runTestDataUnit(params, unit, 'cleanup');
|
|
270
|
+
}
|
|
271
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"testdata.test.d.ts","sourceRoot":"","sources":["../../../source/core/test-support/testdata.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { cleanupTestData, defineTestData, discoverTestDataModules, loadTestData, resolveTestDataDirectory } from './testdata.js';
|
|
6
|
+
const ORIGINAL_ENV = { ...process.env };
|
|
7
|
+
const tempDirs = [];
|
|
8
|
+
afterEach(async () => {
|
|
9
|
+
process.env = { ...ORIGINAL_ENV };
|
|
10
|
+
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
|
|
11
|
+
});
|
|
12
|
+
/** 造一个临时脚本目录(内容按 `文件名 → 模块源码` 写入) */
|
|
13
|
+
async function makeDirectory(files) {
|
|
14
|
+
const dir = await mkdtemp(join(tmpdir(), 'common-testdata-'));
|
|
15
|
+
tempDirs.push(dir);
|
|
16
|
+
await Promise.all(Object.entries(files).map(([name, source]) => writeFile(join(dir, name), source)));
|
|
17
|
+
return dir;
|
|
18
|
+
}
|
|
19
|
+
/** 造一个记录调用顺序的脚本源码(模块级数组挂在 globalThis 上,跨 import 共享) */
|
|
20
|
+
function scriptSource(name, recorder) {
|
|
21
|
+
return `
|
|
22
|
+
const record = (phase) => globalThis.${recorder}.push(\`\${phase}:${name}\`)
|
|
23
|
+
export function load() { record('load') }
|
|
24
|
+
export function cleanup() { record('cleanup') }
|
|
25
|
+
`;
|
|
26
|
+
}
|
|
27
|
+
describe('discoverTestDataModules', () => {
|
|
28
|
+
it('目录不存在时返回空数组(没写测试数据的服务是合法的)', async () => {
|
|
29
|
+
const dir = join(tmpdir(), `common-testdata-missing-${Date.now()}`);
|
|
30
|
+
await expect(discoverTestDataModules(dir)).resolves.toEqual([]);
|
|
31
|
+
});
|
|
32
|
+
it('按文件名升序装载,name 为去扩展名的文件名', async () => {
|
|
33
|
+
const dir = await makeDirectory({
|
|
34
|
+
'02-second.mjs': scriptSource('second', '__order02'),
|
|
35
|
+
'01-first.mjs': scriptSource('first', '__order01'),
|
|
36
|
+
});
|
|
37
|
+
const units = await discoverTestDataModules(dir);
|
|
38
|
+
expect(units.map((unit) => unit.name)).toEqual(['01-first', '02-second']);
|
|
39
|
+
expect(units.every((unit) => typeof unit.load === 'function' && typeof unit.cleanup === 'function')).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
it('`_` 开头的文件是共享 helper,不参与 load / cleanup', async () => {
|
|
42
|
+
const dir = await makeDirectory({
|
|
43
|
+
'_shared.mjs': 'export const helper = 1',
|
|
44
|
+
'01-real.mjs': scriptSource('real', '__orderShared'),
|
|
45
|
+
});
|
|
46
|
+
const units = await discoverTestDataModules(dir);
|
|
47
|
+
expect(units.map((unit) => unit.name)).toEqual(['01-real']);
|
|
48
|
+
});
|
|
49
|
+
it('非脚本文件(.md / .json / .d.ts)被忽略', async () => {
|
|
50
|
+
const dir = await makeDirectory({
|
|
51
|
+
'README.md': '# 说明',
|
|
52
|
+
'data.json': '{}',
|
|
53
|
+
'types.d.ts': 'export declare function load(): void',
|
|
54
|
+
'01-real.mjs': scriptSource('real', '__orderIgnore'),
|
|
55
|
+
});
|
|
56
|
+
const units = await discoverTestDataModules(dir);
|
|
57
|
+
expect(units.map((unit) => unit.name)).toEqual(['01-real']);
|
|
58
|
+
});
|
|
59
|
+
it('缺 cleanup 不报错(库里的行由资源单元的 clean() 兜底)', async () => {
|
|
60
|
+
const dir = await makeDirectory({
|
|
61
|
+
'01-plain.mjs': 'export function load() { globalThis.__orderNoCleanup.push(\'load\') }',
|
|
62
|
+
});
|
|
63
|
+
const order = globalThis.__orderNoCleanup = [];
|
|
64
|
+
const units = await discoverTestDataModules(dir);
|
|
65
|
+
expect(units.map((unit) => unit.name)).toEqual(['01-plain']);
|
|
66
|
+
expect(units[0]?.cleanup).toBeUndefined();
|
|
67
|
+
// `load` 照跑,`cleanup` 直接跳过(不是报错)
|
|
68
|
+
await loadTestData({ ctx: makeCtx('test'), units });
|
|
69
|
+
await cleanupTestData({ ctx: makeCtx('test'), units });
|
|
70
|
+
expect(order).toEqual(['load']);
|
|
71
|
+
});
|
|
72
|
+
it('缺 load 导出直接抛错', async () => {
|
|
73
|
+
const dir = await makeDirectory({
|
|
74
|
+
'01-bad.mjs': 'export function cleanup() {}',
|
|
75
|
+
});
|
|
76
|
+
await expect(discoverTestDataModules(dir)).rejects.toThrow(/缺少 load 导出/);
|
|
77
|
+
});
|
|
78
|
+
it('装载期抛错时带上文件名(定位不要靠猜)', async () => {
|
|
79
|
+
const dir = await makeDirectory({
|
|
80
|
+
'01-broken.mjs': 'throw new Error("boom")',
|
|
81
|
+
});
|
|
82
|
+
await expect(discoverTestDataModules(dir)).rejects.toThrow(/装载 "01-broken.mjs" 失败/);
|
|
83
|
+
});
|
|
84
|
+
it('dependsOn 缺省时按文件名升序', async () => {
|
|
85
|
+
const dir = await makeDirectory({
|
|
86
|
+
'02-b.mjs': scriptSource('b', '__orderNoDep'),
|
|
87
|
+
'01-a.mjs': scriptSource('a', '__orderNoDep'),
|
|
88
|
+
});
|
|
89
|
+
await expect(discoverTestDataModules(dir)).resolves.toMatchObject([{ name: '01-a' }, { name: '02-b' }]);
|
|
90
|
+
});
|
|
91
|
+
it('dependsOn 决定顺序:依赖在前,文件名管不了它', async () => {
|
|
92
|
+
const dir = await makeDirectory({
|
|
93
|
+
'01-late.mjs': `export const dependsOn = ['99-early']\n${scriptSource('late', '__orderDep')}`,
|
|
94
|
+
'99-early.mjs': scriptSource('early', '__orderDep'),
|
|
95
|
+
});
|
|
96
|
+
const units = await discoverTestDataModules(dir);
|
|
97
|
+
expect(units.map((unit) => unit.name)).toEqual(['99-early', '01-late']);
|
|
98
|
+
});
|
|
99
|
+
it('同层按文件名升序(无依赖关系的脚本可复现)', async () => {
|
|
100
|
+
const dir = await makeDirectory({
|
|
101
|
+
'03-c.mjs': `export const dependsOn = ['01-base']\n${scriptSource('c', '__orderTie')}`,
|
|
102
|
+
'02-b.mjs': `export const dependsOn = ['01-base']\n${scriptSource('b', '__orderTie')}`,
|
|
103
|
+
'01-base.mjs': scriptSource('base', '__orderTie'),
|
|
104
|
+
});
|
|
105
|
+
const units = await discoverTestDataModules(dir);
|
|
106
|
+
expect(units.map((unit) => unit.name)).toEqual(['01-base', '02-b', '03-c']);
|
|
107
|
+
});
|
|
108
|
+
it('依赖不存在的脚本直接抛错', async () => {
|
|
109
|
+
const dir = await makeDirectory({
|
|
110
|
+
'01-a.mjs': `export const dependsOn = ['99-missing']\n${scriptSource('a', '__orderMissing')}`,
|
|
111
|
+
});
|
|
112
|
+
await expect(discoverTestDataModules(dir)).rejects.toThrow(/依赖的 "99-missing" 不存在/);
|
|
113
|
+
});
|
|
114
|
+
it('依赖自己直接抛错', async () => {
|
|
115
|
+
const dir = await makeDirectory({
|
|
116
|
+
'01-a.mjs': `export const dependsOn = ['01-a']\n${scriptSource('a', '__orderSelf')}`,
|
|
117
|
+
});
|
|
118
|
+
await expect(discoverTestDataModules(dir)).rejects.toThrow(/依赖了自己/);
|
|
119
|
+
});
|
|
120
|
+
it('循环依赖直接抛错(不猜顺序)', async () => {
|
|
121
|
+
const dir = await makeDirectory({
|
|
122
|
+
'01-a.mjs': `export const dependsOn = ['02-b']\n${scriptSource('a', '__orderCycle')}`,
|
|
123
|
+
'02-b.mjs': `export const dependsOn = ['01-a']\n${scriptSource('b', '__orderCycle')}`,
|
|
124
|
+
});
|
|
125
|
+
await expect(discoverTestDataModules(dir)).rejects.toThrow(/循环依赖/);
|
|
126
|
+
});
|
|
127
|
+
it('dependsOn 不是字符串数组直接抛错', async () => {
|
|
128
|
+
const dir = await makeDirectory({
|
|
129
|
+
'01-a.mjs': `export const dependsOn = [1]\n${scriptSource('a', '__orderType')}`,
|
|
130
|
+
});
|
|
131
|
+
await expect(discoverTestDataModules(dir)).rejects.toThrow(/dependsOn 必须是字符串数组/);
|
|
132
|
+
});
|
|
133
|
+
it('`export default { load, cleanup }` 也是合法写法(定义对象直接可用)', async () => {
|
|
134
|
+
const dir = await makeDirectory({
|
|
135
|
+
'01-default.mjs': `
|
|
136
|
+
export default {
|
|
137
|
+
dependsOn: ['01-default'],
|
|
138
|
+
load() {},
|
|
139
|
+
cleanup() {},
|
|
140
|
+
}
|
|
141
|
+
`,
|
|
142
|
+
});
|
|
143
|
+
// 自依赖应被排序器拦下:证明默认导出的 dependsOn 确实被读到了
|
|
144
|
+
await expect(discoverTestDataModules(dir)).rejects.toThrow(/依赖了自己/);
|
|
145
|
+
});
|
|
146
|
+
it('默认导出优先于同名命名导出(不留「改了默认导出、旧导出还生效」的歧义)', async () => {
|
|
147
|
+
const dir = await makeDirectory({
|
|
148
|
+
'01-both.mjs': `
|
|
149
|
+
export default { load() { globalThis.__orderBoth.push('load:default') }, cleanup() {} }
|
|
150
|
+
export function load() { globalThis.__orderBoth.push('load:named') }
|
|
151
|
+
export function cleanup() {}
|
|
152
|
+
`,
|
|
153
|
+
});
|
|
154
|
+
const order = globalThis.__orderBoth = [];
|
|
155
|
+
const units = await discoverTestDataModules(dir);
|
|
156
|
+
await loadTestData({ ctx: makeCtx('test'), units });
|
|
157
|
+
expect(order).toEqual(['load:default']);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
describe('defineTestData', () => {
|
|
161
|
+
it('原样返回 load / cleanup,并保留 dependsOn', () => {
|
|
162
|
+
const load = () => { };
|
|
163
|
+
const cleanup = () => { };
|
|
164
|
+
const script = defineTestData({ dependsOn: ['01-database'], load, cleanup });
|
|
165
|
+
expect(script.load).toBe(load);
|
|
166
|
+
expect(script.cleanup).toBe(cleanup);
|
|
167
|
+
expect(script.dependsOn).toEqual(['01-database']);
|
|
168
|
+
});
|
|
169
|
+
it('没有 dependsOn 时结果里没有这个字段', () => {
|
|
170
|
+
expect(defineTestData({ load: () => { } })).not.toHaveProperty('dependsOn');
|
|
171
|
+
});
|
|
172
|
+
it('缺 load 直接抛错(定义期就发现,而不是装载期)', () => {
|
|
173
|
+
expect(() => defineTestData({ cleanup: () => { } })).toThrow(/缺少 load/);
|
|
174
|
+
});
|
|
175
|
+
it('不写 cleanup 是合法的(库里那半由资源单元划掉)', () => {
|
|
176
|
+
const script = defineTestData({ load: () => { } });
|
|
177
|
+
expect(script.cleanup).toBeUndefined();
|
|
178
|
+
});
|
|
179
|
+
it('cleanup 写了但不是函数则抛错(写错了不要静默跣过)', () => {
|
|
180
|
+
expect(() => defineTestData({ load: () => { }, cleanup: 'yes' })).toThrow(/cleanup 必须是函数/);
|
|
181
|
+
});
|
|
182
|
+
it('dependsOn 不是字符串数组直接抛错', () => {
|
|
183
|
+
expect(() => defineTestData({ dependsOn: [1], load: () => { }, cleanup: () => { } })).toThrow(/非空字符串数组/);
|
|
184
|
+
});
|
|
185
|
+
it('dependsOn 含空字符串直接抛错(空名指不到任何脚本)', () => {
|
|
186
|
+
expect(() => defineTestData({ dependsOn: [''], load: () => { }, cleanup: () => { } })).toThrow(/非空字符串数组/);
|
|
187
|
+
});
|
|
188
|
+
it('dependsOn 重复项直接抛错(重复声明没有意义,多半是写错)', () => {
|
|
189
|
+
expect(() => defineTestData({ dependsOn: ['01-a', '01-a'], load: () => { }, cleanup: () => { } })).toThrow(/重复项/);
|
|
190
|
+
});
|
|
191
|
+
it('结果冻结:外部改不动 load / cleanup / dependsOn', () => {
|
|
192
|
+
const script = defineTestData({ dependsOn: ['01-a'], load: () => { }, cleanup: () => { } });
|
|
193
|
+
expect(Object.isFrozen(script)).toBe(true);
|
|
194
|
+
expect(Object.isFrozen(script.dependsOn)).toBe(true);
|
|
195
|
+
});
|
|
196
|
+
it('泛型写 typeof server 时 ctx 带上全部槽位的类型', async () => {
|
|
197
|
+
const calls = [];
|
|
198
|
+
const script = defineTestData({
|
|
199
|
+
load(ctx) {
|
|
200
|
+
// 五个字段都有类型:写错(如 services.user.remove)会在 tsc 阶段报错
|
|
201
|
+
calls.push(ctx.resources.database.marker, String(ctx.config.feature), ctx.env.FOO ?? 'none');
|
|
202
|
+
ctx.clients.webhook.send();
|
|
203
|
+
ctx.services.user.create();
|
|
204
|
+
},
|
|
205
|
+
cleanup() { },
|
|
206
|
+
});
|
|
207
|
+
// 类型正确还不够:真跑一遍,确认运行时拿到的就是这些字段
|
|
208
|
+
await script.load({
|
|
209
|
+
env: { ENV: 'test' },
|
|
210
|
+
resources: { database: { marker: 'db' } },
|
|
211
|
+
config: { feature: true },
|
|
212
|
+
clients: { webhook: { send: () => { calls.push('webhook'); } } },
|
|
213
|
+
services: { user: { create: () => { calls.push('user'); } } },
|
|
214
|
+
log: { debug: () => { }, info: () => { }, warn: () => { }, error: () => { } },
|
|
215
|
+
});
|
|
216
|
+
expect(calls).toEqual(['db', 'true', 'none', 'webhook', 'user']);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
/** 造一个最小 ctx(`resources` 用不着时给空对象;`log` 静默) */
|
|
220
|
+
function makeCtx(env, resources = {}) {
|
|
221
|
+
return {
|
|
222
|
+
env: { ENV: env },
|
|
223
|
+
resources: resources,
|
|
224
|
+
config: {},
|
|
225
|
+
clients: {},
|
|
226
|
+
services: {},
|
|
227
|
+
log: { debug: () => { }, info: () => { }, warn: () => { }, error: () => { } },
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
/** 造一批内存脚本单元(不经磁盘,专测执行语义) */
|
|
231
|
+
function unitsOf(order, names) {
|
|
232
|
+
return names.map((name) => ({
|
|
233
|
+
name,
|
|
234
|
+
load: () => {
|
|
235
|
+
order.push(`load:${name}`);
|
|
236
|
+
},
|
|
237
|
+
cleanup: () => {
|
|
238
|
+
order.push(`cleanup:${name}`);
|
|
239
|
+
},
|
|
240
|
+
}));
|
|
241
|
+
}
|
|
242
|
+
describe('testdata 执行语义', () => {
|
|
243
|
+
it('load 正序、cleanup 逆序', async () => {
|
|
244
|
+
const order = [];
|
|
245
|
+
const units = unitsOf(order, ['a', 'b', 'c']);
|
|
246
|
+
await loadTestData({ ctx: makeCtx('test'), units });
|
|
247
|
+
expect(order).toEqual(['load:a', 'load:b', 'load:c']);
|
|
248
|
+
order.length = 0;
|
|
249
|
+
await cleanupTestData({ ctx: makeCtx('test'), units });
|
|
250
|
+
expect(order).toEqual(['cleanup:c', 'cleanup:b', 'cleanup:a']);
|
|
251
|
+
});
|
|
252
|
+
it('先整体 cleanup(逆序)再整体 load(正序)就是一次干净基线', async () => {
|
|
253
|
+
const order = [];
|
|
254
|
+
const units = unitsOf(order, ['a', 'b']);
|
|
255
|
+
// `TestServer.reprovision()` 就是这两步的顺序:先拆干净,再重建
|
|
256
|
+
await cleanupTestData({ ctx: makeCtx('test'), units });
|
|
257
|
+
await loadTestData({ ctx: makeCtx('test'), units });
|
|
258
|
+
// 分两趟而不是「逐脚本 cleanup + load」:后者会让前一个脚本刚插入的数据
|
|
259
|
+
// 被后一个脚本的 cleanup 顺手删掉(顺序依赖)。
|
|
260
|
+
expect(order).toEqual(['cleanup:b', 'cleanup:a', 'load:a', 'load:b']);
|
|
261
|
+
});
|
|
262
|
+
it('每个脚本收到的就是同一个 ctx(env / resources / log 都是装配产物)', async () => {
|
|
263
|
+
const seen = [];
|
|
264
|
+
const resources = { database: { marker: 'live-db' } };
|
|
265
|
+
const ctx = makeCtx('test', resources);
|
|
266
|
+
const units = [
|
|
267
|
+
{ name: 'a', load: (received) => { seen.push(received); }, cleanup: () => { } },
|
|
268
|
+
];
|
|
269
|
+
await loadTestData({ ctx, units });
|
|
270
|
+
// 同一个对象:脚本里 `ctx.resources.database` 就是被测服务用的那个实例,不是副本
|
|
271
|
+
expect(seen[0]).toBe(ctx);
|
|
272
|
+
expect(seen[0].resources.database).toBe(resources.database);
|
|
273
|
+
});
|
|
274
|
+
it('执行器自己按 dependsOn 定序,不信任调用方传入的顺序', async () => {
|
|
275
|
+
const order = [];
|
|
276
|
+
const units = [
|
|
277
|
+
{ name: '01-late', dependsOn: ['99-early'], load: () => { order.push('load:late'); }, cleanup: () => { order.push('cleanup:late'); } },
|
|
278
|
+
{ name: '99-early', load: () => { order.push('load:early'); }, cleanup: () => { order.push('cleanup:early'); } },
|
|
279
|
+
];
|
|
280
|
+
await loadTestData({ ctx: makeCtx('test'), units });
|
|
281
|
+
expect(order).toEqual(['load:early', 'load:late']);
|
|
282
|
+
order.length = 0;
|
|
283
|
+
await cleanupTestData({ ctx: makeCtx('test'), units });
|
|
284
|
+
expect(order).toEqual(['cleanup:late', 'cleanup:early']);
|
|
285
|
+
});
|
|
286
|
+
it('没有脚本时两个入口都是空操作(但闸门照样生效)', async () => {
|
|
287
|
+
await expect(loadTestData({ ctx: makeCtx('test') })).resolves.toBeUndefined();
|
|
288
|
+
await expect(cleanupTestData({ ctx: makeCtx('development') })).resolves.toBeUndefined();
|
|
289
|
+
});
|
|
290
|
+
it.each([
|
|
291
|
+
['loadTestData', loadTestData],
|
|
292
|
+
['cleanupTestData', cleanupTestData],
|
|
293
|
+
])('%s:脚本失败直接冒泡(不降级成「跳过这条」)', async (_name, run) => {
|
|
294
|
+
const order = [];
|
|
295
|
+
const units = [
|
|
296
|
+
{ name: 'ok', load: () => { order.push('load:ok'); }, cleanup: () => { order.push('cleanup:ok'); } },
|
|
297
|
+
{ name: 'broken', load: () => { throw new Error('insert failed'); }, cleanup: () => { throw new Error('delete failed'); } },
|
|
298
|
+
];
|
|
299
|
+
await expect(run({ ctx: makeCtx('test'), units })).rejects.toThrow(/failed/);
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
describe('testdata 的环境闸门', () => {
|
|
303
|
+
const units = (order) => unitsOf(order, ['a']);
|
|
304
|
+
it.each([
|
|
305
|
+
['loadTestData', loadTestData],
|
|
306
|
+
['cleanupTestData', cleanupTestData],
|
|
307
|
+
])('%s:非可清理环境一个脚本都不跑', async (_name, run) => {
|
|
308
|
+
const order = [];
|
|
309
|
+
await expect(run({ ctx: makeCtx('production'), units: units(order) })).rejects.toThrow(/拒绝在 ENV=/);
|
|
310
|
+
expect(order).toEqual([]);
|
|
311
|
+
});
|
|
312
|
+
it.each(['production', 'staging', ''])('ENV=%s 时拒绝,报错带上当前值', async (env) => {
|
|
313
|
+
await expect(loadTestData({ ctx: makeCtx(env) })).rejects.toThrow(/ENV="/);
|
|
314
|
+
});
|
|
315
|
+
it.each(['development', 'test'])('ENV=%s 时放行', async (env) => {
|
|
316
|
+
const order = [];
|
|
317
|
+
await loadTestData({ ctx: makeCtx(env), units: units(order) });
|
|
318
|
+
expect(order).toEqual(['load:a']);
|
|
319
|
+
});
|
|
320
|
+
it('闸门先于一切:没有脚本也要在非可清理环境下失败', async () => {
|
|
321
|
+
await expect(cleanupTestData({ ctx: makeCtx('production') })).rejects.toThrow(/ENV="production"/);
|
|
322
|
+
});
|
|
323
|
+
it('判据是 ctx.env 快照,不是 process.env(事后改写不影响结论)', async () => {
|
|
324
|
+
const order = [];
|
|
325
|
+
process.env.ENV = 'production';
|
|
326
|
+
await loadTestData({ ctx: makeCtx('test'), units: units(order) });
|
|
327
|
+
expect(order).toEqual(['load:a']);
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
describe('resolveTestDataDirectory', () => {
|
|
331
|
+
it('默认是 <cwd>/testdata', () => {
|
|
332
|
+
expect(resolveTestDataDirectory()).toBe(join(process.cwd(), 'testdata'));
|
|
333
|
+
});
|
|
334
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 框架统一提供的类型增强(设计 §8「类型增强」红线)。
|
|
3
|
+
*
|
|
4
|
+
* `request.user` / `request.token` / `request.id` 与 `routeOptions.config.accessControls`(以及
|
|
5
|
+
* 限流规则 `rateLimit`)的 `declare module 'fastify'` 由 common 统一提供,各仓删除自建声明。
|
|
6
|
+
*/
|
|
7
|
+
import type { RequestContext } from './modules/context/index.js';
|
|
8
|
+
/** 路由级访问控制声明(各服务在 `routeOptions.config.accessControls` 里写) */
|
|
9
|
+
export type AccessControl = 'NoLoginRequired' | 'IgnorePermission' | 'RequiredSecurityCode';
|
|
10
|
+
/** 路由级限流规则(`createRateLimitMiddleware` 消费,另有路由级 `config.rateLimit`) */
|
|
11
|
+
export interface RateLimitRule {
|
|
12
|
+
limit: number;
|
|
13
|
+
/** 窗口大小(秒) */
|
|
14
|
+
window: number;
|
|
15
|
+
/** 限流键维度,默认 `['ip']` */
|
|
16
|
+
keys?: string[];
|
|
17
|
+
}
|
|
18
|
+
declare module 'fastify' {
|
|
19
|
+
interface FastifyRequest {
|
|
20
|
+
/** 认证后由 `auth` 中间件填充 */
|
|
21
|
+
user?: unknown;
|
|
22
|
+
/** 认证后由 `auth` 中间件填充 */
|
|
23
|
+
token?: unknown;
|
|
24
|
+
/** 由 core 固定注入的请求级上下文(`registerRequestContext`) */
|
|
25
|
+
ctx?: RequestContext;
|
|
26
|
+
}
|
|
27
|
+
interface FastifyContextConfig {
|
|
28
|
+
/** 路由级访问控制(`NoLoginRequired` / `IgnorePermission` / `RequiredSecurityCode`) */
|
|
29
|
+
accessControls?: AccessControl[];
|
|
30
|
+
/** 路由级限流规则 */
|
|
31
|
+
rateLimit?: RateLimitRule;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=type-augment.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"type-augment.d.ts","sourceRoot":"","sources":["../../source/core/type-augment.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAA;AAEhE,8DAA8D;AAC9D,MAAM,MAAM,aAAa,GAAG,iBAAiB,GAAG,kBAAkB,GAAG,sBAAsB,CAAA;AAE3F,uEAAuE;AACvE,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,cAAc;IACd,MAAM,EAAE,MAAM,CAAA;IACd,wBAAwB;IACxB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;CAChB;AAED,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,cAAc;QACtB,wBAAwB;QACxB,IAAI,CAAC,EAAE,OAAO,CAAA;QACd,wBAAwB;QACxB,KAAK,CAAC,EAAE,OAAO,CAAA;QACf,mDAAmD;QACnD,GAAG,CAAC,EAAE,cAAc,CAAA;KACrB;IAED,UAAU,oBAAoB;QAC5B,+EAA+E;QAC/E,cAAc,CAAC,EAAE,aAAa,EAAE,CAAA;QAChC,cAAc;QACd,SAAS,CAAC,EAAE,aAAa,CAAA;KAC1B;CACF"}
|