# Web Server 包测试规范

## 版本信息
| 版本 | 日期 | 描述 | 作者 |
|------|------|------|------|
| 1.0 | 2025-12-26 | 从测试策略文档拆分，专注Web Server包测试 | James (Claude Code) |

## 概述

本文档定义了Web Server包的测试标准和最佳实践。
- **目标**: `packages/server` - API服务器包
- **测试类型**: 集成测试（模块间协作测试）

## 测试框架栈

- **Vitest**: 测试运行器
- **hono/testing**: Hono官方测试工具
- **TypeORM**: 数据库测试
- **PostgreSQL**: 测试数据库
- **shared-test-util**: 共享测试基础设施

## 测试策略

### 集成测试（Integration Tests）
- **范围**: 模块间集成、API端点集成
- **目标**: 验证各业务模块在server环境中的正确集成
- **位置**: `packages/server/tests/integration/**/*.test.ts`
- **框架**: Vitest + hono/testing + TypeORM
- **覆盖率目标**: ≥ 60%

## 测试文件结构

```
packages/server/
├── src/
│   ├── api.ts              # API路由导出
│   └── index.ts            # 服务器入口
└── tests/
    ├── integration/
    │   ├── auth.integration.test.ts      # 认证集成测试
    │   ├── users.integration.test.ts     # 用户管理集成测试
    │   ├── files.integration.test.ts     # 文件管理集成测试
    │   └── api-integration.test.ts       # API端点集成测试
    └── fixtures/
        ├── test-db.ts                    # 测试数据库配置
        └── test-data.ts                  # 测试数据工厂
```

## 集成测试最佳实践

### 1. 使用hono/testing测试API端点
```typescript
import { test } from 'vitest';
import { integrateRoutes } from 'hono/testing';
import app from '../src/api';

describe('POST /api/auth/login', () => {
  it('应该成功登录并返回token', async () => {
    // Arrange
    const testData = {
      username: 'testuser',
      password: 'password123'
    };

    // Act
    const res = await integrateRoutes(app).POST('/api/auth/login', {
      json: testData
    });

    // Assert
    expect(res.status).toBe(200);
    const json = await res.json();
    expect(json).toHaveProperty('token');
    expect(json.user.username).toBe('testuser');
  });
});
```

### 2. 测试数据库集成
```typescript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { App } from 'hono';
import { DataSource } from 'typeorm';
import { getTestDataSource } from './fixtures/test-db';
import { User } from '@d8d/user-module/entities';

describe('用户管理集成测试', () => {
  let dataSource: DataSource;

  beforeEach(async () => {
    // 设置测试数据库
    dataSource = await getTestDataSource();
    await dataSource.initialize();
  });

  afterEach(async () => {
    // 清理测试数据库
    await dataSource.destroy();
  });

  it('应该创建用户并返回用户数据', async () => {
    const app = new App();
    app.route('/api/users', userRoutes);

    const res = await integrateRoutes(app).POST('/api/users', {
      json: {
        username: 'testuser',
        email: 'test@example.com',
        password: 'password123'
      }
    });

    expect(res.status).toBe(201);

    // 验证数据库中的数据
    const userRepo = dataSource.getRepository(User);
    const user = await userRepo.findOne({ where: { username: 'testuser' } });
    expect(user).toBeDefined();
    expect(user?.email).toBe('test@example.com');
  });
});
```

### 3. 测试认证中间件
```typescript
import { generateToken } from '@d8d/shared-utils/jwt.util';

describe('认证保护的API端点', () => {
  it('应该拒绝未认证的请求', async () => {
    const res = await integrateRoutes(app).GET('/api/users/me');

    expect(res.status).toBe(401);
    expect(await res.json()).toEqual({
      error: '未授权访问'
    });
  });

  it('应该接受有效token的请求', async () => {
    const token = generateToken({ userId: 1, username: 'testuser' });

    const res = await integrateRoutes(app).GET('/api/users/me', {
      headers: {
        Authorization: `Bearer ${token}`
      }
    });

    expect(res.status).toBe(200);
    const json = await res.json();
    expect(json.username).toBe('testuser');
  });
});
```

### 4. 测试模块间集成
```typescript
describe('认证与用户模块集成', () => {
  it('应该成功注册用户并自动登录', async () => {
    const app = new App();
    app.route('/api/auth', authRoutes);
    app.route('/api/users', userRoutes);

    // 1. 注册用户
    const registerRes = await integrateRoutes(app).POST('/api/auth/register', {
      json: {
        username: 'newuser',
        email: 'new@example.com',
        password: 'password123'
      }
    });

    expect(registerRes.status).toBe(201);

    // 2. 登录
    const loginRes = await integrateRoutes(app).POST('/api/auth/login', {
      json: {
        username: 'newuser',
        password: 'password123'
      }
    });

    expect(loginRes.status).toBe(200);
    const { token } = await loginRes.json();
    expect(token).toBeDefined();

    // 3. 使用token访问受保护的端点
    const meRes = await integrateRoutes(app).GET('/api/users/me', {
      headers: {
        Authorization: `Bearer ${token}`
      }
    });

    expect(meRes.status).toBe(200);
    const user = await meRes.json();
    expect(user.username).toBe('newuser');
  });
});
```

### 5. 测试文件上传集成
```typescript
import { createReadStream } from 'fs';
import { FormData } from 'hono/client';

describe('文件上传集成测试', () => {
  it('应该成功上传文件并返回文件URL', async () => {
    const formData = new FormData();
    formData.append('file', createReadStream('test/fixtures/test-file.png'));

    const res = await integrateRoutes(app).POST('/api/files/upload', {
      body: formData
    });

    expect(res.status).toBe(201);
    const json = await res.json();
    expect(json).toHaveProperty('url');
    expect(json).toHaveProperty('id');
  });
});
```

### 6. 测试错误处理
```typescript
describe('API错误处理', () => {
  it('应该返回404当资源不存在', async () => {
    const res = await integrateRoutes(app).GET('/api/users/99999');

    expect(res.status).toBe(404);
    expect(await res.json()).toEqual({
      error: '用户不存在'
    });
  });

  it('应该返回400当请求数据无效', async () => {
    const res = await integrateRoutes(app).POST('/api/users', {
      json: {
        username: '', // 无效的用户名
        email: 'invalid-email' // 无效的邮箱
      }
    });

    expect(res.status).toBe(400);
    const json = await res.json();
    expect(json).toHaveProperty('errors');
  });
});
```

### 7. 使用共享测试工具
```typescript
import { createIntegrationTestApp, setupTestDatabase, teardownTestDatabase } from '@d8d/shared-test-util';

describe('使用共享测试工具', () => {
  let dataSource: DataSource;
  let app: Hono;

  beforeAll(async () => {
    dataSource = await setupTestDatabase();
    app = await createIntegrationTestApp(dataSource);
  });

  afterAll(async () => {
    await teardownTestDatabase(dataSource);
  });

  it('应该使用共享工具运行集成测试', async () => {
    const res = await integrateRoutes(app).GET('/api/users');

    expect(res.status).toBe(200);
  });
});
```

## 测试数据管理

### 测试数据工厂
```typescript
// tests/fixtures/test-data.ts
import { User } from '@d8d/user-module/entities';

export function createTestUser(overrides = {}): Partial<User> {
  return {
    id: 1,
    username: 'testuser',
    email: 'test@example.com',
    password: 'hashedpassword',
    role: 'user',
    active: true,
    createdAt: new Date(),
    ...overrides
  };
}

export async function seedTestUser(dataSource: DataSource, userData = {}) {
  const userRepo = dataSource.getRepository(User);
  const user = userRepo.create(createTestUser(userData));
  return await userRepo.save(user);
}
```

### 数据库清理策略
```typescript
// 选项1: 事务回滚（推荐）
describe('使用事务回滚', () => {
  let queryRunner: QueryRunner;

  beforeEach(async () => {
    queryRunner = dataSource.createQueryRunner();
    await queryRunner.startTransaction();
  });

  afterEach(async () => {
    await queryRunner.rollbackTransaction();
    await queryRunner.release();
  });
});

// 选项2: 每个测试后清理
describe('使用数据库清理', () => {
  afterEach(async () => {
    const entities = dataSource.entityMetadatas;
    for (const entity of entities) {
      const repository = dataSource.getRepository(entity.name);
      await repository.clear();
    }
  });
});
```

## 测试命名约定

### 文件命名
- 集成测试：`[module].integration.test.ts`
- API测试：`[endpoint].integration.test.ts`

### 测试描述
```typescript
describe('用户管理API', () => {
  describe('GET /api/users', () => {
    it('应该返回用户列表', async () => { });
    it('应该支持分页', async () => { });
    it('应该支持搜索过滤', async () => { });
  });

  describe('POST /api/users', () => {
    it('应该创建新用户', async () => { });
    it('应该验证重复用户名', async () => { });
    it('应该验证邮箱格式', async () => { });
  });
});
```

## 环境配置

### 测试环境变量
```typescript
// vitest.config.ts
export default defineConfig({
  test: {
    setupFiles: ['./tests/setup.ts'],
    env: {
      NODE_ENV: 'test',
      DATABASE_URL: 'postgresql://postgres:test_password@localhost:5432/test_d8dai',
      JWT_SECRET: 'test_secret',
      REDIS_URL: 'redis://localhost:6379/1'
    }
  }
});
```

### 测试数据库设置
```typescript
// tests/fixtures/test-db.ts
import { DataSource } from 'typeorm';
import { User } from '@d8d/user-module/entities';
import { File } from '@d8d/file-module/entities';

export async function getTestDataSource(): Promise<DataSource> {
  return new DataSource({
    type: 'postgres',
    host: 'localhost',
    port: 5432,
    username: 'postgres',
    password: 'test_password',
    database: 'test_d8dai',
    entities: [User, File],
    synchronize: true, // 测试环境自动同步表结构
    dropSchema: true, // 每次测试前清空数据库
    logging: false
  });
}
```

## 覆盖率标准

| 测试类型 | 最低要求 | 目标要求 |
|----------|----------|----------|
| 集成测试 | 50% | 60% |

**关键端点要求**：
- 认证端点：100%覆盖
- 用户管理端点：80%覆盖
- 文件上传端点：70%覆盖

## 运行测试

### 本地开发
```bash
# 运行所有集成测试
cd packages/server && pnpm test

# 运行特定集成测试
pnpm test users.integration.test.ts

# 生成覆盖率报告
pnpm test:coverage

# 运行特定测试用例
pnpm test --testNamePattern="应该成功创建用户"
```

### CI/CD
```yaml
server-integration-tests:
  runs-on: ubuntu-latest
  services:
    postgres:
      image: postgres:17
      env:
        POSTGRES_PASSWORD: test_password
        POSTGRES_DB: test_d8dai
      options: >-
        --health-cmd pg_isready
        --health-interval 10s
        --health-timeout 5s
        --health-retries 5
  steps:
    - uses: actions/checkout@v3
    - uses: pnpm/action-setup@v2
    - run: cd packages/server && pnpm install
    - run: cd packages/server && pnpm test
```

## 调试技巧

### 1. 使用调试模式
```bash
# 运行特定测试并显示详细信息
pnpm test --testNamePattern="用户登录" --reporter=verbose

# 监听模式（开发时）
pnpm test --watch
```

### 2. 查看响应详情
```typescript
const res = await integrateRoutes(app).POST('/api/users', { json: userData });

// 打印完整响应
console.log('Status:', res.status);
console.log('Headers:', res.headers);
console.log('Body:', await res.json());
```

### 3. 数据库调试
```typescript
// 启用SQL查询日志
const dataSource = new DataSource({
  // ...其他配置
  logging: true, // 显示所有SQL查询
  maxQueryExecutionTime: 1000 // 记录慢查询
});
```

## 常见错误避免

### ❌ 不要在集成测试中使用mock
```typescript
// 错误：模拟数据库查询
vi.mock('@d8d/user-module/services/user.service', () => ({
  UserService: {
    findAll: vi.fn(() => Promise.resolve(mockUsers))
  }
}));

// 正确：使用真实的数据库和服务
```

### ❌ 不要忽略异步清理
```typescript
// 错误：不清理数据库
afterEach(() => {
  // 数据库没有被清理
});

// 正确：确保数据库清理
afterEach(async () => {
  await dataSource.dropDatabase();
});
```

### ❌ 不要硬编码测试数据
```typescript
// 错误：硬编码ID
it('应该返回用户详情', async () => {
  const res = await integrateRoutes(app).GET('/api/users/123');
});

// 正确：使用动态创建的数据
it('应该返回用户详情', async () => {
  const user = await seedTestUser(dataSource);
  const res = await integrateRoutes(app).GET(`/api/users/${user.id}`);
});
```

## 参考实现

- Server包集成测试：`packages/server/tests/integration/`
- 共享测试工具：`packages/shared-test-util/`

## 相关文档

- [测试策略概述](./testing-strategy.md)
- [后端模块包测试规范](./backend-module-testing-standards.md)
- [Web UI包测试规范](./web-ui-testing-standards.md)
- [后端模块包开发规范](./backend-module-package-standards.md)

---

**文档状态**: 正式版
**适用范围**: packages/server
