# L1 集成测试写 API 调用的 4 个坑

**date**: 2026-05-17
**context**: PR #402 (`feature/robot-manager-refactor-pr1-platform-dict-enum`) backend-integration CI failed 20/25，本地复跑定位到测试本身 4 处错。

## 症状

新写的 `platform-master.integration.test.ts` + `v3-stage-guard.test.ts` 25 个测试 20 个 fail：
- API 测试返回 404 / 401
- guard 测试 throw "前置 seed 不全（orgId=false modelId=false skuId=false）"

## 4 个独立坑

### 1. URL 前缀必须是 `/api/v1`，不是 `/api`
- `backend/src/config/configuration.ts`: `apiPrefix: process.env.API_PREFIX || '/api/v1'`
- `testing/backend/helpers/app.helper.ts::createTestApp` 显式 `setGlobalPrefix('/api/v1')`
- Caddy 反代也是按 `/api/v1` 转发后端
- 凭直觉写 `/api/...` → 直接 404 NotFound
- **正例**：`testing/backend/integration/organization/organizations.api.test.ts` 全部用 `/api/v1/organizations`

### 2. 必须带 JWT token（全局 JwtAuthGuard 兜底）
- `app.module.ts` L110 注册 `APP_GUARD: JwtAuthGuard`，**所有路由默认强鉴权**，控制器即使不显式 `@UseGuards` 也会被全局 guard 拦
- 测试想绕过：用 `setupIntegrationTest(app, prisma)` 拿 `adminToken`，每个请求 `.set('Authorization', \`Bearer \${adminToken}\`)`
- 没带 token → 401 Unauthorized（不是 ai-review 误报的"controller 未挂 guard"——是 false positive；全局兜底就够了）

### 3. 响应被 TransformInterceptor 包了一层
- `main.ts` 注册 `TransformInterceptor`：所有成功响应包成 `{ code, data, message }`
- 测试读 `res.body` 直接拿到的是包装外壳，不是业务正文
- **正确读法**：`res.body.data`
- 排除：登录响应的 `accessToken` 也走包装 → `loginResponse.body.data.accessToken`

### 4. 测试 DB seed 不含 Organization / RobotModel / RobotSku
- `reset_test_db_schema` 跑 `prisma/seed.ts` → 只 seed 权限 / 角色 / 字典 / itadmin / 16 roles，**没有业务实体**（Org / RobotModel / RobotSku / RobotUnit 全 0）
- `prisma/seeds/robot-manager-e2e-seed.ts` 不在默认 seed 里，是 one-shot 脚本
- 测试 `findFirst({ organization })` 拿不到 → 整段假设失败
- **正确做法**：beforeAll/beforeEach 现场 `prisma.organization.create(...)` + `robotModel.create(...)` + `robotSku.create(...)`，afterAll 按前缀清理（`code: { startsWith: 't_' }`）
- 千万别静默 skip 这种 fixture 不全的场景——`v3-stage-guard.test.ts` 第 1 版用 throw Error 把假设失败显式化，CI fail 比 silent skip 快

## 复用法

后续给 `platform_master` / `agent` / 任何新模块写 L1 集成测试，**4 个 invariant 先 checklist 过一遍**：

```ts
import { setupIntegrationTest } from '../../helpers/test-setup.helper';

describe('XXX API', () => {
  let app: INestApplication;
  let adminToken: string;
  const authGet = (path: string) =>
    request(app.getHttpServer()).get(path).set('Authorization', `Bearer ${adminToken}`);

  beforeAll(async () => {
    app = await createTestApp();
    const ctx = await setupIntegrationTest(app, prisma);
    adminToken = ctx.adminToken;
    // 业务 fixture 现场 create，不依赖 seed
  });

  it('GET /api/v1/xxx returns wrapped body', async () => {
    const res = await authGet('/api/v1/xxx').expect(200);
    expect(res.body.data).toBeInstanceOf(Array);
  });
});
```

## 为什么没第一时间发现

- 本地 dev DB `npm run db:seed` 会跑全套 seed（含 e2e-seed），所以本地手测看不出来
- 测试 DB 走 `reset_test_db_schema` + `prisma/seed.ts`（不含 e2e-seed），fixture 缺
- 这条差异不在 onboarding docs 里 → 写新模块的 L1 测试容易踩

## 反例（已修）

`v3-stage-guard.test.ts` v1：
```ts
const orgId = (await prisma.organization.findFirst())?.id;
// orgId 在测试 DB 永远是 undefined → 第一个 it.each 就 fail
```

`v2`（修复后）：
```ts
beforeAll(async () => {
  const org = await prisma.organization.create({ data: { code: `t_org_${Date.now()}`, name: '...' } });
  orgId = org.id;
  // model / sku 同理
});
```

## 关联
- PR #402 fix commit
- `docs/standards/05-development-workflow.md` 测试章节可加这 4 个 checkpoint
