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

7.6 KiB
Raw Permalink Blame 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 永远发现不了的生产缺陷。