Files
danyang-match/docs/Phase-23B-V11-E2E-Infrastructure-Completion-Report-2026-09-18.md
T
WorkBuddy 730dddde30 test(e2e): Phase 23B — rebuild E2E infrastructure; fix 6 real prod defects it exposed
Phase 23 closed as PARTIAL: fake coverage was gone, but the E2E layer itself
was still empty — no conftest.py anywhere, ~15 fixtures that never existed,
6 files crashing at import on os.environ["E2E_PASSWORD"], and CI never ran E2E.
STATUS's "10 E2E files / ~80+ cases ✅" described files that had never executed.

Infrastructure delivered:
- tests/e2e/conftest.py: ApiClient (trust_env=False — a host http_proxy turns
  localhost calls into 502), operator_token(username) / new_user / admin_token
  fixtures, and a session probe that calls pytest.exit(4) when the stack is
  unreachable. NOT skip: skip would launder "did not run" into "passed".
- pytest.ini, tests/e2e/requirements.txt, and tests/e2e/sql/
  seed_e2e_operators.sql — migrations contain NO operator seed at all, so a
  fresh database has zero logins; the seed creates 6 e2e accounts with role
  bindings, idempotent, password injected via `psql -v pwd`.
- CI job `e2e`: compose up -> poll /health/live (timeout = fail) -> seed ->
  pytest -> always teardown. Gate D-23-D is now closed.
- 8 E2E files migrated off import-time env reads; tautological assertions
  (`x == t or not x`), a counselor token standing in for content_editor, and a
  bare `status_code == 200` claiming to prove INV-020 were replaced with real
  permission/behavior assertions.

Six real production defects found only by running against real PostgreSQL
(H2 has zero coverage for all of them):
- D-23B-A (Critical): stale V042__add_appointment_operator_register_permission.sql
  left in target/classes; src had replaced it with V042__add_service_location_
  announcements.sql. Packing without clean put two V042 in the jar -> Flyway
  "Found more than one migration with version 042" -> app would not start.
  Guard added: testBuildOutputHasNoDuplicateMigrationVersions scans build output
  (src is clean, so only this catches it).
- D-23B-B (High): non-UUID path variable reached the DB -> 500 + leaked
  `invalid input syntax for type uuid`. Now rejected at the controller.
- D-23B-C (High): 6 E2E files raised KeyError at import -> never collected.
- D-23B-E (Critical): permissions = 48, role_permissions for super_admin = 18,
  matchmaker = 0. V015's "grant all to super_admin" was a point-in-time snapshot;
  the 30 permissions added later were never granted. On a fresh stack an admin
  cannot create a service station and matchmakers can do nothing. V044 restores
  super_admin to its documented definition (not an expansion); the matchmaker
  permission set needs a product decision and is left as a residual.
- D-23B-I (Medium): IllegalArgumentException (e.g. invalid activity type) was
  unmapped -> 500. Now BAD_REQUEST.
- D-23B-J (High): `operator does not exist: uuid = character varying` — Java
  ids are String, columns are uuid, PostgreSQL does not implicitly cast in
  comparisons. Fixed at the source with JDBC `stringtype=unspecified` (docker
  profile); production datasource comes from deploy env and needs the same.

One judgement retracted: an initial read suggested service_appointment.register
was missing. It is not — the endpoint uses service.assisted_registration, seeded
in V029, and the code deliberately avoids a duplicate permission. Nothing was
changed on that basis.

Verification: 33 E2E passed against a live stack; unreachable stack and a
missing operator password both exit 4 (never skip/pass); backend regression
656 / 0 failures / 0 errors / 3 skipped (baseline 655, +1 guard, negatively
validated by injecting a duplicate V042 -> exactly one test turned red).
2026-09-18 13:50:19 +08:00

127 lines
7.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 永远发现不了的生产缺陷。