Files
danyang-match/docs/Phase-23B-V11-E2E-Infrastructure-Completion-Report-2026-09-18.md
T

127 lines
7.6 KiB
Markdown
Raw Permalink Normal View History

# Phase 23B — E2E 测试基建重建:复验完成报告
- 日期:2026-09-18
- 范围:**补上 E2E 基建**(用户批准:Phase 23 暴露 E2E 层几乎为空后的决定)
- 结论:**E2E 层首次真实可执行**;过程中暴露并修复 6 处真实生产缺陷
- 基线:`v1.0.0-RC.2` / V1.1 KEEP-VERIFY 纪律(历史 ✅ 不可信,必须由运行时证明)
---
## 一、为什么有这一轮
Phase 23 的结论是「部分关闭」:假覆盖归零了,但 **E2E 这一环仍然是空的**——
- 全仓无 `conftest.py`,约 15 个 fixture(`api_client` / `verified_user` / `banner_factory` …)根本不存在;
- 多个文件在 **import 期**就 `os.environ["E2E_PASSWORD"]` → `KeyError` → 收集失败;
- CI 从不跑 E2E(D-23-D)。
即:STATUS 里「10 个 E2E 文件 ~80+ 用例 ✅」描述的,是一批**从未被执行过**的文件。
本轮的目标不是"补几个用例",而是把「能真实执行"这件事本身"建起来:
栈怎么起、账号从哪来、缺栈时怎样失败、CI 怎样阻断。
---
## 二、交付物
| 交付 | 文件 | 作用 |
|---|---|---|
| fixture 体系 | `tests/e2e/conftest.py` | `ApiClient`、`admin_token`、`operator_token(username)`、`new_user`、`live_stack` 探活 |
| pytest 配置 | `pytest.ini` | `testpaths=tests/e2e`、标记 `e2e`、说明为何不收集 `run_v11_tests.py` |
| 依赖清单 | `tests/e2e/requirements.txt` | pytest / requests |
| 运营账号自举 | `tests/e2e/sql/seed_e2e_operators.sql` | 6 个 e2e 账号 + 角色绑定,幂等,密码由 `psql -v pwd` 注入 |
| CI 串联 | `.github/workflows/ci.yml` → job `e2e` | 起真实栈 → 等就绪 → seed → pytest → 失败阻断 → 清理 |
| 8 个测试文件改造 | `tests/e2e/*_test.py` | 消除 import 期崩溃;把恒真/名不副实的断言换成真实断言 |
### conftest 的三条硬设计(写进代码注释,防止回退)
1. **栈不可达 = `pytest.exit(4)`,不是 `skip`。**
`skip` 会把"未执行"伪装成"通过"——那正是 Phase 23 清掉的 44 个 `pass` 空壳的另一种变体。
2. **`session.trust_env = False`。**
宿主机若设了 `http_proxy`,requests 会把 localhost 请求也发给代理并返回 502,
表现为"连接失败"而不是真实业务结果(本机实测踩到)。
3. **用户隔离。** mock 微信登录同 code 同 openid,换 code 即换人;每个用例用独立新用户。
---
## 三、本轮发现并修复的真实生产缺陷
> 这些不是"测试问题",是**起真栈才暴露**的生产缺陷。H2 测试库全部零覆盖。
| 编号 | 级别 | 真相 | 处置 |
|---|---|---|---|
| **D-23B-A** | Critical | `backend/target/classes/db/migration/` 残留旧 `V042__add_appointment_operator_register_permission.sql`(src 中已被 `V042__add_service_location_announcements.sql` 取代)。未 clean 打包 → jar 内两个 042 → Flyway 抛 `Found more than one migration with version 042` → **应用起不来** | 清理残留;新增守卫 `testBuildOutputHasNoDuplicateMigrationVersions`(扫构建产物,src 干净也扫得出) |
| **D-23B-B** | High | `GET /counseling/services/{id}` 传入非 UUID → PostgreSQL 抛 `invalid input syntax for type uuid` → **500 且泄漏数据库异常** | Controller 层 UUID 格式校验 → `BAD_REQUEST` |
| **D-23B-C** | High | 6 个 E2E 文件 import 期 `os.environ["E2E_PASSWORD"]` → `KeyError` → 收集失败,**从未执行** | 全部迁移到 conftest fixture |
| **D-23B-E** | Critical | 权限 seed 缺口:permissions 共 **48** 条,`super_admin` 只有 **18** 条,`matchmaker` **0** 条。V015 的"为 super_admin 分配全部权限"是**执行时点快照**,之后新增的 30 条从未回补 → 全新环境下管理员创建服务站 403、红娘全部功能 403 | 新增 `V044__grant_all_permissions_to_super_admin.sql`(对齐 super_admin 定义,非扩权)。**matchmaker 权限集需产品定义,列为遗留** |
| **D-23B-I** | Medium | `ActivityService` 对非法活动类型抛 `IllegalArgumentException`,全局异常处理器未映射 → **500**(客户端参数错被伪装成服务端故障) | `GlobalExceptionHandler` 增加 `IllegalArgumentException → BAD_REQUEST` |
| **D-23B-J** | High | `operator does not exist: uuid = character varying`:Java 实体主键/外键统一用 `String`,PostgreSQL 列是 `uuid`,比较操作不做隐式转换 → 多处查询 500(`/service-appointments/my` 等)。**H2 不报错 → 零覆盖** | docker profile JDBC URL 增加 `?stringtype=unspecified`(pgjdbc 官方解法,一处解决全局) |
### 一处"判断被证伪",如实撤回
初查时发现 `V042__add_appointment_operator_register_permission.sql` 的内容
(`service_appointment.register` 权限)不在 src 里,一度判定为"功能缺失"。
进一步核实:端点实际用的是 `service.assisted_registration`(V029 已 seed 并授予
服务站运营/平台管理员),Controller 注释明确写着"刻意收敛,避免权限膨胀"。
→ **该判断不成立,已撤回**,未据此改动任何代码。
---
## 四、验证证据
### 1. E2E 真实执行(本机活栈:postgres + redis + 当前代码 jar @8085)
```
33 passed in 3.21s
```
覆盖:首页 8 入口固定顺序(INV-017)、Banner 禁外链(INV-016)、本地资格声明与
用户隔离(INV-011)、咨询正文权限(INV-012/013)、内容编辑员边界(INV-019)、
服务站/咨询师越权(INV-018/020)、活动类型筛选与非法值、文章生命周期、咨询取消。
### 2. 基建自身的负向属性("未执行"不会被算成"通过")
| 场景 | 结果 |
|---|---|
| `E2E_BASE_URL` 指向未监听端口 | `pytest.exit`,**退出码 4**,输出明确提示 |
| 未设置 `E2E_OPERATOR_PASSWORD` | `pytest.exit`,退出码 4 |
### 3. 后端全量回归
```
Tests run: 656, Failures: 0, Errors: 0, Skipped: 3 BUILD SUCCESS
```
基线 655(Phase 23 后),净 +1 为新增的构建产物守卫 `testBuildOutputHasNoDuplicateMigrationVersions`。
3 个 skip 均有显式原因(2× `@EnabledIfEnvironmentVariable(CI=true)`、1× `@Disabled("需要完整数据库连接")`),无一为空壳。
**守卫负向验证**:向 `target/classes/db/migration` 注入一个重复的 `V042__*.sql` →
恰好 `testBuildOutputHasNoDuplicateMigrationVersions` 变红(报"版本号 42 重复"),
其余 12 个用例仍绿;移除注入文件后恢复全绿。
---
## 五、遗留(未静默扩权)
1. **matchmaker 角色 0 权限**(D-23B-E 一半):红娘该拥有哪些权限属产品定义,
本轮不擅自授予;转介类 E2E 场景因此仍无法正向执行(已在文件中注明,未写假用例)。
2. **NPE → 500 的跨模块根因**:`@AuthenticationPrincipal UserPrincipal` 在
operator token 下为 `null`,部分端点未做 null 检查 → 500(应 401/403)。
本轮只修了被 E2E 命中的路径,其余建议统一处理。
3. **`run_v11_tests.py` 未被 pytest 收集**:它在 import 期登录、依赖
`docker exec` 取库内 ID,是"人工跑的活栈脚本"。其 16 个场景应逐步迁移进
fixture 体系。
4. **生产环境 JDBC**:`stringtype=unspecified` 本次只改了 docker profile;
生产的 datasource 由部署环境变量提供,需同步加上(运维项)。
5. **CI 首次运行成本**:`e2e` job 需构建 backend 镜像(Dockerfile 内联网构建),耗时较长。
---
## 六、结论
Phase 23B **关闭**。
E2E 从"一批从未执行过的文件 + 一个不存在的 fixture 体系",变为
「**有栈就真跑、没栈就退出、CI 会阻断**」的可执行基建;
顺带用真实 PostgreSQL 挖出 6 个 H2 永远发现不了的生产缺陷。