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).
7.6 KiB
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 的三条硬设计(写进代码注释,防止回退)
- 栈不可达 =
pytest.exit(4),不是skip。skip会把"未执行"伪装成"通过"——那正是 Phase 23 清掉的 44 个pass空壳的另一种变体。 session.trust_env = False。 宿主机若设了http_proxy,requests 会把 localhost 请求也发给代理并返回 502, 表现为"连接失败"而不是真实业务结果(本机实测踩到)。- 用户隔离。 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 个用例仍绿;移除注入文件后恢复全绿。
五、遗留(未静默扩权)
- matchmaker 角色 0 权限(D-23B-E 一半):红娘该拥有哪些权限属产品定义, 本轮不擅自授予;转介类 E2E 场景因此仍无法正向执行(已在文件中注明,未写假用例)。
- NPE → 500 的跨模块根因:
@AuthenticationPrincipal UserPrincipal在 operator token 下为null,部分端点未做 null 检查 → 500(应 401/403)。 本轮只修了被 E2E 命中的路径,其余建议统一处理。 run_v11_tests.py未被 pytest 收集:它在 import 期登录、依赖docker exec取库内 ID,是"人工跑的活栈脚本"。其 16 个场景应逐步迁移进 fixture 体系。- 生产环境 JDBC:
stringtype=unspecified本次只改了 docker profile; 生产的 datasource 由部署环境变量提供,需同步加上(运维项)。 - CI 首次运行成本:
e2ejob 需构建 backend 镜像(Dockerfile 内联网构建),耗时较长。
六、结论
Phase 23B 关闭。
E2E 从"一批从未执行过的文件 + 一个不存在的 fixture 体系",变为 「有栈就真跑、没栈就退出、CI 会阻断」的可执行基建; 顺带用真实 PostgreSQL 挖出 6 个 H2 永远发现不了的生产缺陷。