@lobehub/chat 1.96.17 → 1.96.18
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/.cursor/rules/backend-architecture.mdc +54 -51
- package/.cursor/rules/code-review.mdc +69 -0
- package/.cursor/rules/cursor-rules.mdc +28 -0
- package/.cursor/rules/cursor-ux.mdc +94 -0
- package/.cursor/rules/debug.mdc +193 -0
- package/.cursor/rules/i18n/i18n.mdc +66 -68
- package/.cursor/rules/packages/react-layout-kit.mdc +19 -26
- package/.cursor/rules/react-component.mdc +91 -6
- package/.cursor/rules/rules-attach.mdc +76 -0
- package/.cursor/rules/system-role.mdc +2 -6
- package/.cursor/rules/testing-guide.mdc +881 -0
- package/.cursor/rules/typescript.mdc +19 -0
- package/.cursor/rules/zustand-action-patterns.mdc +57 -15
- package/.cursor/rules/zustand-slice-organization.mdc +12 -12
- package/CHANGELOG.md +33 -0
- package/changelog/v1.json +12 -0
- package/package.json +1 -1
- package/src/app/[variants]/(main)/files/(content)/NotSupportClient.tsx +8 -1
- package/src/config/modelProviders/github.ts +1 -1
- package/.cursor/rules/cursor-ux-optimize.mdc +0 -27
- package/.cursor/rules/packages/lobe-ui.mdc +0 -72
@@ -0,0 +1,881 @@
|
|
1
|
+
---
|
2
|
+
description:
|
3
|
+
globs: *.test.ts,*.test.tsx
|
4
|
+
alwaysApply: false
|
5
|
+
---
|
6
|
+
---
|
7
|
+
type: agent-requested
|
8
|
+
title: 测试指南 - LobeChat Testing Guide
|
9
|
+
description: LobeChat 项目的 Vitest 测试环境配置、运行方式、修复原则指南
|
10
|
+
---
|
11
|
+
|
12
|
+
# 测试指南 - LobeChat Testing Guide
|
13
|
+
|
14
|
+
## 🧪 测试环境概览
|
15
|
+
|
16
|
+
LobeChat 项目使用 Vitest 测试库,配置了两种不同的测试环境:
|
17
|
+
|
18
|
+
### 客户端测试环境 (DOM Environment)
|
19
|
+
|
20
|
+
- **配置文件**: [vitest.config.ts](mdc:vitest.config.ts)
|
21
|
+
- **环境**: Happy DOM (浏览器环境模拟)
|
22
|
+
- **数据库**: PGLite (浏览器环境的 PostgreSQL)
|
23
|
+
- **用途**: 测试前端组件、客户端逻辑、React 组件等
|
24
|
+
- **设置文件**: [tests/setup.ts](mdc:tests/setup.ts)
|
25
|
+
|
26
|
+
### 服务端测试环境 (Node Environment)
|
27
|
+
|
28
|
+
- **配置文件**: [vitest.config.server.ts](mdc:vitest.config.server.ts)
|
29
|
+
- **环境**: Node.js
|
30
|
+
- **数据库**: 真实的 PostgreSQL 数据库
|
31
|
+
- **并发限制**: 单线程运行 (`singleFork: true`)
|
32
|
+
- **用途**: 测试数据库模型、服务端逻辑、API 端点等
|
33
|
+
- **设置文件**: [tests/setup-db.ts](mdc:tests/setup-db.ts)
|
34
|
+
|
35
|
+
## 🚀 测试运行命令
|
36
|
+
|
37
|
+
### package.json 脚本说明
|
38
|
+
|
39
|
+
查看 [package.json](mdc:package.json) 中的测试相关脚本:
|
40
|
+
|
41
|
+
```json
|
42
|
+
{
|
43
|
+
"test": "npm run test-app && npm run test-server",
|
44
|
+
"test-app": "vitest run --config vitest.config.ts",
|
45
|
+
"test-app:coverage": "vitest run --config vitest.config.ts --coverage",
|
46
|
+
"test-server": "vitest run --config vitest.config.server.ts",
|
47
|
+
"test-server:coverage": "vitest run --config vitest.config.server.ts --coverage"
|
48
|
+
}
|
49
|
+
```
|
50
|
+
|
51
|
+
### 推荐的测试运行方式
|
52
|
+
|
53
|
+
#### ✅ 正确的命令格式
|
54
|
+
|
55
|
+
```bash
|
56
|
+
# 运行所有客户端测试
|
57
|
+
npx vitest run --config vitest.config.ts
|
58
|
+
|
59
|
+
# 运行所有服务端测试
|
60
|
+
npx vitest run --config vitest.config.server.ts
|
61
|
+
|
62
|
+
# 运行特定测试文件 (支持模糊匹配)
|
63
|
+
npx vitest run --config vitest.config.ts basic
|
64
|
+
npx vitest run --config vitest.config.ts user.test.ts
|
65
|
+
|
66
|
+
# 运行特定文件的特定行号
|
67
|
+
npx vitest run --config vitest.config.ts src/utils/helper.test.ts:25
|
68
|
+
npx vitest run --config vitest.config.ts basic/foo.test.ts:10,basic/foo.test.ts:25
|
69
|
+
|
70
|
+
# 过滤特定测试用例名称
|
71
|
+
npx vitest -t "test case name" --config vitest.config.ts
|
72
|
+
|
73
|
+
# 组合使用文件和测试名称过滤
|
74
|
+
npx vitest run --config vitest.config.ts filename.test.ts -t "specific test"
|
75
|
+
```
|
76
|
+
|
77
|
+
#### ❌ 避免的命令格式
|
78
|
+
|
79
|
+
```bash
|
80
|
+
# ❌ 不要使用 pnpm test xxx (这不是有效的 vitest 命令)
|
81
|
+
pnpm test some-file
|
82
|
+
|
83
|
+
# ❌ 不要使用裸 vitest (会进入 watch 模式)
|
84
|
+
vitest test-file.test.ts
|
85
|
+
|
86
|
+
# ❌ 不要混淆测试环境
|
87
|
+
npx vitest run --config vitest.config.server.ts client-component.test.ts
|
88
|
+
```
|
89
|
+
|
90
|
+
### 关键运行参数说明
|
91
|
+
|
92
|
+
- **`vitest run`**: 运行一次测试然后退出 (避免 watch 模式)
|
93
|
+
- **`vitest`**: 默认进入 watch 模式,持续监听文件变化
|
94
|
+
- **`--config`**: 指定配置文件,选择正确的测试环境
|
95
|
+
- **`-t`**: 过滤测试用例名称,支持正则表达式
|
96
|
+
- **`--coverage`**: 生成测试覆盖率报告
|
97
|
+
|
98
|
+
## 🔧 测试修复原则
|
99
|
+
|
100
|
+
### 核心原则 ⚠️
|
101
|
+
|
102
|
+
1. **充分阅读测试代码**: 在修复测试之前,必须完整理解测试的意图和实现
|
103
|
+
2. **测试优先修复**: 如果是测试本身写错了,修改测试而不是实现代码
|
104
|
+
3. **专注单一问题**: 只修复指定的测试,不要添加额外测试或功能
|
105
|
+
4. **不自作主张**: 不要因为发现其他问题就直接修改,先提出再讨论
|
106
|
+
|
107
|
+
### 测试修复流程
|
108
|
+
|
109
|
+
```mermaid
|
110
|
+
flowchart TD
|
111
|
+
subgraph "阶段一:分析与复现"
|
112
|
+
A[开始:收到测试失败报告] --> B[定位并运行失败的测试];
|
113
|
+
B --> C{是否能在本地复现?};
|
114
|
+
C -->|否| D[检查测试环境/配置/依赖];
|
115
|
+
C -->|是| E[分析:阅读测试代码、错误日志、Git 历史];
|
116
|
+
end
|
117
|
+
|
118
|
+
subgraph "阶段二:诊断与调试"
|
119
|
+
E --> F[建立假设:问题出在测试、代码还是环境?];
|
120
|
+
F --> G["调试:使用 console.log 或 debugger 深入检查"];
|
121
|
+
G --> H{假设是否被证实?};
|
122
|
+
H -->|否, 重新假设| F;
|
123
|
+
end
|
124
|
+
|
125
|
+
subgraph "阶段三:修复与验证"
|
126
|
+
H -->|是| I{确定根本原因};
|
127
|
+
I -->|测试逻辑错误| J[修复测试代码];
|
128
|
+
I -->|实现代码 Bug| K[修复实现代码];
|
129
|
+
I -->|环境/配置问题| L[修复配置或依赖];
|
130
|
+
J --> M[验证修复:重新运行失败的测试];
|
131
|
+
K --> M;
|
132
|
+
L --> M;
|
133
|
+
M --> N{测试是否通过?};
|
134
|
+
N -->|否, 修复无效| F;
|
135
|
+
N -->|是| O[扩大验证:运行当前文件内所有测试];
|
136
|
+
O --> P{是否全部通过?};
|
137
|
+
P -->|否, 引入新问题| F;
|
138
|
+
end
|
139
|
+
|
140
|
+
subgraph "阶段四:总结"
|
141
|
+
P -->|是| Q[完成:撰写修复总结];
|
142
|
+
end
|
143
|
+
|
144
|
+
D --> F;
|
145
|
+
```
|
146
|
+
|
147
|
+
### 修复完成后的总结
|
148
|
+
|
149
|
+
测试修复完成后,应该提供简要说明,包括:
|
150
|
+
|
151
|
+
1. **错误原因分析**: 说明测试失败的根本原因
|
152
|
+
- 测试逻辑错误
|
153
|
+
- 实现代码bug
|
154
|
+
- 环境配置问题
|
155
|
+
- 依赖变更导致的问题
|
156
|
+
|
157
|
+
2. **修复方法说明**: 简述采用的修复方式
|
158
|
+
- 修改了哪些文件
|
159
|
+
- 采用了什么解决方案
|
160
|
+
- 为什么选择这种修复方式
|
161
|
+
|
162
|
+
**示例格式**:
|
163
|
+
|
164
|
+
```markdown
|
165
|
+
## 测试修复总结
|
166
|
+
|
167
|
+
**错误原因**: 测试中的 mock 数据格式与实际 API 返回格式不匹配,导致断言失败。
|
168
|
+
|
169
|
+
**修复方法**: 更新了测试文件中的 mock 数据结构,使其与最新的 API 响应格式保持一致。具体修改了 `user.test.ts` 中的 `mockUserData` 对象结构。
|
170
|
+
```
|
171
|
+
|
172
|
+
## 📂 测试文件组织
|
173
|
+
|
174
|
+
### 文件命名约定
|
175
|
+
|
176
|
+
- **客户端测试**: `*.test.ts`, `*.test.tsx` (任意位置)
|
177
|
+
- **服务端测试**: `src/database/models/**/*.test.ts`, `src/database/server/**/*.test.ts` (限定路径)
|
178
|
+
|
179
|
+
### 测试文件组织风格
|
180
|
+
|
181
|
+
项目采用 **测试文件与源文件同目录** 的组织风格:
|
182
|
+
|
183
|
+
- 测试文件放在对应源文件的同一目录下
|
184
|
+
- 命名格式:`原文件名.test.ts` 或 `原文件名.test.tsx`
|
185
|
+
|
186
|
+
例如:
|
187
|
+
|
188
|
+
```
|
189
|
+
src/components/Button/
|
190
|
+
├── index.tsx # 源文件
|
191
|
+
└── index.test.tsx # 测试文件
|
192
|
+
```
|
193
|
+
|
194
|
+
## 🛠️ 测试调试技巧
|
195
|
+
|
196
|
+
### 运行失败测试的步骤
|
197
|
+
|
198
|
+
1. **确定测试类型**: 查看文件路径确定使用哪个配置
|
199
|
+
2. **运行单个测试**: 使用 `-t` 参数隔离问题
|
200
|
+
3. **检查错误日志**: 仔细阅读错误信息和堆栈跟踪
|
201
|
+
4. **查看最近修改记录**: 检查相关文件的最近变更情况
|
202
|
+
5. **添加调试日志**: 在测试中添加 `console.log` 了解执行流程
|
203
|
+
|
204
|
+
### Electron IPC 接口测试策略 🖥️
|
205
|
+
|
206
|
+
对于涉及 Electron IPC 接口的测试,由于提供真实的 Electron 环境比较复杂,采用 **Mock 返回值** 的方式进行测试。
|
207
|
+
|
208
|
+
#### 基本 Mock 设置
|
209
|
+
|
210
|
+
```typescript
|
211
|
+
import { vi } from "vitest";
|
212
|
+
import { electronIpcClient } from "@/server/modules/ElectronIPCClient";
|
213
|
+
|
214
|
+
// Mock Electron IPC 客户端
|
215
|
+
vi.mock("@/server/modules/ElectronIPCClient", () => ({
|
216
|
+
electronIpcClient: {
|
217
|
+
getFilePathById: vi.fn(),
|
218
|
+
deleteFiles: vi.fn(),
|
219
|
+
// 根据需要添加其他 IPC 方法
|
220
|
+
},
|
221
|
+
}));
|
222
|
+
```
|
223
|
+
|
224
|
+
#### 在测试中设置 Mock 行为
|
225
|
+
|
226
|
+
```typescript
|
227
|
+
beforeEach(() => {
|
228
|
+
// 重置所有 Mock
|
229
|
+
vi.resetAllMocks();
|
230
|
+
|
231
|
+
// 设置默认的 Mock 返回值
|
232
|
+
vi.mocked(electronIpcClient.getFilePathById).mockResolvedValue(
|
233
|
+
"/path/to/file.txt"
|
234
|
+
);
|
235
|
+
vi.mocked(electronIpcClient.deleteFiles).mockResolvedValue({
|
236
|
+
success: true,
|
237
|
+
});
|
238
|
+
});
|
239
|
+
```
|
240
|
+
|
241
|
+
#### 测试不同场景的示例
|
242
|
+
|
243
|
+
```typescript
|
244
|
+
it("应该处理文件删除成功的情况", async () => {
|
245
|
+
// 设置成功场景的 Mock
|
246
|
+
vi.mocked(electronIpcClient.deleteFiles).mockResolvedValue({
|
247
|
+
success: true,
|
248
|
+
});
|
249
|
+
|
250
|
+
const result = await service.deleteFiles(["desktop://file1.txt"]);
|
251
|
+
|
252
|
+
expect(electronIpcClient.deleteFiles).toHaveBeenCalledWith([
|
253
|
+
"desktop://file1.txt",
|
254
|
+
]);
|
255
|
+
expect(result.success).toBe(true);
|
256
|
+
});
|
257
|
+
|
258
|
+
it("应该处理文件删除失败的情况", async () => {
|
259
|
+
// 设置失败场景的 Mock
|
260
|
+
vi.mocked(electronIpcClient.deleteFiles).mockRejectedValue(
|
261
|
+
new Error("删除失败")
|
262
|
+
);
|
263
|
+
|
264
|
+
const result = await service.deleteFiles(["desktop://file1.txt"]);
|
265
|
+
|
266
|
+
expect(result.success).toBe(false);
|
267
|
+
expect(result.errors).toBeDefined();
|
268
|
+
});
|
269
|
+
```
|
270
|
+
|
271
|
+
#### Mock 策略的优势
|
272
|
+
|
273
|
+
1. **环境简化**: 避免了复杂的 Electron 环境搭建
|
274
|
+
2. **测试可控**: 可以精确控制 IPC 调用的返回值和行为
|
275
|
+
3. **场景覆盖**: 容易测试各种成功/失败场景
|
276
|
+
4. **执行速度**: Mock 调用比真实 IPC 调用更快
|
277
|
+
|
278
|
+
#### 注意事项
|
279
|
+
|
280
|
+
- **Mock 准确性**: 确保 Mock 的行为与真实 IPC 接口行为一致
|
281
|
+
- **类型安全**: 使用 `vi.mocked()` 确保类型安全
|
282
|
+
- **Mock 重置**: 在 `beforeEach` 中重置 Mock 状态,避免测试间干扰
|
283
|
+
- **调用验证**: 不仅要验证返回值,还要验证 IPC 方法是否被正确调用
|
284
|
+
|
285
|
+
### 检查最近修改记录 🔍
|
286
|
+
|
287
|
+
为了更好地判断测试失败的根本原因,需要**系统性地检查相关文件的修改历史**。这是问题定位的关键步骤。
|
288
|
+
|
289
|
+
#### 第一步:确定需要检查的文件范围
|
290
|
+
|
291
|
+
1. **测试文件本身**: `path/to/component.test.ts`
|
292
|
+
2. **对应的实现文件**: `path/to/component.ts` 或 `path/to/component/index.ts`
|
293
|
+
3. **相关依赖文件**: 测试或实现中导入的其他模块
|
294
|
+
|
295
|
+
#### 第二步:检查当前工作目录状态
|
296
|
+
|
297
|
+
```bash
|
298
|
+
# 查看所有未提交的修改状态
|
299
|
+
git status
|
300
|
+
|
301
|
+
# 重点关注测试文件和实现文件是否有未提交的修改
|
302
|
+
git status | grep -E "(test|spec)"
|
303
|
+
```
|
304
|
+
|
305
|
+
#### 第三步:检查未提交的修改内容
|
306
|
+
|
307
|
+
```bash
|
308
|
+
# 查看测试文件的未提交修改 (工作区 vs 暂存区)
|
309
|
+
git diff path/to/component.test.ts | cat
|
310
|
+
|
311
|
+
# 查看对应实现文件的未提交修改
|
312
|
+
git diff path/to/component.ts | cat
|
313
|
+
|
314
|
+
# 查看已暂存但未提交的修改
|
315
|
+
git diff --cached path/to/component.test.ts | cat
|
316
|
+
git diff --cached path/to/component.ts | cat
|
317
|
+
```
|
318
|
+
|
319
|
+
#### 第四步:检查提交历史和时间相关性
|
320
|
+
|
321
|
+
**首先查看提交时间,判断修改的时效性**:
|
322
|
+
|
323
|
+
```bash
|
324
|
+
# 查看测试文件的最近提交历史,包含提交时间
|
325
|
+
git log --pretty=format:"%h %ad %s" --date=relative -5 path/to/component.test.ts | cat
|
326
|
+
|
327
|
+
# 查看实现文件的最近提交历史,包含提交时间
|
328
|
+
git log --pretty=format:"%h %ad %s" --date=relative -5 path/to/component.ts | cat
|
329
|
+
|
330
|
+
# 查看详细的提交时间(ISO格式,便于精确判断)
|
331
|
+
git log --pretty=format:"%h %ad %an %s" --date=iso -3 path/to/component.ts | cat
|
332
|
+
git log --pretty=format:"%h %ad %an %s" --date=iso -3 path/to/component.test.ts | cat
|
333
|
+
```
|
334
|
+
|
335
|
+
**判断提交的参考价值**:
|
336
|
+
|
337
|
+
1. **最近提交(24小时内)**: 🔴 **高度相关** - 很可能是导致测试失败的直接原因
|
338
|
+
2. **近期提交(1-7天内)**: 🟡 **中等相关** - 可能相关,需要仔细分析修改内容
|
339
|
+
3. **较早提交(超过1周)**: ⚪ **低相关性** - 除非是重大重构,否则不太可能是直接原因
|
340
|
+
|
341
|
+
#### 第五步:基于时间相关性查看具体修改内容
|
342
|
+
|
343
|
+
**根据提交时间的远近,优先查看最近的修改**:
|
344
|
+
|
345
|
+
```bash
|
346
|
+
# 如果有24小时内的提交,重点查看这些修改
|
347
|
+
git show HEAD -- path/to/component.test.ts | cat
|
348
|
+
git show HEAD -- path/to/component.ts | cat
|
349
|
+
|
350
|
+
# 查看次新的提交(如果最新提交时间较远)
|
351
|
+
git show HEAD~1 -- path/to/component.ts | cat
|
352
|
+
git show <recent-commit-hash> -- path/to/component.ts | cat
|
353
|
+
|
354
|
+
# 对比最近两次提交的差异
|
355
|
+
git diff HEAD~1 HEAD -- path/to/component.ts | cat
|
356
|
+
```
|
357
|
+
|
358
|
+
#### 第六步:分析修改与测试失败的关系
|
359
|
+
|
360
|
+
基于修改记录和时间相关性判断:
|
361
|
+
|
362
|
+
1. **最近修改了实现代码**:
|
363
|
+
|
364
|
+
```bash
|
365
|
+
# 重点检查实现逻辑的变化
|
366
|
+
git diff HEAD~1 path/to/component.ts | cat
|
367
|
+
```
|
368
|
+
|
369
|
+
- 很可能是实现代码的变更导致测试失败
|
370
|
+
- 检查实现逻辑是否正确
|
371
|
+
- 确认测试是否需要相应更新
|
372
|
+
|
373
|
+
2. **最近修改了测试代码**:
|
374
|
+
|
375
|
+
```bash
|
376
|
+
# 重点检查测试逻辑的变化
|
377
|
+
git diff HEAD~1 path/to/component.test.ts | cat
|
378
|
+
```
|
379
|
+
|
380
|
+
- 可能是测试本身写错了
|
381
|
+
- 检查测试逻辑和断言是否正确
|
382
|
+
- 确认测试是否符合实现的预期行为
|
383
|
+
|
384
|
+
3. **两者都有最近修改**:
|
385
|
+
|
386
|
+
```bash
|
387
|
+
# 对比两个文件的修改时间
|
388
|
+
git log --pretty=format:"%ad %f" --date=iso -1 path/to/component.ts | cat
|
389
|
+
git log --pretty=format:"%ad %f" --date=iso -1 path/to/component.test.ts | cat
|
390
|
+
```
|
391
|
+
|
392
|
+
- 需要综合分析两者的修改
|
393
|
+
- 确定哪个修改更可能导致问题
|
394
|
+
- 优先检查时间更近的修改
|
395
|
+
|
396
|
+
4. **都没有最近修改**:
|
397
|
+
- 可能是依赖变更或环境问题
|
398
|
+
- 检查 `package.json`、配置文件等的修改
|
399
|
+
- 查看是否有全局性的代码重构
|
400
|
+
|
401
|
+
#### 修改记录检查示例
|
402
|
+
|
403
|
+
```bash
|
404
|
+
# 完整的检查流程示例
|
405
|
+
echo "=== 检查文件修改状态 ==="
|
406
|
+
git status | grep component
|
407
|
+
|
408
|
+
echo "=== 检查未提交修改 ==="
|
409
|
+
git diff src/components/Button/index.test.tsx | cat
|
410
|
+
git diff src/components/Button/index.tsx | cat
|
411
|
+
|
412
|
+
echo "=== 检查提交历史和时间 ==="
|
413
|
+
git log --pretty=format:"%h %ad %s" --date=relative -3 src/components/Button/index.test.tsx | cat
|
414
|
+
git log --pretty=format:"%h %ad %s" --date=relative -3 src/components/Button/index.tsx | cat
|
415
|
+
|
416
|
+
echo "=== 根据时间优先级查看修改内容 ==="
|
417
|
+
# 如果有24小时内的提交,重点查看
|
418
|
+
git show HEAD -- src/components/Button/index.tsx | cat
|
419
|
+
```
|
420
|
+
|
421
|
+
## 🗃️ 数据库 Model 测试指南
|
422
|
+
|
423
|
+
### 测试环境选择 💡
|
424
|
+
|
425
|
+
数据库 Model 层通过环境变量控制数据库类型,在两种测试环境下有不同的数据库后端:客户端环境 (PGLite) 和 服务端环境 (PostgreSQL)
|
426
|
+
|
427
|
+
### ⚠️ 双环境验证要求
|
428
|
+
|
429
|
+
**对于所有 Model 测试,必须在两个环境下都验证通过**:
|
430
|
+
|
431
|
+
#### 完整验证流程
|
432
|
+
|
433
|
+
```bash
|
434
|
+
# 1. 先在客户端环境测试(快速验证)
|
435
|
+
npx vitest run --config vitest.config.ts src/database/models/__tests__/myModel.test.ts
|
436
|
+
|
437
|
+
# 2. 再在服务端环境测试(兼容性验证)
|
438
|
+
npx vitest run --config vitest.config.server.ts src/database/models/__tests__/myModel.test.ts
|
439
|
+
```
|
440
|
+
|
441
|
+
### 创建新 Model 测试的最佳实践 📋
|
442
|
+
|
443
|
+
#### 1. 参考现有实现和测试模板
|
444
|
+
|
445
|
+
创建新 Model 测试前,**必须先参考现有的实现模式**:
|
446
|
+
|
447
|
+
- **Model 实现参考**:
|
448
|
+
- **测试模板参考**:
|
449
|
+
- **复杂示例参考**:
|
450
|
+
|
451
|
+
#### 2. 用户权限检查 - 安全第一 🔒
|
452
|
+
|
453
|
+
这是**最关键的安全要求**。所有涉及用户数据的操作都必须包含用户权限检查:
|
454
|
+
|
455
|
+
**❌ 错误示例 - 存在安全漏洞**:
|
456
|
+
|
457
|
+
```typescript
|
458
|
+
// 危险:缺少用户权限检查,任何用户都能操作任何数据
|
459
|
+
update = async (id: string, data: Partial<MyModel>) => {
|
460
|
+
return this.db
|
461
|
+
.update(myTable)
|
462
|
+
.set(data)
|
463
|
+
.where(eq(myTable.id, id)) // ❌ 只检查 ID,没有检查 userId
|
464
|
+
.returning();
|
465
|
+
};
|
466
|
+
```
|
467
|
+
|
468
|
+
**✅ 正确示例 - 安全的实现**:
|
469
|
+
|
470
|
+
```typescript
|
471
|
+
// 安全:必须同时匹配 ID 和 userId
|
472
|
+
update = async (id: string, data: Partial<MyModel>) => {
|
473
|
+
return this.db
|
474
|
+
.update(myTable)
|
475
|
+
.set(data)
|
476
|
+
.where(
|
477
|
+
and(
|
478
|
+
eq(myTable.id, id),
|
479
|
+
eq(myTable.userId, this.userId) // ✅ 用户权限检查
|
480
|
+
)
|
481
|
+
)
|
482
|
+
.returning();
|
483
|
+
};
|
484
|
+
```
|
485
|
+
|
486
|
+
**必须进行用户权限检查的方法**:
|
487
|
+
|
488
|
+
- `update()` - 更新操作
|
489
|
+
- `delete()` - 删除操作
|
490
|
+
- `findById()` - 查找特定记录
|
491
|
+
- 任何涉及特定记录的查询或修改操作
|
492
|
+
|
493
|
+
#### 3. 测试文件结构和必测场景
|
494
|
+
|
495
|
+
**基本测试结构**:
|
496
|
+
|
497
|
+
```typescript
|
498
|
+
// @vitest-environment node
|
499
|
+
describe("MyModel", () => {
|
500
|
+
describe("create", () => {
|
501
|
+
it("should create a new record");
|
502
|
+
it("should handle edge cases");
|
503
|
+
});
|
504
|
+
|
505
|
+
describe("queryAll", () => {
|
506
|
+
it("should return records for current user only");
|
507
|
+
it("should handle empty results");
|
508
|
+
});
|
509
|
+
|
510
|
+
describe("update", () => {
|
511
|
+
it("should update own records");
|
512
|
+
it("should NOT update other users records"); // 🔒 安全测试
|
513
|
+
});
|
514
|
+
|
515
|
+
describe("delete", () => {
|
516
|
+
it("should delete own records");
|
517
|
+
it("should NOT delete other users records"); // 🔒 安全测试
|
518
|
+
});
|
519
|
+
|
520
|
+
describe("user isolation", () => {
|
521
|
+
it("should enforce user data isolation"); // 🔒 核心安全测试
|
522
|
+
});
|
523
|
+
});
|
524
|
+
```
|
525
|
+
|
526
|
+
**必须测试的安全场景** 🔒:
|
527
|
+
|
528
|
+
```typescript
|
529
|
+
it("should not update records of other users", async () => {
|
530
|
+
// 创建其他用户的记录
|
531
|
+
const [otherUserRecord] = await serverDB
|
532
|
+
.insert(myTable)
|
533
|
+
.values({ userId: "other-user", data: "original" })
|
534
|
+
.returning();
|
535
|
+
|
536
|
+
// 尝试更新其他用户的记录
|
537
|
+
const result = await myModel.update(otherUserRecord.id, { data: "hacked" });
|
538
|
+
|
539
|
+
// 应该返回 undefined 或空数组(因为权限检查失败)
|
540
|
+
expect(result).toBeUndefined();
|
541
|
+
|
542
|
+
// 验证原始数据未被修改
|
543
|
+
const unchanged = await serverDB.query.myTable.findFirst({
|
544
|
+
where: eq(myTable.id, otherUserRecord.id),
|
545
|
+
});
|
546
|
+
expect(unchanged?.data).toBe("original"); // 数据应该保持不变
|
547
|
+
});
|
548
|
+
```
|
549
|
+
|
550
|
+
#### 4. Mock 外部依赖服务
|
551
|
+
|
552
|
+
如果 Model 依赖外部服务(如 FileService),需要正确 Mock:
|
553
|
+
|
554
|
+
**设置 Mock**:
|
555
|
+
|
556
|
+
```typescript
|
557
|
+
// 在文件顶部设置 Mock
|
558
|
+
const mockGetFullFileUrl = vi.fn();
|
559
|
+
vi.mock("@/server/services/file", () => ({
|
560
|
+
FileService: vi.fn().mockImplementation(() => ({
|
561
|
+
getFullFileUrl: mockGetFullFileUrl,
|
562
|
+
})),
|
563
|
+
}));
|
564
|
+
|
565
|
+
// 在 beforeEach 中重置和配置 Mock
|
566
|
+
beforeEach(async () => {
|
567
|
+
vi.clearAllMocks();
|
568
|
+
mockGetFullFileUrl.mockImplementation(
|
569
|
+
(url: string) => `https://example.com/${url}`
|
570
|
+
);
|
571
|
+
});
|
572
|
+
```
|
573
|
+
|
574
|
+
**验证 Mock 调用**:
|
575
|
+
|
576
|
+
```typescript
|
577
|
+
it("should process URLs through FileService", async () => {
|
578
|
+
// ... 测试逻辑
|
579
|
+
|
580
|
+
// 验证 Mock 被正确调用
|
581
|
+
expect(mockGetFullFileUrl).toHaveBeenCalledWith("expected-url");
|
582
|
+
expect(mockGetFullFileUrl).toHaveBeenCalledTimes(1);
|
583
|
+
});
|
584
|
+
```
|
585
|
+
|
586
|
+
#### 5. 数据库状态管理
|
587
|
+
|
588
|
+
**正确的数据清理模式**:
|
589
|
+
|
590
|
+
```typescript
|
591
|
+
const userId = "test-user";
|
592
|
+
const otherUserId = "other-user";
|
593
|
+
|
594
|
+
beforeEach(async () => {
|
595
|
+
// 清理用户表(级联删除相关数据)
|
596
|
+
await serverDB.delete(users);
|
597
|
+
|
598
|
+
// 创建测试用户
|
599
|
+
await serverDB.insert(users).values([{ id: userId }, { id: otherUserId }]);
|
600
|
+
});
|
601
|
+
|
602
|
+
afterEach(async () => {
|
603
|
+
// 清理测试数据
|
604
|
+
await serverDB.delete(users);
|
605
|
+
});
|
606
|
+
```
|
607
|
+
|
608
|
+
#### 6. 测试数据类型和外键约束处理 ⚠️
|
609
|
+
|
610
|
+
**必须使用 Schema 导出的类型**:
|
611
|
+
|
612
|
+
```typescript
|
613
|
+
// ✅ 正确:使用 schema 导出的类型
|
614
|
+
import { NewGenerationBatch, NewGeneration } from '../../schemas';
|
615
|
+
|
616
|
+
const testBatch: NewGenerationBatch = {
|
617
|
+
userId,
|
618
|
+
generationTopicId: 'test-topic-id',
|
619
|
+
provider: 'test-provider',
|
620
|
+
model: 'test-model',
|
621
|
+
prompt: 'Test prompt for image generation',
|
622
|
+
width: 1024,
|
623
|
+
height: 1024,
|
624
|
+
config: { /* ... */ },
|
625
|
+
};
|
626
|
+
|
627
|
+
const testGeneration: NewGeneration = {
|
628
|
+
id: 'test-gen-id',
|
629
|
+
generationBatchId: 'test-batch-id',
|
630
|
+
asyncTaskId: null, // 处理外键约束
|
631
|
+
fileId: null, // 处理外键约束
|
632
|
+
seed: 12345,
|
633
|
+
userId,
|
634
|
+
};
|
635
|
+
```
|
636
|
+
|
637
|
+
```typescript
|
638
|
+
// ❌ 错误:没有类型声明或使用错误类型
|
639
|
+
const testBatch = { // 缺少类型声明
|
640
|
+
generationTopicId: 'test-topic-id',
|
641
|
+
// ...
|
642
|
+
};
|
643
|
+
|
644
|
+
const testGeneration = { // 缺少类型声明
|
645
|
+
asyncTaskId: 'invalid-uuid', // 外键约束错误
|
646
|
+
fileId: 'non-existent-file', // 外键约束错误
|
647
|
+
// ...
|
648
|
+
};
|
649
|
+
```
|
650
|
+
|
651
|
+
**外键约束处理策略**:
|
652
|
+
|
653
|
+
1. **使用 null 值**: 对于可选的外键字段,使用 null 避免约束错误
|
654
|
+
2. **创建关联记录**: 如果需要测试关联关系,先创建被引用的记录
|
655
|
+
3. **理解约束关系**: 了解哪些字段有外键约束,避免引用不存在的记录
|
656
|
+
|
657
|
+
```typescript
|
658
|
+
// 外键约束处理示例
|
659
|
+
beforeEach(async () => {
|
660
|
+
// 清理数据库
|
661
|
+
await serverDB.delete(users);
|
662
|
+
|
663
|
+
// 创建测试用户
|
664
|
+
await serverDB.insert(users).values([{ id: userId }]);
|
665
|
+
|
666
|
+
// 如果需要测试文件关联,创建文件记录
|
667
|
+
if (needsFileAssociation) {
|
668
|
+
await serverDB.insert(files).values({
|
669
|
+
id: 'test-file-id',
|
670
|
+
userId,
|
671
|
+
name: 'test.jpg',
|
672
|
+
url: 'test-url',
|
673
|
+
size: 1024,
|
674
|
+
fileType: 'image/jpeg',
|
675
|
+
});
|
676
|
+
}
|
677
|
+
});
|
678
|
+
```
|
679
|
+
|
680
|
+
**排序测试的可预测性**:
|
681
|
+
|
682
|
+
```typescript
|
683
|
+
// ✅ 正确:使用明确的时间戳确保排序结果可预测
|
684
|
+
it('should find batches by topic id in correct order', async () => {
|
685
|
+
const oldDate = new Date('2024-01-01T10:00:00Z');
|
686
|
+
const newDate = new Date('2024-01-02T10:00:00Z');
|
687
|
+
|
688
|
+
const batch1 = { ...testBatch, prompt: 'First batch', userId, createdAt: oldDate };
|
689
|
+
const batch2 = { ...testBatch, prompt: 'Second batch', userId, createdAt: newDate };
|
690
|
+
|
691
|
+
await serverDB.insert(generationBatches).values([batch1, batch2]);
|
692
|
+
|
693
|
+
const results = await generationBatchModel.findByTopicId(testTopic.id);
|
694
|
+
|
695
|
+
expect(results[0].prompt).toBe('Second batch'); // 最新优先 (desc order)
|
696
|
+
expect(results[1].prompt).toBe('First batch');
|
697
|
+
});
|
698
|
+
```
|
699
|
+
|
700
|
+
```typescript
|
701
|
+
// ❌ 错误:依赖数据库的默认时间戳,结果不可预测
|
702
|
+
it('should find batches by topic id', async () => {
|
703
|
+
const batch1 = { ...testBatch, prompt: 'First batch', userId };
|
704
|
+
const batch2 = { ...testBatch, prompt: 'Second batch', userId };
|
705
|
+
|
706
|
+
await serverDB.insert(generationBatches).values([batch1, batch2]);
|
707
|
+
|
708
|
+
// 插入顺序和数据库时间戳可能不一致,导致测试不稳定
|
709
|
+
const results = await generationBatchModel.findByTopicId(testTopic.id);
|
710
|
+
expect(results[0].prompt).toBe('Second batch'); // 可能失败
|
711
|
+
});
|
712
|
+
```
|
713
|
+
|
714
|
+
|
715
|
+
|
716
|
+
### 常见问题和解决方案 💡
|
717
|
+
|
718
|
+
#### 问题 1:权限检查缺失导致安全漏洞
|
719
|
+
|
720
|
+
**现象**: 测试失败,用户能修改其他用户的数据
|
721
|
+
**解决**: 在 Model 的 `update` 和 `delete` 方法中添加 `and(eq(table.id, id), eq(table.userId, this.userId))`
|
722
|
+
|
723
|
+
#### 问题 2:Mock 未生效或验证失败
|
724
|
+
|
725
|
+
**现象**: `undefined is not a spy` 错误
|
726
|
+
**解决**: 检查 Mock 设置位置和方式,确保在测试文件顶部设置,在 `beforeEach` 中重置
|
727
|
+
|
728
|
+
#### 问题 3:测试数据污染
|
729
|
+
|
730
|
+
**现象**: 测试间相互影响,结果不稳定
|
731
|
+
**解决**: 在 `beforeEach` 和 `afterEach` 中正确清理数据库状态
|
732
|
+
|
733
|
+
#### 问题 4:外部依赖导致测试失败
|
734
|
+
|
735
|
+
**现象**: 因为真实的外部服务调用导致测试不稳定
|
736
|
+
**解决**: Mock 所有外部依赖,使测试更可控和快速
|
737
|
+
|
738
|
+
#### 问题 5:外键约束违反导致测试失败
|
739
|
+
|
740
|
+
**现象**: `insert or update on table "xxx" violates foreign key constraint`
|
741
|
+
**解决**:
|
742
|
+
- 将可选外键字段设为 `null` 而不是无效的字符串值
|
743
|
+
- 或者先创建被引用的记录,再创建当前记录
|
744
|
+
|
745
|
+
```typescript
|
746
|
+
// ❌ 错误:无效的外键值
|
747
|
+
const testData = {
|
748
|
+
asyncTaskId: 'invalid-uuid', // 表中不存在此记录
|
749
|
+
fileId: 'non-existent-file', // 表中不存在此记录
|
750
|
+
};
|
751
|
+
|
752
|
+
// ✅ 正确:使用 null 值
|
753
|
+
const testData = {
|
754
|
+
asyncTaskId: null, // 避免外键约束
|
755
|
+
fileId: null, // 避免外键约束
|
756
|
+
};
|
757
|
+
|
758
|
+
// ✅ 或者:先创建被引用的记录
|
759
|
+
beforeEach(async () => {
|
760
|
+
const [asyncTask] = await serverDB.insert(asyncTasks).values({
|
761
|
+
id: 'valid-task-id',
|
762
|
+
status: 'pending',
|
763
|
+
type: 'generation',
|
764
|
+
}).returning();
|
765
|
+
|
766
|
+
const testData = {
|
767
|
+
asyncTaskId: asyncTask.id, // 使用有效的外键值
|
768
|
+
};
|
769
|
+
});
|
770
|
+
```
|
771
|
+
|
772
|
+
#### 问题 6:排序测试结果不一致
|
773
|
+
|
774
|
+
**现象**: 相同的测试有时通过,有时失败,特别是涉及排序的测试
|
775
|
+
**解决**: 使用明确的时间戳,不要依赖数据库的默认时间戳
|
776
|
+
|
777
|
+
```typescript
|
778
|
+
// ❌ 错误:依赖插入顺序和默认时间戳
|
779
|
+
await serverDB.insert(table).values([data1, data2]); // 时间戳不可预测
|
780
|
+
|
781
|
+
// ✅ 正确:明确指定时间戳
|
782
|
+
const oldDate = new Date('2024-01-01T10:00:00Z');
|
783
|
+
const newDate = new Date('2024-01-02T10:00:00Z');
|
784
|
+
await serverDB.insert(table).values([
|
785
|
+
{ ...data1, createdAt: oldDate },
|
786
|
+
{ ...data2, createdAt: newDate },
|
787
|
+
]);
|
788
|
+
```
|
789
|
+
|
790
|
+
#### 问题 7:Mock 验证失败或调用次数不匹配
|
791
|
+
|
792
|
+
**现象**: `expect(mockFunction).toHaveBeenCalledWith(...)` 失败
|
793
|
+
**解决**:
|
794
|
+
- 检查 Mock 函数的实际调用参数和期望参数是否完全匹配
|
795
|
+
- 确认 Mock 在正确的时机被重置和配置
|
796
|
+
- 使用 `toHaveBeenCalledTimes()` 验证调用次数
|
797
|
+
|
798
|
+
```typescript
|
799
|
+
// 在 beforeEach 中正确配置 Mock
|
800
|
+
beforeEach(() => {
|
801
|
+
vi.clearAllMocks(); // 重置所有 Mock
|
802
|
+
|
803
|
+
mockGetFullFileUrl.mockImplementation((url: string) => `https://example.com/${url}`);
|
804
|
+
mockTransformGeneration.mockResolvedValue({
|
805
|
+
id: 'test-id',
|
806
|
+
// ... 其他字段
|
807
|
+
});
|
808
|
+
});
|
809
|
+
|
810
|
+
// 测试中验证 Mock 调用
|
811
|
+
it('should call FileService with correct parameters', async () => {
|
812
|
+
await model.someMethod();
|
813
|
+
|
814
|
+
// 验证调用参数
|
815
|
+
expect(mockGetFullFileUrl).toHaveBeenCalledWith('expected-url');
|
816
|
+
// 验证调用次数
|
817
|
+
expect(mockGetFullFileUrl).toHaveBeenCalledTimes(1);
|
818
|
+
});
|
819
|
+
```
|
820
|
+
|
821
|
+
### Model 测试检查清单 ✅
|
822
|
+
|
823
|
+
创建 Model 测试时,请确保以下各项都已完成:
|
824
|
+
|
825
|
+
#### 🔧 基础配置
|
826
|
+
- [ ] **双环境验证** - 在客户端环境 (vitest.config.ts) 和服务端环境 (vitest.config.server.ts) 下都测试通过
|
827
|
+
- [ ] 参考了 `_template.ts` 和现有 Model 的实现模式
|
828
|
+
- [ ] **使用正确的 Schema 类型** - 测试数据使用 `NewXxx` 类型声明,如 `NewGenerationBatch`、`NewGeneration`
|
829
|
+
|
830
|
+
#### 🔒 安全测试
|
831
|
+
- [ ] **所有涉及用户数据的操作都包含用户权限检查**
|
832
|
+
- [ ] 包含了用户权限隔离的安全测试
|
833
|
+
- [ ] 测试了用户无法访问其他用户数据的场景
|
834
|
+
|
835
|
+
#### 🗃️ 数据处理
|
836
|
+
- [ ] **正确处理外键约束** - 使用 `null` 值或先创建被引用记录
|
837
|
+
- [ ] **排序测试使用明确时间戳** - 不依赖数据库默认时间,确保结果可预测
|
838
|
+
- [ ] 在 `beforeEach` 和 `afterEach` 中正确管理数据库状态
|
839
|
+
- [ ] 所有测试都能独立运行且互不干扰
|
840
|
+
|
841
|
+
#### 🎭 Mock 和外部依赖
|
842
|
+
- [ ] 正确 Mock 了外部依赖服务 (如 FileService、GenerationModel)
|
843
|
+
- [ ] 在 `beforeEach` 中重置和配置 Mock
|
844
|
+
- [ ] 验证了 Mock 服务的调用参数和次数
|
845
|
+
- [ ] 测试了外部服务错误场景的处理
|
846
|
+
|
847
|
+
#### 📋 测试覆盖
|
848
|
+
- [ ] 测试覆盖了所有主要方法 (create, query, update, delete)
|
849
|
+
- [ ] 测试了边界条件和错误场景
|
850
|
+
- [ ] 包含了空结果处理的测试
|
851
|
+
- [ ] **确认两个环境下的测试结果一致**
|
852
|
+
|
853
|
+
#### 🚨 常见问题检查
|
854
|
+
- [ ] 没有外键约束违反错误
|
855
|
+
- [ ] 排序测试结果稳定可预测
|
856
|
+
- [ ] Mock 验证无失败
|
857
|
+
- [ ] 无测试数据污染问题
|
858
|
+
|
859
|
+
### 安全警告 ⚠️
|
860
|
+
|
861
|
+
**数据库 Model 层是安全的第一道防线**。如果 Model 层缺少用户权限检查:
|
862
|
+
|
863
|
+
1. **任何用户都能访问和修改其他用户的数据**
|
864
|
+
2. **即使上层有权限检查,也可能被绕过**
|
865
|
+
3. **可能导致严重的数据泄露和安全事故**
|
866
|
+
|
867
|
+
因此,**每个涉及用户数据的 Model 方法都必须包含用户权限检查,且必须有对应的安全测试来验证这些检查的有效性**。
|
868
|
+
|
869
|
+
## 🎯 总结
|
870
|
+
|
871
|
+
修复测试时,记住以下关键点:
|
872
|
+
|
873
|
+
- **使用正确的命令**: `npx vitest run --config [config-file]`
|
874
|
+
- **理解测试意图**: 先读懂测试再修复
|
875
|
+
- **查看最近修改**: 检查相关文件的 git 修改记录,判断问题根源
|
876
|
+
- **选择正确环境**: 客户端测试用 `vitest.config.ts`,服务端用 `vitest.config.server.ts`
|
877
|
+
- **专注单一问题**: 只修复当前的测试失败
|
878
|
+
- **验证修复结果**: 确保修复后测试通过且无副作用
|
879
|
+
- **提供修复总结**: 说明错误原因和修复方法
|
880
|
+
- **Model 测试安全第一**: 必须包含用户权限检查和对应的安全测试
|
881
|
+
- **Model 双环境验证**: 必须在 PGLite 和 PostgreSQL 两个环境下都验证通过
|