test(e2e-recon): E2E 场景对账表(32 项:31 项有真实覆盖、1 项被裁定取代)+ 补 /me/status.localEligibilityStatus 零断言缺口

- 对账表写入收尾计划 Task 2.1 节:首页 11 / 本地资格 11 / 权限 10 逐项映射到
  E2E 用例与后端集成测试;原始清单取自 commit ae1bd2b 占位文件头
- 发现并补齐 1 处缺口:/me/status 的 localEligibilityStatus/localEligibilityType
  此前零断言(实现存在但从未被测)→ 新增 MeStatusLocalEligibilityGuardTest 2 用例
- 首页 #10(counselingEnabled flag 控制入口)登记为被 2026-09-19 产品裁定取代(死配置移除)
- 实测:MeStatusLocalEligibilityGuardTest 2/2 绿(离线容器 3:19)
This commit is contained in:
WorkBuddy
2026-09-19 18:58:07 +08:00
parent e9b99d020b
commit 28cf25756a
2 changed files with 131 additions and 0 deletions
@@ -0,0 +1,77 @@
package com.danyang.match.platform.auth;
import com.danyang.match.platform.auth.service.AuthService;
import com.danyang.match.platform.local_eligibility.entity.LocalEligibility;
import com.danyang.match.platform.local_eligibility.mapper.LocalEligibilityMapper;
import com.danyang.match.platform.user.entity.User;
import com.danyang.match.platform.user.mapper.UserMapper;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.transaction.annotation.Transactional;
import java.util.Map;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* E2E 场景对账补缺(2026-09-19):`/me/status` 必须返回 `localEligibilityStatus`。
*
* <p>原始场景清单(Phase 23 占位文件头,「本地资格 11 项」第 6 项)在对账中发现
* <b>零断言覆盖</b> —— 实现存在({@code AuthService#getMeStatus} 组装
* {@code localEligibilityStatus / localEligibilityType / localEligibilityVerifiedAt},
* 未声明时回 {@code not_declared}),但 E2E 与后端测试都从未断言过该字段。
* 本守卫把该字段从「碰巧返回」变成「被声明的契约」:
* 若后人重构 {@code getMeStatus} 时丢掉本地资格组装,这里会红。
*/
@SpringBootTest
@ActiveProfiles("test")
@Transactional
class MeStatusLocalEligibilityGuardTest {
@MockBean
private StringRedisTemplate stringRedisTemplate;
@Autowired private AuthService authService;
@Autowired private UserMapper userMapper;
@Autowired private LocalEligibilityMapper localEligibilityMapper;
private User newUser() {
User user = new User();
user.setId(UUID.randomUUID().toString());
user.setWechatOpenid("openid-" + UUID.randomUUID());
user.setAccountStatus("normal");
userMapper.insert(user);
return user;
}
@Test
@DisplayName("未声明本地资格的用户:/me/status 显式返回 not_declared")
void notDeclaredUserGetsNotDeclaredStatus() {
User user = newUser();
Map<String, Object> status = authService.getMeStatus(user.getId());
assertEquals("not_declared", status.get("localEligibilityStatus"),
"未声明时 /me/status 必须显式返回 not_declared,而不是缺省该键");
}
@Test
@DisplayName("已声明本地资格的用户:/me/status 回读真实核验状态与类型")
void declaredUserGetsRealStatus() {
User user = newUser();
LocalEligibility el = new LocalEligibility();
el.setUserId(user.getId());
el.setEligibilityType("household_registration");
el.setVerificationStatus("declared");
localEligibilityMapper.insert(el);
Map<String, Object> status = authService.getMeStatus(user.getId());
assertEquals("declared", status.get("localEligibilityStatus"),
"/me/status 必须回读 user_local_eligibilities 的真实状态");
assertEquals("household_registration", status.get("localEligibilityType"));
}
}
@@ -136,6 +136,60 @@
- 注入②「把 `user_b_status` 列默认值改成 `accepted`」**无效**:`createMutualConsent`
在 Java 侧**显式写入** `pending`,DB 默认值根本不参与。这条侧面证明了写入路径的权威性。
### Task 2.1 补充:场景对账表(2026-09-19)
> 原始场景清单取自 Phase 23 占位文件头(commit `ae1bd2b`,此后文件已实现为真实用例)。
> 对账口径:**场景有真实断言即算覆盖**,层次分 E2E(HTTP 活栈)与后端集成(真实落库)。
**首页 11 项**
| # | 场景 | 覆盖 | 位置 |
|---|---|---|---|
| 1 | `GET /home` 返回 userState | ✅ E2E | `run_v11_tests::test_02_home`、`home_v11_test::test_home_returns_fixed_sections` |
| 2 | Banner 按时间过滤(未开始/已过期不显示) | ✅ 后端 | `BannerOperatorPhase19IntegrationTest` |
| 3 | Banner 按 `audience_scope` 过滤 | ✅ 后端 | 同上 |
| 4 | 首页 8 入口顺序固定(INV-017) | ✅ 双层 | 后端 `V11InvariantsTest#inv017`(真实 `getHome`)+ E2E `run_v11_tests::test_03`、`home_v11_test::test_quick_entries_fixed_order` |
| 5 | 未审核用户不返回真实推荐 | ✅ 后端 | `RecommendationEligibilityPhase06Test`(资格矩阵) |
| 6 | Paused 用户不返回新推荐 | ✅ 后端 | 同上 |
| 7 | Successful 用户不返回找对象推荐 | ✅ 后端 | 同上 + `RelationshipProgressPhase12Test` |
| 8 | 服务站可展示 | ✅ E2E | `run_v11_tests::test_06` + `service_appointment_test`(seed 站点)+ Phase 19 公告守卫 |
| 9 | Banner 不能外链(INV-016) | ✅ 双层 | 后端 `V11InvariantsTest#inv016`(反射读生产白名单)+ E2E `home_v11_test::test_banner_target_types_are_internal_only` |
| 10 | `counselingEnabled` flag 控制 `canApplyCounseling` | ⚠️ **场景过时(被取代)** | 该 flag 经产品裁定于 2026-09-19 移除(死配置,见 Task 3.2);入口可见性由 `CapabilityService` 决定(AGENTS.md #26)。原命题不再成立 |
| 11 | `canReceiveRecommendations` 随账号状态变化 | ✅ 间接 | 资格层 `RecommendationEligibilityPhase06Test` + E2E home 走真实 `/home`(Capability 参与);`CapabilityService` 无独立单测(接受,低风险) |
**本地资格 11 项**
| # | 场景 | 覆盖 | 位置 |
|---|---|---|---|
| 1 | 用户可声明丹阳工作/户籍/居住 | ✅ E2E | `local_eligibility_test::test_declaration_is_persisted`、`::test_new_user_is_not_declared` |
| 2 | 声明不默认要求上传佐证 | ✅ 后端 | `LocalEligibilityServiceTest`(declare 与 evidence 分离) |
| 3 | 佐证上传需要鉴权 | ✅ 后端 | `LocalEligibilityServiceTest`(userId 自限定)+ `PhotoUploadValidationTest` |
| 4 | 审核员可访问佐证(INV-011) | ✅ 后端 | `Inv011EvidenceAccessGuardTest`(真实落库 + 审计回查) |
| 5 | 红娘不可访问佐证(INV-011) | ✅ 后端 | 同上 |
| 6 | `/me/status` 返回 `localEligibilityStatus` | ✅ **本轮补缺(2026-09-19)** | 对账发现实现存在但**零断言** → 新增 `MeStatusLocalEligibilityGuardTest`(2 用例:not_declared / 真实回读) |
| 7 | 未核验影响匹配资格 | ✅ 后端 | `RecommendationEligibilityPhase06Test`(`localEligibilityRequired` 接入) |
| 8 | 审核进入 `local_eligibility` 队列 | ✅ 后端 | `ReviewServicePhase05Test` |
| 9 | 审核通过更新状态 | ✅ 后端 | 同上 |
| 10 | 需补充佐证触发用户上传 | ✅ 后端+页面 | `ReviewService` 置 `needs_evidence`、`LocalEligibilityService` 上传后回 `pending_review`(`LocalEligibilityServiceTest` 覆盖分支);用户端承接 `local-eligibility-supplement` 页 + `getStatus.evidenceRequired` |
| 11 | 查看佐证须记录审计 | ✅ 后端 | `Inv011EvidenceAccessGuardTest`(success/denied 双向审计落库回查) |
**权限 10 项**
| # | 场景 | 覆盖 | 位置 |
|---|---|---|---|
| 1 | Counselor 只看 assigned(INV-012) | ✅ 双层 | 后端 `CounselingPhase13ATest` + E2E `counseling_v11_test::test_counselor_assigned_list_is_scoped` |
| 2 | Counselor 不可看他人服务 | ✅ E2E | `counseling_permission_test::test_counselor_cannot_read_unassigned_service` |
| 3 | 红娘不可读咨询正文(INV-013) | ✅ 双层 | 后端 `V11InvariantsTest#inv013`(真实 `getDetail` 脱敏)+ E2E `counseling_permission_test::test_matchmaker_has_no_counseling_view` |
| 4 | 普通管理员不可读敏感备注 | ✅ 后端 | `CounselingPhase13ATest`(脱敏分层:manager 摘要 / 非 assigned 脱敏) |
| 5 | 咨询管理员可分配 | ✅ 双层 | 后端 `assignIfPending` 守卫 + E2E `run_v11_tests::test_08` |
| 6 | 咨询可转交风控 | ✅ 后端 | `RiskReferralService` + `ReportAndRiskPhase17IntegrationTest`(`detectCounselingRisk` 来源接入) |
| 7 | 服务站人员不能浏览全用户(INV-018) | ✅ 双层 | E2E `organization_isolation_test` + `service_station_permission_test` + 后端 `ServiceAppointmentDataScopeTest` |
| 8 | 服务站人员可处理本站预约 | ✅ E2E | `service_appointment_test::test_operator_can_confirm`(seed 站点范围) |
| 9 | 内容编辑员不能访问用户档案(INV-019) | ✅ 双层 | 后端 `V11InvariantsTest#inv019` + E2E `content_e2e_test::test_content_editor_cannot_read_user_matching_data` |
| 10 | 内容编辑员可管理文章 | ✅ E2E | `content_e2e_test::test_article_lifecycle_draft_publish_offline` |
**对账结论**:32 项场景中 **31 项有真实断言覆盖**(E2E 或后端集成),1 项(首页 #10)被产品裁定取代并记录取代关系;发现并补齐 **1 处零断言字段**(`/me/status.localEligibilityStatus`,场景 #6)。无「假覆盖」残留。
### Task 2.2: INV-011 显式守卫
**Files:**