---
globs: src/database/**/*.test.ts
alwaysApply: false
---

## 🗃️ 数据库 Model 测试指南

### 测试环境选择 💡

数据库 Model 层通过环境变量控制数据库类型，在两种测试环境下有不同的数据库后端：客户端环境 (PGLite) 和 服务端环境 (PostgreSQL)

### ⚠️ 双环境验证要求

**对于所有 Model 测试，必须在两个环境下都验证通过**：

#### 完整验证流程

```bash
# 1. 先在客户端环境测试（快速验证）
npx vitest run --config vitest.config.ts src/database/models/__tests__/myModel.test.ts

# 2. 再在服务端环境测试（兼容性验证）
npx vitest run --config vitest.config.server.ts src/database/models/__tests__/myModel.test.ts
```

### 创建新 Model 测试的最佳实践 📋

#### 1. 参考现有实现和测试模板

创建新 Model 测试前，**必须先参考现有的实现模式**：

- **Model 实现参考**:
- **测试模板参考**:
- **复杂示例参考**:

#### 2. 用户权限检查 - 安全第一 🔒

这是**最关键的安全要求**。所有涉及用户数据的操作都必须包含用户权限检查：

**❌ 错误示例 - 存在安全漏洞**:

```typescript
// 危险：缺少用户权限检查，任何用户都能操作任何数据
update = async (id: string, data: Partial<MyModel>) => {
  return this.db
    .update(myTable)
    .set(data)
    .where(eq(myTable.id, id)) // ❌ 只检查 ID，没有检查 userId
    .returning();
};
```

**✅ 正确示例 - 安全的实现**:

```typescript
// 安全：必须同时匹配 ID 和 userId
update = async (id: string, data: Partial<MyModel>) => {
  return this.db
    .update(myTable)
    .set(data)
    .where(
      and(
        eq(myTable.id, id),
        eq(myTable.userId, this.userId), // ✅ 用户权限检查
      ),
    )
    .returning();
};
```

**必须进行用户权限检查的方法**：

- `update()` - 更新操作
- `delete()` - 删除操作
- `findById()` - 查找特定记录
- 任何涉及特定记录的查询或修改操作

#### 3. 测试文件结构和必测场景

**基本测试结构**:

```typescript
// @vitest-environment node
describe('MyModel', () => {
  describe('create', () => {
    it('should create a new record');
    it('should handle edge cases');
  });

  describe('queryAll', () => {
    it('should return records for current user only');
    it('should handle empty results');
  });

  describe('update', () => {
    it('should update own records');
    it('should NOT update other users records'); // 🔒 安全测试
  });

  describe('delete', () => {
    it('should delete own records');
    it('should NOT delete other users records'); // 🔒 安全测试
  });

  describe('user isolation', () => {
    it('should enforce user data isolation'); // 🔒 核心安全测试
  });
});
```

**必须测试的安全场景** 🔒:

```typescript
it('should not update records of other users', async () => {
  // 创建其他用户的记录
  const [otherUserRecord] = await serverDB
    .insert(myTable)
    .values({ userId: 'other-user', data: 'original' })
    .returning();

  // 尝试更新其他用户的记录
  const result = await myModel.update(otherUserRecord.id, { data: 'hacked' });

  // 应该返回 undefined 或空数组（因为权限检查失败）
  expect(result).toBeUndefined();

  // 验证原始数据未被修改
  const unchanged = await serverDB.query.myTable.findFirst({
    where: eq(myTable.id, otherUserRecord.id),
  });
  expect(unchanged?.data).toBe('original'); // 数据应该保持不变
});
```

#### 4. Mock 外部依赖服务

如果 Model 依赖外部服务（如 FileService），需要正确 Mock：

**设置 Mock**:

```typescript
// 在文件顶部设置 Mock
const mockGetFullFileUrl = vi.fn();
vi.mock('@/server/services/file', () => ({
  FileService: vi.fn().mockImplementation(() => ({
    getFullFileUrl: mockGetFullFileUrl,
  })),
}));

// 在 beforeEach 中重置和配置 Mock
beforeEach(async () => {
  vi.clearAllMocks();
  mockGetFullFileUrl.mockImplementation((url: string) => `https://example.com/${url}`);
});
```

**验证 Mock 调用**:

```typescript
it('should process URLs through FileService', async () => {
  // ... 测试逻辑

  // 验证 Mock 被正确调用
  expect(mockGetFullFileUrl).toHaveBeenCalledWith('expected-url');
  expect(mockGetFullFileUrl).toHaveBeenCalledTimes(1);
});
```

#### 5. 数据库状态管理

**正确的数据清理模式**:

```typescript
const userId = 'test-user';
const otherUserId = 'other-user';

beforeEach(async () => {
  // 清理用户表（级联删除相关数据）
  await serverDB.delete(users);

  // 创建测试用户
  await serverDB.insert(users).values([{ id: userId }, { id: otherUserId }]);
});

afterEach(async () => {
  // 清理测试数据
  await serverDB.delete(users);
});
```

#### 6. 测试数据类型和外键约束处理 ⚠️

**必须使用 Schema 导出的类型**:

```typescript
// ✅ 正确：使用 schema 导出的类型
import { NewGeneration, NewGenerationBatch } from '../../schemas';

const testBatch: NewGenerationBatch = {
  userId,
  generationTopicId: 'test-topic-id',
  provider: 'test-provider',
  model: 'test-model',
  prompt: 'Test prompt for image generation',
  width: 1024,
  height: 1024,
  config: {
    /* ... */
  },
};

const testGeneration: NewGeneration = {
  id: 'test-gen-id',
  generationBatchId: 'test-batch-id',
  asyncTaskId: null, // 处理外键约束
  fileId: null, // 处理外键约束
  seed: 12345,
  userId,
};
```

```typescript
// ❌ 错误：没有类型声明或使用错误类型
const testBatch = {
  // 缺少类型声明
  generationTopicId: 'test-topic-id',
  // ...
};

const testGeneration = {
  // 缺少类型声明
  asyncTaskId: 'invalid-uuid', // 外键约束错误
  fileId: 'non-existent-file', // 外键约束错误
  // ...
};
```

**外键约束处理策略**:

1. **使用 null 值**: 对于可选的外键字段，使用 null 避免约束错误
2. **创建关联记录**: 如果需要测试关联关系，先创建被引用的记录
3. **理解约束关系**: 了解哪些字段有外键约束，避免引用不存在的记录

```typescript
// 外键约束处理示例
beforeEach(async () => {
  // 清理数据库
  await serverDB.delete(users);

  // 创建测试用户
  await serverDB.insert(users).values([{ id: userId }]);

  // 如果需要测试文件关联，创建文件记录
  if (needsFileAssociation) {
    await serverDB.insert(files).values({
      id: 'test-file-id',
      userId,
      name: 'test.jpg',
      url: 'test-url',
      size: 1024,
      fileType: 'image/jpeg',
    });
  }
});
```

**排序测试的可预测性**:

```typescript
// ✅ 正确：使用明确的时间戳确保排序结果可预测
it('should find batches by topic id in correct order', async () => {
  const oldDate = new Date('2024-01-01T10:00:00Z');
  const newDate = new Date('2024-01-02T10:00:00Z');

  const batch1 = { ...testBatch, prompt: 'First batch', userId, createdAt: oldDate };
  const batch2 = { ...testBatch, prompt: 'Second batch', userId, createdAt: newDate };

  await serverDB.insert(generationBatches).values([batch1, batch2]);

  const results = await generationBatchModel.findByTopicId(testTopic.id);

  expect(results[0].prompt).toBe('Second batch'); // 最新优先 (desc order)
  expect(results[1].prompt).toBe('First batch');
});
```

```typescript
// ❌ 错误：依赖数据库的默认时间戳，结果不可预测
it('should find batches by topic id', async () => {
  const batch1 = { ...testBatch, prompt: 'First batch', userId };
  const batch2 = { ...testBatch, prompt: 'Second batch', userId };

  await serverDB.insert(generationBatches).values([batch1, batch2]);

  // 插入顺序和数据库时间戳可能不一致，导致测试不稳定
  const results = await generationBatchModel.findByTopicId(testTopic.id);
  expect(results[0].prompt).toBe('Second batch'); // 可能失败
});
```

### 常见问题和解决方案 💡

#### 问题 1：权限检查缺失导致安全漏洞

**现象**: 测试失败，用户能修改其他用户的数据 **解决**: 在 Model 的 `update` 和 `delete` 方法中添加 `and(eq(table.id, id), eq(table.userId, this.userId))`

#### 问题 2：Mock 未生效或验证失败

**现象**: `undefined is not a spy` 错误 **解决**: 检查 Mock 设置位置和方式，确保在测试文件顶部设置，在 `beforeEach` 中重置

#### 问题 3：测试数据污染

**现象**: 测试间相互影响，结果不稳定 **解决**: 在 `beforeEach` 和 `afterEach` 中正确清理数据库状态

#### 问题 4：外部依赖导致测试失败

**现象**: 因为真实的外部服务调用导致测试不稳定 **解决**: Mock 所有外部依赖，使测试更可控和快速

#### 问题 5：外键约束违反导致测试失败

**现象**: `insert or update on table "xxx" violates foreign key constraint` **解决**:

- 将可选外键字段设为 `null` 而不是无效的字符串值
- 或者先创建被引用的记录，再创建当前记录

```typescript
// ❌ 错误：无效的外键值
const testData = {
    asyncTaskId: 'invalid-uuid',  // 表中不存在此记录
    fileId: 'non-existent-file',  // 表中不存在此记录
};

// ✅ 正确：使用 null 值
const testData = {
    asyncTaskId: null,  // 避免外键约束
    fileId: null,       // 避免外键约束
};

// ✅ 或者：先创建被引用的记录
beforeEach(async () => {
    const [asyncTask] = await serverDB.insert(asyncTasks).values({
        id: 'valid-task-id',
        status: 'pending',
        type: 'generation',
    }).returning();

    const testData = {
        asyncTaskId: asyncTask.id,  // 使用有效的外键值
    };
});
```

#### 问题 6：排序测试结果不一致

**现象**: 相同的测试有时通过，有时失败，特别是涉及排序的测试 **解决**: 使用明确的时间戳，不要依赖数据库的默认时间戳

```typescript
// ❌ 错误：依赖插入顺序和默认时间戳
await serverDB.insert(table).values([data1, data2]); // 时间戳不可预测

// ✅ 正确：明确指定时间戳
const oldDate = new Date('2024-01-01T10:00:00Z');
const newDate = new Date('2024-01-02T10:00:00Z');
await serverDB.insert(table).values([
  { ...data1, createdAt: oldDate },
  { ...data2, createdAt: newDate },
]);
```

#### 问题 7：Mock 验证失败或调用次数不匹配

**现象**: `expect(mockFunction).toHaveBeenCalledWith(...)` 失败 **解决**:

- 检查 Mock 函数的实际调用参数和期望参数是否完全匹配
- 确认 Mock 在正确的时机被重置和配置
- 使用 `toHaveBeenCalledTimes()` 验证调用次数

```typescript
// 在 beforeEach 中正确配置 Mock
beforeEach(() => {
  vi.clearAllMocks(); // 重置所有 Mock

  mockGetFullFileUrl.mockImplementation((url: string) => `https://example.com/${url}`);
  mockTransformGeneration.mockResolvedValue({
    id: 'test-id',
    // ... 其他字段
  });
});

// 测试中验证 Mock 调用
it('should call FileService with correct parameters', async () => {
  await model.someMethod();

  // 验证调用参数
  expect(mockGetFullFileUrl).toHaveBeenCalledWith('expected-url');
  // 验证调用次数
  expect(mockGetFullFileUrl).toHaveBeenCalledTimes(1);
});
```

### Model 测试检查清单 ✅

创建 Model 测试时，请确保以下各项都已完成：

#### 🔧 基础配置

- [ ] **双环境验证** - 在客户端环境 (vitest.config.ts) 和服务端环境 (vitest.config.server.ts) 下都测试通过
- [ ] 参考了 `_template.ts` 和现有 Model 的实现模式
- [ ] **使用正确的 Schema 类型** - 测试数据使用 `NewXxx` 类型声明，如 `NewGenerationBatch`、`NewGeneration`

#### 🔒 安全测试

- [ ] **所有涉及用户数据的操作都包含用户权限检查**
- [ ] 包含了用户权限隔离的安全测试
- [ ] 测试了用户无法访问其他用户数据的场景

#### 🗃️ 数据处理

- [ ] **正确处理外键约束** - 使用 `null` 值或先创建被引用记录
- [ ] **排序测试使用明确时间戳** - 不依赖数据库默认时间，确保结果可预测
- [ ] 在 `beforeEach` 和 `afterEach` 中正确管理数据库状态
- [ ] 所有测试都能独立运行且互不干扰

#### 🎭 Mock 和外部依赖

- [ ] 正确 Mock 了外部依赖服务 (如 FileService、GenerationModel)
- [ ] 在 `beforeEach` 中重置和配置 Mock
- [ ] 验证了 Mock 服务的调用参数和次数
- [ ] 测试了外部服务错误场景的处理

#### 📋 测试覆盖

- [ ] 测试覆盖了所有主要方法 (create, query, update, delete)
- [ ] 测试了边界条件和错误场景
- [ ] 包含了空结果处理的测试
- [ ] **确认两个环境下的测试结果一致**

#### 🚨 常见问题检查

- [ ] 没有外键约束违反错误
- [ ] 排序测试结果稳定可预测
- [ ] Mock 验证无失败
- [ ] 无测试数据污染问题

### 安全警告 ⚠️

**数据库 Model 层是安全的第一道防线**。如果 Model 层缺少用户权限检查：

1. **任何用户都能访问和修改其他用户的数据**
2. **即使上层有权限检查，也可能被绕过**
3. **可能导致严重的数据泄露和安全事故**

因此，**每个涉及用户数据的 Model 方法都必须包含用户权限检查，且必须有对应的安全测试来验证这些检查的有效性**。
