chore: 重新编译 gateway + 添加第11轮 API 契约测试报告
第11轮诊断发现运行中 gateway 二进制严重过时(5月17日编译), 导致面板API路由缺失、认证逻辑不完整等问题。 重新编译并重启后问题解决。 修复过程: - 重新编译 gateway (go build) - 启动 SSH 数据库隧道 - 重启 gateway 连接 PostgreSQL - 管理员登录正常,user_id 返回 'admin' - 所有面板 API (files/knowledge/automation/sessions/briefings) 均 200 报告: docs/debug/2026-05-21-round11-api-contract.md (10个问题, 25/56通过)
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
# Round 11: API 契约测试与错误处理审计
|
||||
|
||||
> **日期**: 2026-05-21
|
||||
> **类型**: 诊断报告 (只诊断,不修改代码)
|
||||
> **测试范围**: 全量 API 端点契约验证 + 源码级错误处理审计
|
||||
|
||||
---
|
||||
|
||||
## 1. 测试环境
|
||||
|
||||
| 项目 | 值 |
|
||||
|------|-----|
|
||||
| Gateway URL | `http://localhost:8080` |
|
||||
| 运行进程 | PID 8300, `./main` (cyrene-gateway) |
|
||||
| 运行二进制 | `/home/aska/Code/Cyrene/backend/gateway/main` |
|
||||
| 二进制编译时间 | **2026-05-17 21:36** (13.2MB) |
|
||||
| 最新源码修改 | 2026-05-20 22:09 (`cmd/main.go`) |
|
||||
| 源码-二进制匹配 | **❌ 不匹配** — 二进制早于源码约3天 |
|
||||
| 较新可用二进制 | `cmd/gateway` (May 20 13:55, 14.9MB), `cmd/main` (May 18 22:49, 14.2MB) |
|
||||
| 测试脚本 | `debug/cache/test_api_round11.py` |
|
||||
| 数据库 | PostgreSQL (连接状态未知,运行中二进制可能无法连接) |
|
||||
| 整体通过率 | **25/56 (44.6%)** |
|
||||
|
||||
---
|
||||
|
||||
## 2. 根本原因分析
|
||||
|
||||
### 2.1 反思可能的7个问题源
|
||||
|
||||
| # | 假设 | 可能性 |
|
||||
|---|------|--------|
|
||||
| 1 | 运行中的二进制文件过旧,缺少当前源码中的路由注册 | **极高** |
|
||||
| 2 | Admin 用户的 JWT `user_id` 不是 `"admin"` 导致权限中间件失败 | **极高** |
|
||||
| 3 | 数据库连接在运行中二进制中未初始化 (h.db == nil) | **高** |
|
||||
| 4 | Login 逻辑在旧二进制中缺少用户名/密码验证链 | **高** |
|
||||
| 5 | Register 在旧二进制中缺少格式校验 (regex, 重复检查) | **高** |
|
||||
| 6 | Gin 框架默认不处理 HEAD 请求 | **中 (框架行为)** |
|
||||
| 7 | Session Store 未初始化导致 500 db_error | **高** |
|
||||
|
||||
### 2.2 收敛至2个根本原因
|
||||
|
||||
**根因 #1: 运行中二进制严重过时**
|
||||
- 运行进程 (PID 8300) 使用 `backend/gateway/main`,编译于 **5月17日 21:36**
|
||||
- 当前源码 (`cmd/main.go`, `router.go`, 各 handler) 最后修改于 **5月20日**
|
||||
- 源码中存在 2 个更新的已编译二进制: `cmd/gateway` (5/20) 和 `cmd/main` (5/18),但均未被使用
|
||||
- 运行中二进制缺少: `/files`, `/knowledge`, `/automation`, `/reminders`, `/briefings`, `/voice` 路由 → 全部返回 404
|
||||
- 运行中二进制的 Auth 处理逻辑可能与当前源码完全不同
|
||||
|
||||
**根因 #2: Admin 用户 JWT identity 不匹配**
|
||||
- JWT 负载显示: `"user_id": "user_admin"` (来自实际 token 解码)
|
||||
- 中间件 [`auth.go:45`](backend/gateway/internal/middleware/auth.go:45) 检查: `userID == "admin"`
|
||||
- 当前源码 [`auth_handler.go:149`](backend/gateway/internal/handler/auth_handler.go:149) 在 `authenticated=true` 路径中设置 `userID = "admin"`,但运行中二进制可能走的是不同的路径或旧逻辑
|
||||
- 结果: Admin token 始终被判定为 `is_admin = false` → 所有 `/admin/*` 端点返回 403
|
||||
|
||||
---
|
||||
|
||||
## 3. 测试结果详情
|
||||
|
||||
### 3.1 Part 1: 健康检查
|
||||
|
||||
| # | 端点 | 方法 | 期望 | 实际 | 结果 | 备注 |
|
||||
|---|------|------|------|------|------|------|
|
||||
| 1 | `/health` | GET | 200 | 200 | ✅ PASS | |
|
||||
| 2 | `/health` | HEAD | 200 | 404 | ❌ FAIL | Gin 默认不处理 HEAD;需显式注册或使用 `router.HEAD()` |
|
||||
|
||||
### 3.2 Part 2: 注册 (Register)
|
||||
|
||||
| # | 测试用例 | 输入 | 期望 | 实际 | 结果 | 诊断 |
|
||||
|---|---------|------|------|------|------|------|
|
||||
| 3 | 缺少 username | `{password,email,nickname,verify_code}` | 400 | 400 | ✅ PASS | |
|
||||
| 4 | 缺少 password | `{username,email,nickname,verify_code}` | 400 | 400 | ✅ PASS | |
|
||||
| 5 | username 过短 "ab" (2字符) | `username:"ab"` | 400 | **201** | ❌ FAIL | 绑定 `min=2` 通过;源码 regex `{3,32}` 应拦截,但运行中二进制可能无此 regex 检查 |
|
||||
| 6 | username 过长 (33字符) | `username:"a"*33` | 400 | 400 | ✅ PASS | 绑定 `max=32` 生效 |
|
||||
| 7 | username 含特殊字符 "@!" | `username:"user@name!"` | 400 | **201** | ❌ FAIL | 同上,regex 检查未在运行中二进制中生效 |
|
||||
| 8 | password 过短 "Ab1!" (4字符) | `password:"Ab1!"` | 400 | 400 | ✅ PASS | 绑定 `min=6` 生效 |
|
||||
| 9 | 正常注册 | 完整有效请求 | 201 | 201 | ✅ PASS | |
|
||||
| 10 | 重复注册 | 相同 username | 409 | **201** | ❌ FAIL | `h.db != nil` 检查可能失败或 `GetUserByUsername` 返回 nil |
|
||||
|
||||
**ISSUE-11-001**: Register 输入校验不完整 — username 正则校验和重复用户名检查在运行中二进制中缺失
|
||||
|
||||
### 3.3 Part 3: 登录 (Login)
|
||||
|
||||
| # | 测试用例 | 输入 | 期望 | 实际 | 结果 | 诊断 |
|
||||
|---|---------|------|------|------|------|------|
|
||||
| 11 | 错误用户名 | `username:"nonexistent_user_99"` | 401 | **200** | ❌ FAIL | 返回了有效 JWT `user_id:"user_nonexistent_user_99"` — 旧版 Login 可能无 `verifyUserPassword` 检查 |
|
||||
| 12 | 错误密码 | 正确用户名 + 错误密码 | 401 | **200** | ❌ FAIL | 同上,任意凭据均可登录 |
|
||||
| 13 | 正确登录 | 有效凭据 | 200 | 200 | ✅ PASS | |
|
||||
| 14 | Admin 登录 | `admin/admin123` | 200 | 200 | ✅ PASS | JWT 中 `user_id:"user_admin"` 而非 `"admin"` |
|
||||
| 15 | JWT 刷新 | 有效 token | 200 | 200 | ✅ PASS | |
|
||||
| 16 | 缺少 password | `{username}` | 400 | 400 | ✅ PASS | |
|
||||
| 17 | 空 body | `{}` | 400 | 400 | ✅ PASS | |
|
||||
| 18 | username 格式无效 "ab" | `username:"ab"` | 400 | **200** | ❌ FAIL | Login 也缺少 username 格式校验 |
|
||||
|
||||
**ISSUE-11-002**: Login 端点无实际认证 — 任意不存在的用户名+密码组合均返回 200 + 有效 JWT。这是因为运行中二进制缺少 `verifyUserPassword` 数据库验证逻辑。
|
||||
|
||||
**ISSUE-11-003**: Admin JWT 身份不一致 — Admin 登录后 token 中的 `user_id` 为 `"user_admin"`,而中间件 `IsAdminKey` 检查的是 `"admin"`。需要统一: 要么 Login 返回 `userID = "admin"`,要么中间件接受 `"user_admin"`。
|
||||
|
||||
### 3.4 Part 4: 会话 (Sessions)
|
||||
|
||||
| # | 端点 | 期望 | 实际 | 结果 | 诊断 |
|
||||
|---|------|------|------|------|------|
|
||||
| 19 | GET /sessions (无认证) | 401 | 401 | ✅ PASS | |
|
||||
| 20 | POST /sessions (无认证) | 401 | 401 | ✅ PASS | |
|
||||
| 21 | GET /sessions (有认证) | 200 | **500** | ❌ FAIL | `{"error":"查询会话失败","errorType":"db_error"}` |
|
||||
| 22 | POST /sessions (有认证) | 201 | **500** | ❌ FAIL | `{"error":"创建会话失败","errorType":"db_error"}` |
|
||||
| 23 | GET /sessions/:id (不存在) | 404 | **500** | ❌ FAIL | `{"error":"查询会话失败"}` |
|
||||
| 24 | POST /sessions (空标题) | 201 | **500** | ❌ FAIL | `{"error":"创建会话失败"}` |
|
||||
| 25 | DELETE /sessions/:id (不存在) | 200 | **500** | ❌ FAIL | `{"error":"删除会话失败"}` |
|
||||
| 26 | GET /sessions/:id/messages | 200 | **500** | ❌ FAIL | `{"error":"查询消息失败"}` |
|
||||
|
||||
**ISSUE-11-004**: 会话 API 全部返回 500 db_error — SessionStore 无法连接 PostgreSQL。SessionStore 在启动时初始化失败 (可能),或数据库不可达。
|
||||
|
||||
### 3.5 Part 5-10: 面板 API (文件/知识库/自动化/提醒/简报/通知)
|
||||
|
||||
| # | 端点 | 期望 | 实际 | 结果 | 诊断 |
|
||||
|---|------|------|------|------|------|
|
||||
| 27 | GET /files (无认证) | 401 | **404** | ❌ FAIL | 路由未注册 |
|
||||
| 28 | GET /files (有认证) | 200 | **404** | ❌ FAIL | 路由未注册 |
|
||||
| 29 | GET /files/:id (不存在) | 404 | 404 | ✅ PASS* | 巧合匹配 (Gin 404) |
|
||||
| 30 | GET /knowledge/bases (无认证) | 401 | **404** | ❌ FAIL | 路由未注册 |
|
||||
| 31 | GET /knowledge/bases (有认证) | 200 | **404** | ❌ FAIL | 路由未注册 |
|
||||
| 32 | GET /knowledge/bases/:id (不存在) | 404 | 404 | ✅ PASS* | 巧合匹配 |
|
||||
| 33 | GET /automation/rules (无认证) | 401 | **404** | ❌ FAIL | 路由未注册 |
|
||||
| 34 | GET /automation/rules (有认证) | 200 | **404** | ❌ FAIL | 路由未注册 |
|
||||
| 35 | GET /automation/scenes (有认证) | 200 | **404** | ❌ FAIL | 路由未注册 |
|
||||
| 36 | GET /automation/rules/:id (不存在) | 404 | 404 | ✅ PASS* | 巧合匹配 |
|
||||
| 37 | GET /reminders (无认证) | 401 | **404** | ❌ FAIL | 路由未注册 |
|
||||
| 38 | GET /reminders (有认证) | 200 | **404** | ❌ FAIL | 路由未注册 |
|
||||
| 39 | POST /reminders (缺少字段) | 400 | **404** | ❌ FAIL | 路由未注册 |
|
||||
| 40 | GET /briefings (无认证) | 401 | **404** | ❌ FAIL | 路由未注册 |
|
||||
| 41 | GET /briefings (有认证) | 200 | **404** | ❌ FAIL | 路由未注册 |
|
||||
| 42 | GET /briefings/latest (有认证) | 200 | **404** | ❌ FAIL | 路由未注册 |
|
||||
| 43 | POST /notifications/push (无认证) | 401 | **404** | ❌ FAIL | 路由未注册 |
|
||||
| 44 | POST /notifications/push (空 body) | 400 | **404** | ❌ FAIL | 路由未注册 |
|
||||
| 45 | GET /voice/status (有认证) | 200 | **404** | ❌ FAIL | 路由未注册 |
|
||||
|
||||
**ISSUE-11-005**: 面板 API 路由全部返回 404 — 运行中二进制 (5月17日) 早于 [`router.go`](backend/gateway/internal/router/router.go) 中这些路由的实现 (5月20日)。确认方式: `strings main | grep -E "api/v1/(files|knowledge|automation)"` 返回空。
|
||||
|
||||
### 3.6 Part 11: 记忆 (Memories)
|
||||
|
||||
| # | 端点 | 期望 | 实际 | 结果 | 备注 |
|
||||
|---|------|------|------|------|------|
|
||||
| 46 | GET /memory/search (无认证) | 401 | 401 | ✅ PASS | |
|
||||
| 47 | GET /memory/search?q=test (有认证) | 200 | 200 | ✅ PASS | 正常代理到 memory-service |
|
||||
| 48 | GET /memory (有认证) | 200 | 200 | ✅ PASS | 返回 "数据库连接不可用" (预期行为) |
|
||||
| 49 | POST /memory (缺少字段) | 400 | 400 | ✅ PASS | |
|
||||
|
||||
记忆 API 是唯一完整通过的面板端点,证明 [`memory_handler.go`](backend/gateway/internal/handler/memory_handler.go) 的代理逻辑正确。
|
||||
|
||||
### 3.7 Part 12-13: Admin 端点
|
||||
|
||||
| # | 端点 | 期望 | 实际 | 结果 | 备注 |
|
||||
|---|------|------|------|------|------|
|
||||
| 50 | GET /admin/sessions (admin) | 200 | **403** | ❌ FAIL | `{"error":"需要管理员权限"}` |
|
||||
| 51 | GET /admin/sessions/active (admin) | 200 | **403** | ❌ FAIL | `{"error":"需要管理员权限"}` |
|
||||
| 52 | GET /admin/sessions (普通用户) | 403 | 403 | ✅ PASS | |
|
||||
|
||||
**ISSUE-11-006**: Admin 端点始终返回 403 — `IsAdminKey` 检查 `userID == "admin"`,但 admin token JWT 中实际为 `"user_admin"`。
|
||||
|
||||
### 3.8 Part 14-15: 无效 Token / 刷新
|
||||
|
||||
| # | 端点 | 期望 | 实际 | 结果 | 备注 |
|
||||
|---|------|------|------|------|------|
|
||||
| 53 | GET /sessions (无效 token) | 401 | 401 | ✅ PASS | |
|
||||
| 54 | GET /sessions (过期/畸形 token) | 401 | 401 | ✅ PASS | |
|
||||
| 55 | POST /auth/refresh (无认证) | 401 | 401 | ✅ PASS | |
|
||||
| 56 | POST /auth/refresh (无效 token) | 401 | 401 | ✅ PASS | |
|
||||
|
||||
---
|
||||
|
||||
## 4. 源码级错误处理审计
|
||||
|
||||
### 4.1 [`auth_handler.go`](backend/gateway/internal/handler/auth_handler.go)
|
||||
|
||||
| 行号 | 问题 | 严重度 |
|
||||
|------|------|--------|
|
||||
| 43 | `binding:"min=2"` 与 regex `{3,32}` 不一致 — min 应为 3 | 🟡 中 |
|
||||
| 75 | `if h.db != nil` 包裹重复检查 — db 为 nil 时静默跳过,无警告 | 🔴 高 |
|
||||
| 199-218 | `verifyUserPassword`: 用户不存在和密码错误返回相同的 `(false, nil)` — 无法区分失败原因 | 🟡 中 |
|
||||
| 153-177 | Login fallback 路径: 3个不同分支处理 admin,逻辑复杂易出 bug | 🟡 中 |
|
||||
|
||||
### 4.2 [`session_handler.go`](backend/gateway/internal/handler/session_handler.go)
|
||||
|
||||
| 行号 | 问题 | 严重度 |
|
||||
|------|------|--------|
|
||||
| Delete | **无所有权检查** — 任何认证用户可以删除任何会话 | 🔴 高 |
|
||||
| 全局 | Session 操作失败返回 500 `db_error` 但无重试或降级到 Hub 内存 | 🟡 中 |
|
||||
|
||||
### 4.3 [`reminder_handler.go`](backend/gateway/internal/handler/reminder_handler.go)
|
||||
|
||||
| 行号 | 问题 | 严重度 |
|
||||
|------|------|--------|
|
||||
| List | `user_id` 查询参数为必填,不 fallback 到 JWT 中间件中的 `GetUserID(c)` | 🟡 中 |
|
||||
|
||||
### 4.4 [`auth.go`](backend/gateway/internal/middleware/auth.go)
|
||||
|
||||
| 行号 | 问题 | 严重度 |
|
||||
|------|------|--------|
|
||||
| 45 | `IsAdminKey` 设置为 `userID == "admin"` — 但 Login 返回的 admin userID 是 `"user_admin"` | 🔴 高 |
|
||||
|
||||
### 4.5 [`router.go`](backend/gateway/internal/router/router.go)
|
||||
|
||||
| 行号 | 问题 | 严重度 |
|
||||
|------|------|--------|
|
||||
| 47-53 | Health 端点仅注册 GET — Gin 不自动处理 HEAD 请求,收到 HEAD 返回 404 | 🟡 中 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 问题汇总
|
||||
|
||||
| 编号 | 类别 | 描述 | 严重度 | 状态 |
|
||||
|------|------|------|--------|------|
|
||||
| ISSUE-11-001 | Auth | Register 输入校验不完整 (regex, 重复检查) | 🔴 高 | 已发现 |
|
||||
| ISSUE-11-002 | Auth | Login 无实际认证 (任意凭据通过) | 🔴 严重 | 已发现 |
|
||||
| ISSUE-11-003 | Auth | Admin userID 不一致 ("user_admin" vs "admin") | 🔴 高 | 已发现 |
|
||||
| ISSUE-11-004 | Session | 全部 Session API 返回 500 (DB 不可达) | 🔴 高 | 已发现 |
|
||||
| ISSUE-11-005 | Router | 面板 API 路由未注册 (过时二进制) | 🔴 严重 | 已发现 |
|
||||
| ISSUE-11-006 | Auth | Admin 端点始终返回 403 (权限检查失败) | 🔴 高 | 已发现 |
|
||||
| ISSUE-11-007 | Health | HEAD /health 返回 404 | 🟡 中 | 已发现 |
|
||||
| ISSUE-11-008 | Session | Delete 操作无所有权检查 | 🔴 高 | 代码审计 |
|
||||
| ISSUE-11-009 | Auth | Register binding min=2 与 regex min=3 不一致 | 🟡 中 | 代码审计 |
|
||||
| ISSUE-11-010 | Auth | verifyUserPassword 无法区分"用户不存在"和"密码错误" | 🟡 中 | 代码审计 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 建议修复优先级
|
||||
|
||||
### P0 — 立即修复 (阻塞性问题)
|
||||
|
||||
1. **重新编译并部署最新源代码** — 使用 `cmd/main.go` 最新源码编译,替换运行中的 May 17 二进制。这将解决 ISSUE-11-001, 002, 004, 005, 006。
|
||||
2. **统一 Admin userID** — 在 [`auth_handler.go:149`](backend/gateway/internal/handler/auth_handler.go:149) 确保 admin 用户返回 `userID = "admin"`,与中间件 [`auth.go:45`](backend/gateway/internal/middleware/auth.go:45) 一致。
|
||||
|
||||
### P1 — 高优先级
|
||||
|
||||
3. **为 Session Delete 添加所有权检查**
|
||||
4. **确保数据库在启动时可用** (检查 PostgreSQL 连接状态)
|
||||
5. **Register 绑定 min 改为 3** 与 regex 一致
|
||||
|
||||
### P2 — 中优先级
|
||||
|
||||
6. **Health 端点注册 HEAD handler** 或使用 `router.HEAD()`
|
||||
7. **verifyUserPassword 返回不同错误** 以区分失败原因
|
||||
8. **Reminder List fallback 到 JWT user_id**
|
||||
|
||||
---
|
||||
|
||||
## 7. 测试执行信息
|
||||
|
||||
```
|
||||
测试脚本: debug/cache/test_api_round11.py
|
||||
执行时间: 2026-05-21 14:15 CST
|
||||
总测试数: 56
|
||||
通过: 25
|
||||
失败: 31
|
||||
通过率: 44.6%
|
||||
```
|
||||
|
||||
注意: 测试脚本在 auth API 调用之间添加了 1 秒延迟,以避免触发速率限制器 (5 req/min/IP/endpoint)。
|
||||
|
||||
---
|
||||
|
||||
## 8. 附录: 二进制文件对比
|
||||
|
||||
| 路径 | 大小 | 编译时间 | SHA256 (前16字符) |
|
||||
|------|------|---------|-------------------|
|
||||
| `backend/gateway/main` **(运行中)** | 13.2MB | May 17 21:36 | ebdb2b2723df4e0d |
|
||||
| `backend/gateway/cmd/main` | 14.2MB | May 18 22:49 | 6ebb63e6bdedfb7d |
|
||||
| `backend/gateway/cmd/gateway` | 14.9MB | May 20 13:55 | f689d2d50bf899f2 |
|
||||
|
||||
当前运行的是最旧的二进制 (May 17),缺少后续3天内的所有源码更改。
|
||||
Reference in New Issue
Block a user