feat(e2e): Phase 23 Task 2.1 —— 活动互选 8 场景 + 服务预约 8 场景真实实现(16 个真实用例)
背景:`tests/e2e/activity_mutual_consent_test.py` 与 `service_appointment_test.py` 此前是 **诚实占位**(仅 docstring、0 用例,指针指向 run_v11_tests.py),计划 Task 2.1 要求把 文件头保留的场景清单逐条实现为真实 HTTP 用例。本提交完成该任务。 ═══ 新增真实用例 16 个(本机活栈 pytest tests/e2e 49 → 65 passed)═══ activity_mutual_consent_test.py(8,覆盖 INV-014/015/006 + AGENTS.md #19/#20) 1 建立互选记录不建 Match(INV-014) 2 单方确认不建 Match(INV-015) 3 双方确认建 Match Episode 且双方可见 4 已有 active Match 时再次双确认必须复用(#19) 5 曾 Unmatch 后重新双确认开启新 Episode:episode +1(#20) 6 一方拒绝不建 Match,对方随后确认也不建 7 超时:已过期记录不得被推动成双确认(含「未过期必须生效」对照,防假绿) 8 单向意向不泄露(INV-006,含「B 确认后必须看到 accepted」反证) service_appointment_test.py(8) 1 提交预约;2 幂等键重放不得产生第二条;3 运营确认 submitted→confirmed(用户侧同态可见) 4 并发「用户取消 × 运营确认」不得 5xx、最终状态必须收敛;5 我的列表只含本人 6 待确认可取消;7 归属隔离(他人不可读详情/不可取消) 8 运营侧状态闸门:已完成预约不得再被取消 ═══ 两个幂等 seed(E2E 前置数据,本机与 CI 通用)═══ - seed_e2e_activity.sql:organization + 两个 activity。 必须建的原因是硬约束而非选择:`activity_mutual_connection_consents.activity_id` 有 **FK 到 activities(id)**(实测约束名 …_activity_id_fkey),而 activities.organizer_org_id 又 FK 到 organizations(id) 且 NOT NULL —— 全新库里三张表全空,互选无活动可挂。 - seed_e2e_service_station.sql:service_location + **站点-运营关联**。 关联必需:运营确认受数据范围约束(`assertOperatorScope` → `canAccessByOperator`), super_admin **不豁免**,实测 e2e_admin 确认他站预约返回 403。 E2E 因此改用 seed 中该站点关联的 e2e_station。 两个 seed 由测试首用时幂等执行(docker exec psql),无需改 CI 步骤。 ═══ 负向注入(Task 2.1 Step 3)═══ ③ 撤销 service_station_operator 的 service_appointment.confirm → 恰 2 红(test_operator_can_confirm / test_operator_cannot_cancel_completed,403),其余 6 绿; ④ 触发器强制新建互选记录的对方侧 accepted → 3 红(both_confirm_creates_match_episode / timeout_handled / single_interest_not_leaked),其余 5 绿。 两处均已还原并复跑全绿(65 passed),确认无残留(无 e2e* 触发器、权限计数回到 1)。 **两次失败的注入(记录以免后人重复踩)**: - 删除站点→运营关联:**无效** —— 测试的 seed fixture 在用例前幂等重放 seed 会自愈该前置条件; - 改 user_b_status 列默认值:**无效** —— `createMutualConsent` 在 Java 侧显式写 pending, DB 默认值根本不参与(侧面证明写入路径的权威性)。 ═══ 计划文档校正(审阅产物)═══ 按实测推翻 4 处失实断言,登记 7 项新发现(详见计划文档「附 A / 附 B」): - 推翻:matchmaker_services「无表/需扩权」(实为 V009 已建表且 mapper 齐备); 门禁 14/14(实为 18/18);「tests/e2e 现为占位」(实为仅 2 个文件); E2E 基线 49(实为 65)。 - 新发现:① `POST /operator/organizations` 必然 500(createOrganization 不赋 id + IdType.INPUT + 无 DB 默认值;且未校验 chk_org_type)→ 连带「无法新建活动」; ② `createMutualConsent` 从不写 expires_at → 互选永不过期(即批 3 P16-T13,配置与 V041 部分索引空转);③ 用户侧取消预约不校验状态(已确认/已完成也能自取消), 与运营侧明确拒绝 completed 口径不一致 → 需产品裁定; ④ E2E 环境 organizations/activities/service_locations 均为空表(无迁移种子); ⑤ 运营数据范围 super_admin 不豁免(预期行为,勿误判); ⑥ 本机复跑会遇到 Redis 登录锁定键(auth:operator_login_lock:*); ⑦ e2e_* 账号因 ON CONFLICT DO NOTHING 重跑 seed 不覆盖旧口令。 ═══ 验证 ═══ - `pytest tests/e2e` → **65 passed**(含新增 16);`node scripts/run-ui-contracts.mjs` → **18/18 PASS**,退出 0; - 本机环境处置(不属代码改动):把 6 个 e2e_* 账号口令对齐到 CI 口令、清除 Redis 登录锁定键。 - **未验证**:微信开发者工具、CI 上的首次 E2E 实测绿(Task 2.3 Step 3,本机无 Docker Hub 出口)。
This commit is contained in:
@@ -18,11 +18,11 @@
|
||||
| 项 | 状态 |
|
||||
|---|---|
|
||||
| Phase 01~22 | ✅ 复验关闭 |
|
||||
| Phase 23 | ⚠️ **部分关闭** —— 44 个 `pass` 空壳已清、4 个真实 INV 行为守卫已补;**E2E 场景实现与 CI 串联未完成** |
|
||||
| Phase 23B | ✅ E2E 测试基建重建(conftest + seed + CI e2e job,本机活栈 33→49 passed) |
|
||||
| Phase 23 | ⚠️ **部分关闭** —— 44 个 `pass` 空壳已清、4 个真实 INV 行为守卫已补;**E2E 场景实现已完成(2026-09-19,见 Task 2.1)**,仅余 E2E 在 CI 上的首次实测绿 |
|
||||
| Phase 23B | ✅ E2E 测试基建重建(conftest + seed + CI e2e job,本机活栈 49 → **65 passed**) |
|
||||
| 横向加固 | ✅ HTTP 语义(105 处真实状态码)+ 身份收敛 + 幂等键(30 端点,V045/V046)+ 端口/入口守卫 |
|
||||
| 后端回归 | **669 / 0 failures / 0 errors / 3 skipped** |
|
||||
| UI 契约门禁 | **14/14 绿** |
|
||||
| UI 契约门禁 | **18/18 绿**(原文 14/14 已过期,见 Global Constraints) |
|
||||
| 停点 | **Phase 24(UAT)启动门前 —— 需用户确认** |
|
||||
|
||||
> ✅ 批 2 的 E2E 项**已在本机真实执行**(2026-09-18:`pytest tests/e2e` → 49 passed,真栈 + 真库 + 当日代码),
|
||||
@@ -35,8 +35,11 @@
|
||||
|
||||
- **禁止用 `rm` / `git rm` 删除文件** —— 本机有 safe-delete 钩子会把「删文件」放大为「删父目录」(2026-09-18 实测:`git rm` 单个文件导致整个 `docs/` 被移除)。需要移除内容时**一律原地重写**(Write/Edit)。
|
||||
- 不修改业务规则、不扩大 V1 范围;新增能力必须走 Phase 顺序。
|
||||
- 每批完成后跑 `node scripts/run-ui-contracts.mjs`(14 个守卫,必须 **14/14 绿**;已接入 CI job `ui-contracts`)+ `ui-document-baseline-contracts.mjs`(含端口与 `/admin/` 子路径断言)。
|
||||
- 每批完成后跑 `node scripts/run-ui-contracts.mjs`(**守卫数量以执行器 `MIN_GUARDS` 为准;2026-09-19 实为 18 个,须 18/18 绿** —— 原文「14 个 / 14/14」已过期:2026-09-19 起相继新增 `wxml-structure-contracts`、`miniapp-self-method-contracts`、`wxss-component-selector-contracts`、`miniapp-api-client-behavior`)+ `ui-document-baseline-contracts.mjs`(含端口与 `/admin/` 子路径断言)。
|
||||
- 后端改动跑离线容器 `mvn -o test`,必须 0 failures / 0 errors。
|
||||
- **E2E 改动跑 `pytest tests/e2e`**(需活栈;本机 `E2E_BASE_URL=http://localhost:8085/api/v1`、`E2E_OPERATOR_PASSWORD` 见 CI 变量),必须 0 failed。
|
||||
本机复跑前若出现 `429 账号已锁定`,是 Redis 里的登录失败锁定键(`auth:operator_login_lock:<operatorId>`,TTL 15 分钟),
|
||||
由前一轮失败登录累积触发 —— 删掉该键即可,不是业务缺陷。
|
||||
- 提交纪律:每批独立 commit + `git tag`(**单段名**,带 `/` 会被本机常驻进程清理)+ 回读验证;`git bundle create` 覆盖最新提交。
|
||||
- 涉及密码/密钥:不写入文档,只用环境变量或安全 Seed。
|
||||
|
||||
@@ -74,29 +77,53 @@
|
||||
|
||||
## 批 2 — Phase 23 剩余(核心阻塞)
|
||||
|
||||
### Task 2.1: E2E 场景真实实现
|
||||
### Task 2.1: E2E 场景真实实现 ✅(2026-09-19 完成)
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/e2e/**`(现为占位文件,场景清单已完整保留在各文件头部)
|
||||
- Modify: `tests/e2e/activity_mutual_consent_test.py`、`tests/e2e/service_appointment_test.py`
|
||||
- Add: `tests/e2e/sql/seed_e2e_activity.sql`、`tests/e2e/sql/seed_e2e_service_station.sql`
|
||||
- Test: 本地活栈 `pytest tests/e2e`
|
||||
|
||||
> **前置状态修正(2026-09-19 实测)**:原文「现为占位文件」只对上述**两个**文件成立 ——
|
||||
> 它们在 2026-09-18 的 KEEP-VERIFY 轮已从「44 个 `pass` 空壳」改为**诚实占位**(仅 docstring、
|
||||
> 0 个用例)。其余文件(`home_v11_test.py` / `local_eligibility_test.py` / `counseling_*` /
|
||||
> `http_semantics_test.py` / `idempotency_test.py` 等)在 Phase 23B 已是**真实用例**,不是占位。
|
||||
|
||||
**场景清单(占位文件头部已列,不得缩减):**
|
||||
|
||||
| 文件 | 场景数 | 覆盖 |
|
||||
|---|---|---|
|
||||
| 首页聚合 | 11 | 首页 8 入口顺序、Banner 仅内部跳转、今日推荐、公益服务、安全提醒 |
|
||||
| 本地资格 | 11 | 声明 → 佐证上传 → 审核 → 状态流转;INV-011 未授权不可见 |
|
||||
| 互选 | 8 | 二阶段同意:互选结果 → 双方分别确认 → 创建 Match;已 Match 复用 / 已 Unmatch 新建 |
|
||||
| 权限 | 10 | 各角色数据范围(Counselor Assigned / ContentEditor 零用户数据 / 站点点范围) |
|
||||
| 预约 | 7 | 用户创建/详情/列表/取消仅本人;代登记走独立 operator 接口 |
|
||||
| 内容 | — | 婚恋课堂 / 反诈列表与详情 |
|
||||
| 组织隔离 | — | 跨组织默认拒绝 |
|
||||
| 文件 | 场景数 | 覆盖 | 实现状态 |
|
||||
|---|---|---|---|
|
||||
| 首页聚合 | 11 | 首页 8 入口顺序、Banner 仅内部跳转、今日推荐、公益服务、安全提醒 | 分散在 `home_v11_test.py` 等(场景编号与用例数非一一对应,未逐条对账) |
|
||||
| 本地资格 | 11 | 声明 → 佐证上传 → 审核 → 状态流转;INV-011 未授权不可见 | 分散在 `local_eligibility_test.py` 等(同上) |
|
||||
| 互选 | 8 | 二阶段同意:互选结果 → 双方分别确认 → 创建 Match;已 Match 复用 / 已 Unmatch 新建 | ✅ **`activity_mutual_consent_test.py` 8/8 通过** |
|
||||
| 权限 | 10 | 各角色数据范围(Counselor Assigned / ContentEditor 零用户数据 / 站点点范围) | 分散在 `counseling_permission_test.py` / `service_station_permission_test.py` / `organization_isolation_test.py` |
|
||||
| 预约 | 7 | 用户创建/详情/列表/取消仅本人;代登记走独立 operator 接口 | ✅ **`service_appointment_test.py` 8/8 通过**(原「已确认不可自行取消」一条见下方说明,改为有规格支撑的归属隔离 + 运营侧状态闸门两条) |
|
||||
| 内容 | — | 婚恋课堂 / 反诈列表与详情 | `content_e2e_test.py` |
|
||||
| 组织隔离 | — | 跨组织默认拒绝 | `organization_isolation_test.py` |
|
||||
|
||||
- [ ] **Step 1:** 逐文件实现场景(**先删除空 `pass`**,禁止保留空壳)。
|
||||
- [ ] **Step 2:** 本地活栈跑通(需后端 + PostgreSQL + 种子数据 + `E2E_PASSWORD`)。
|
||||
- [ ] **Step 3:** 负向验证:注入 1~2 处缺陷,确认对应 E2E 变红。
|
||||
- [x] **Step 1:** 逐文件实现场景(**先删除空 `pass`**,禁止保留空壳)。两文件共 16 个真实用例,`pass` 空壳为 0。
|
||||
- [x] **Step 2:** 本地活栈跑通(需后端 + PostgreSQL + 种子数据 + `E2E_PASSWORD`)。
|
||||
本机实测 `pytest tests/e2e` → **65 passed**(49 基线 + 16 新增)。新增两个幂等 seed 文件承载
|
||||
「活动互选」与「服务站/站点-运营关联」的前置数据,本机与 CI 通用。
|
||||
- [x] **Step 3:** 负向验证:注入 2 处缺陷,确认对应 E2E 变红(详见下方「负向注入记录」)。
|
||||
|
||||
**Acceptance:** 场景全部真实执行且通过;无空 `pass` 残留。
|
||||
**Acceptance:** 场景全部真实执行且通过;无空 `pass` 残留。✅
|
||||
|
||||
**负向注入记录(Step 3,2026-09-19 实测)**
|
||||
|
||||
| # | 注入 | 期望 | 实测 |
|
||||
|---|---|---|---|
|
||||
| ③ | 撤销 `service_station_operator` 的 `service_appointment.confirm` | 运营确认类用例变红 | **恰 2 个红**(`test_operator_can_confirm` / `test_operator_cannot_cancel_completed`,HTTP 403),其余 6 绿 |
|
||||
| ④ | 触发器强制新建互选记录的对方侧为 `accepted` | 依赖「对方未确认」的用例变红 | **3 个红**(`test_both_confirm_creates_match_episode` / `test_timeout_handled` / `test_single_interest_not_leaked`),其余 5 绿 |
|
||||
|
||||
两处注入均已还原并复跑全绿(65 passed),且确认无残留(无 `e2e*` 触发器、权限计数回到 1)。
|
||||
|
||||
**两次失败注入(记录在案,避免后人重复踩)**
|
||||
|
||||
- 注入①「删除站点→运营关联」**无效**:测试的 seed fixture 会在用例前幂等重放 seed,
|
||||
把关联补回来 —— 即 fixture 会自愈它自己的前置条件。要注入必须选 fixture **不会修复**的东西。
|
||||
- 注入②「把 `user_b_status` 列默认值改成 `accepted`」**无效**:`createMutualConsent`
|
||||
在 Java 侧**显式写入** `pending`,DB 默认值根本不参与。这条侧面证明了写入路径的权威性。
|
||||
|
||||
### Task 2.2: INV-011 显式守卫
|
||||
|
||||
@@ -106,9 +133,29 @@
|
||||
|
||||
**背景**:`LocalEligibilityService` 全部方法以 `userId` 自限定、零 role 分支,佐证无独立服务端读取入口(仅经受权限控制的审核队列)—— **结构性满足,但无测试固化**。
|
||||
|
||||
**2026-09-19 实测落点(照此实现即可,不必重新勘察)**
|
||||
|
||||
- 当前唯一服务端读取入口:`POST /api/v1/operator/reviews/{taskId}/evidences/{evidenceId}/access`
|
||||
(`ReviewController:165`),**两级 gate**:类级 `hasAuthority('review.view')`
|
||||
+ 方法级 `hasAuthority('local_eligibility.evidence.view')`。
|
||||
- 业务侧 `ReviewService.accessLocalEligibilityEvidence(taskId, evidenceId, operatorId, purpose)` 在
|
||||
权限注解之外**再校验四件事**:`purpose` 非空 → 任务是 `local_eligibility` 类型 →
|
||||
`requireClaimOwner`(领取人本人)→ 佐证归属该任务用户的资格记录。
|
||||
**成功与拒绝都会写** `sensitive_access_logs`(`logEvidenceAccess`,`result = success|denied`)——
|
||||
这正是「可核查」的落库路径,也是负向断言应该咬住的地方。
|
||||
- 用户侧入口 `/local-eligibility/{declaration,evidences,submit}` 全部以 `principal.id()` 自限定,无 userId 入参。
|
||||
- 佐证上传前置:`featureFlags.isLocalEvidenceUploadEnabled()`、`evidenceType` 白名单、
|
||||
`fileId` 必须属于该用户自己的 `user_photos`(`userPhotoMapper.findByUserIdOrderBySort`)。
|
||||
- 测试基建:`@SpringBootTest @ActiveProfiles("test")`(H2,`schema-h2.sql`),参照 `V11InvariantsTest`。
|
||||
|
||||
- [ ] **Step 1:** 写负向测试:非授权角色读取他人本地资格佐证 → 必须 403/不可达。
|
||||
- [ ] **Step 2:** 确认真实落库路径(不是只断言常量,见 `CoreInvariantsTest` 的教训)。
|
||||
(建议同时覆盖:非领取人、跨用户佐证、非 local_eligibility 任务、`purpose` 为空)
|
||||
- [ ] **Step 2:** 确认真实落库路径(不是只断言常量,见 `CoreInvariantsTest` 的教训)——
|
||||
即断言 `sensitive_access_logs` 真的新增了对应 `result` 的记录。
|
||||
- [ ] **Step 3:** 负向注入验证守卫变红。
|
||||
⚠️ 本机后端跑的是**当日构建的 jar**,改 Java 代码需重建镜像;注入应优先选
|
||||
**不需要重建**的手段(如撤销 `local_eligibility.evidence.view` 的角色授予、
|
||||
或直接改库内状态),否则该步骤成本极高。
|
||||
|
||||
**Acceptance:** INV-011 有真实行为守卫。
|
||||
|
||||
@@ -166,7 +213,19 @@
|
||||
### Task 3.1: Phase 21 遗留
|
||||
|
||||
- [ ] **隐私设置接口**:`getPrivacySettings` / `updatePrivacySettings` 仍是 **TODO 桩**(返回假数据 / 不更新)→ 接真实 `user_privacy_settings`。
|
||||
- [ ] **注销红娘服务拦截硬编码 `0`**:根因是数据模型**没有 `matchmaker_services` 表**(全仓搜索为空),拦截项在设计上无法落地。**建表属扩权** → 需产品先确认红娘服务的存储模型。
|
||||
- [ ] **注销红娘服务拦截硬编码 `0`**:
|
||||
原文断言「数据模型**没有 `matchmaker_services` 表**(全仓搜索为空),建表属扩权,需产品先确认」——
|
||||
**三条均与实况不符,2026-09-19 实测推翻**:
|
||||
① 表**存在**,由 `V009__add_matchmaker_tables.sql` 创建(`chk_matchmaker_status` 白名单:
|
||||
`submitted / assigned / in_service / waiting_user / completed / cancelled`);
|
||||
② `MatchmakerServiceMapper`、`MatchmakerServiceRecord`、`MatchmakerServiceService` **均已存在**;
|
||||
③ 因此**不需要扩权、不需要产品裁定**,属可直接实现的接线缺陷。
|
||||
另:`AccountLifecycleService.notifyMatchmaker`(注销时通知红娘服务)同样是 TODO 空转。
|
||||
位置:`AccountLifecycleService.checkCanWithdraw` 中
|
||||
`long activeMatchmakerServices = 0; // 需要 MatchmakerServiceMapper`。
|
||||
(原文「全仓搜索为空」很可能是只在 `db/migration/` 内搜了表名的另一种写法 —— 该表在
|
||||
`demo-seed-*.sql` 与 `V009` 中都有;凡「全仓无 X」这类断言必须给出**搜索命令与范围**,
|
||||
否则无法复核,本项目已多次因此产生失实结论。)
|
||||
- [ ] **数据保留流程**:按 `docs/legal/privacy-policy.md` 与 `docs/security/PIA.md` 落实保留期限(**不得擅自永久保存或删除**)。
|
||||
|
||||
### Task 3.2: Phase 22 遗留(死配置)
|
||||
@@ -215,12 +274,64 @@
|
||||
|
||||
## 验收与提交纪律
|
||||
|
||||
1. 批 1~3 每批完成后:`ui-tests/*.mjs` **14/14 绿** + `ui-document-baseline-contracts.mjs` 通过 + 后端 `mvn -o test` 0 failures。
|
||||
2. 批 2 的**端到端前置已满足**(2026-09-18 本机 E2E 49 passed);仍缺的是 **E2E 进 CI 的首次实测绿**(Task 2.3 Step 3)—— 不阻塞 UAT 启动,属回归防线缺口。
|
||||
1. 批 1~3 每批完成后:`ui-tests/*.mjs` **18/18 绿**(数量以执行器 `MIN_GUARDS` 为准)+ `ui-document-baseline-contracts.mjs` 通过 + 后端 `mvn -o test` 0 failures + `pytest tests/e2e` 0 failed。
|
||||
2. 批 2 的**端到端前置已满足**(2026-09-19 本机 `pytest tests/e2e` → **65 passed**);仍缺的是 **E2E 进 CI 的首次实测绿**(Task 2.3 Step 3)—— 不阻塞 UAT 启动,属回归防线缺口。
|
||||
3. 每批独立 commit;`git tag` 用**单段名**;`git bundle create` 覆盖最新提交并回读验证。
|
||||
4. `docs/STATUS.md` / `docs/JIRA_BACKLOG.md` / `docs/善缘丹阳 V1.1 客户需求基线增量开发与修复计划.md` 随批同步。
|
||||
5. **不做**:不修改 Recovery 报告与 `releases/` 冻结快照;不触碰 bundle 自保产物。
|
||||
|
||||
---
|
||||
|
||||
## 附:2026-09-19 审阅校正与新发现
|
||||
|
||||
### A. 校正(本文件中已被实测推翻的断言)
|
||||
|
||||
| 位置 | 原文 | 实测 |
|
||||
|---|---|---|
|
||||
| Task 3.1 第 2 项 | `matchmaker_services` 表不存在、建表属扩权、需产品确认 | 表由 **V009** 创建;mapper/entity/service 齐备;**无需扩权**,可直接接线 |
|
||||
| Global Constraints / 验收纪律 / 当前停点 | UI 契约门禁 14 个、14/14 绿 | **18 个**(2026-09-19 相继新增 4 项守卫),基线 18/18 |
|
||||
| Task 2.1 | `tests/e2e/**` 现为占位文件 | **仅 2 个文件**是占位;其余在 Phase 23B 已是真实用例 |
|
||||
| 当前停点 | 本机活栈 E2E `49 passed` | 基线 49,Task 2.1 完成后 **65 passed** |
|
||||
|
||||
### B. 新发现(不在本计划内,需另行处置)
|
||||
|
||||
1. **`POST /operator/organizations` 必然 500**(High)
|
||||
`OrganizationService.createOrganization` 从不给 `id` 赋值,而 `BaseEntity` 用
|
||||
`@TableId(type = IdType.INPUT)`、`organizations.id` 无 DB 默认值 →
|
||||
`invalid input syntax for type uuid: ""`。且该接口**未校验 `type`**,
|
||||
会撞生产 `chk_org_type`(白名单:`women_federation / trade_union / charity_center /
|
||||
volunteer_team / civil_affairs_partner / other`),大概率在 PG 上再炸一次。
|
||||
影响:运营端无法新建机构 → 无机构则**无法新建活动**(`activities.organizer_org_id` NOT NULL)。
|
||||
2. **互选记录永不过期**(Medium,即批 3 Task 3.3 的 P16-T13)
|
||||
`createMutualConsent` 从不写 `expires_at`(列无默认值),而
|
||||
`confirmSideIfPending` 的过期闸门 `expires_at IS NULL OR expires_at > NOW()`
|
||||
对 NULL 恒为真 → 配置项 `default-mutual-deadline-hours: 48` 与 V041 的部分索引
|
||||
`idx_mutual_consents_expiry` 均为**空转**。E2E 的超时用例因此只能靠回拨库内值触发。
|
||||
3. **用户侧取消预约不校验状态**(Medium,需产品裁定)
|
||||
`ServiceAppointmentService.cancelAppointment` 只校验归属,**已确认/已完成**的预约
|
||||
都能被本人置为 `cancelled`;而运营侧 `cancelAppointmentByOperator` 明确拒绝
|
||||
`completed` / `cancelled`。两侧口径不一致。原空壳用例名
|
||||
`test_cancel_after_confirmation_denied` 无任何规格支撑(需求基线只写了归属隔离),
|
||||
故未被写成断言 —— 待产品确认后再定。
|
||||
4. **E2E 环境缺前置数据**(Low,已由本轮 seed 解决)
|
||||
全新库 `organizations` / `activities` / `service_locations` **均为空表**——
|
||||
无任何迁移种子。已补 `tests/e2e/sql/seed_e2e_activity.sql`、
|
||||
`tests/e2e/sql/seed_e2e_service_station.sql`(幂等,本机与 CI 通用)。
|
||||
5. **运营确认受数据范围约束,super_admin 不豁免**(设计事实,易被误判为缺陷)
|
||||
`assertOperatorScope` → `canAccessByOperator`:
|
||||
`sa.assigned_operator_id = :op OR EXISTS(service_location_operators 关联该站点 × :op)`。
|
||||
实测 `e2e_admin`(super_admin)确认他站预约返回 **403** —— 这是**预期行为**,
|
||||
不是权限配置错误。E2E 因此必须用站点关联的运营账号。
|
||||
6. **本机 E2E 复跑会被登录锁定拦住**(Low,环境事项)
|
||||
Redis 键 `auth:operator_login_lock:<operatorId>`(TTL 15 分钟)由失败登录累积触发,
|
||||
表现为 `429 账号已锁定,请 N 分钟后重试`。清键即可。CI 每轮全新 Redis 不受影响。
|
||||
7. **`e2e_*` 账号口令与 CI 不一致**(Low,环境事项)
|
||||
`seed_e2e_operators.sql` 用 `ON CONFLICT (username) DO NOTHING`,重跑 seed **不覆盖**旧口令;
|
||||
本机活栈若复用旧卷,口令会停在上一轮的值 → 大量 401。
|
||||
本机已用 `UPDATE operators SET password_hash = crypt('<CI 口令>', gen_salt('bf',8)) WHERE username LIKE 'e2e\_%'`
|
||||
对齐到 CI 口令。(这与 Task 2.3 顺带修的 CI 缺陷 #5 是同一根因的两面。)
|
||||
|
||||
---
|
||||
|
||||
*创建: 2026-09-18 — 承接 `docs/文档治理报告-2026-09-18.md` 与 STATUS.md 的两处悬空引用(D1)*
|
||||
*审阅校正: 2026-09-19 — 按实测推翻 4 处失实断言(见附 A),登记 7 项新发现(见附 B)*
|
||||
|
||||
Reference in New Issue
Block a user