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).
127 lines
7.6 KiB
Markdown
127 lines
7.6 KiB
Markdown
# 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 永远发现不了的生产缺陷。
|