# 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 永远发现不了的生产缺陷。