|
@@ -0,0 +1,374 @@
|
|
|
|
|
+package com.etotem.cfc.unit;
|
|
|
|
|
+
|
|
|
|
|
+import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
|
|
|
|
+import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
|
|
|
|
+import com.etotem.cfc.common.Result;
|
|
|
|
|
+import com.etotem.cfc.controller.HealthReportController;
|
|
|
|
|
+import com.etotem.cfc.dto.ParsedReportPayload;
|
|
|
|
|
+import com.etotem.cfc.entity.HealthDiseaseRisk;
|
|
|
|
|
+import com.etotem.cfc.entity.HealthGutFlora;
|
|
|
|
|
+import com.etotem.cfc.entity.HealthIndicator;
|
|
|
|
|
+import com.etotem.cfc.entity.HealthReport;
|
|
|
|
|
+import com.etotem.cfc.mapper.HealthDiseaseRiskMapper;
|
|
|
|
|
+import com.etotem.cfc.mapper.HealthGutFloraMapper;
|
|
|
|
|
+import com.etotem.cfc.mapper.HealthIndicatorMapper;
|
|
|
|
|
+import com.etotem.cfc.mapper.HealthReportMapper;
|
|
|
|
|
+import com.etotem.cfc.service.HealthReportService;
|
|
|
|
|
+import com.etotem.cfc.service.ReportBlockService;
|
|
|
|
|
+import com.etotem.cfc.service.ReportMemberPermissionService;
|
|
|
|
|
+import org.apache.ibatis.builder.MapperBuilderAssistant;
|
|
|
|
|
+import org.junit.jupiter.api.BeforeAll;
|
|
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
|
|
+import org.junit.jupiter.api.DisplayName;
|
|
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
|
|
+
|
|
|
|
|
+import java.lang.reflect.Field;
|
|
|
|
|
+import java.lang.reflect.Modifier;
|
|
|
|
|
+import java.util.ArrayList;
|
|
|
|
|
+import java.util.Collections;
|
|
|
|
|
+import java.util.HashMap;
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+import java.util.Map;
|
|
|
|
|
+
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.assertSame;
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
|
|
+import static org.mockito.ArgumentMatchers.any;
|
|
|
|
|
+import static org.mockito.ArgumentMatchers.anyLong;
|
|
|
|
|
+import static org.mockito.ArgumentMatchers.anyString;
|
|
|
|
|
+import static org.mockito.ArgumentMatchers.eq;
|
|
|
|
|
+import static org.mockito.Mockito.mock;
|
|
|
|
|
+import static org.mockito.Mockito.never;
|
|
|
|
|
+import static org.mockito.Mockito.verify;
|
|
|
|
|
+import static org.mockito.Mockito.when;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 能力键 {@code health.report.detail} 契约级单测(S-cfc-146,TDD-RED 阶段)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p><b>契约溯源</b></p>
|
|
|
|
|
+ * <ul>
|
|
|
|
|
+ * <li>需求:docs/tdd/SRS.md «三、未覆盖控制器» HealthReportController(59 方法,0 覆盖)</li>
|
|
|
|
|
+ * <li>设计:{@link HealthReportController#getReportDetail}
|
|
|
|
|
+ * {@code @PostMapping("/report/detail")}(前缀 /api/health)+ {@link HealthReportService#getReportDetail}</li>
|
|
|
|
|
+ * <li>契约:docs/superpowers/api/API_REFERENCE.md §4.59.2(SC-C-06 / SC-X-02)——
|
|
|
|
|
+ * {@code POST /api/health/report/detail} 请求 {@code {reportId}};
|
|
|
|
|
+ * 响应 {@code Result<Map>}:报告详情(含指标明细 + 菌群数据 + 疾病风险评估);
|
|
|
|
|
+ * 权限校验(本人/管理员/被授权成员),无权返回「无权查看该报告」</li>
|
|
|
|
|
+ * <li>消费侧:cfc-frontend/pages/health/gut-flora-detail.vue loadData() 读取
|
|
|
|
|
+ * {@code report / indicators / gutFlora / diseaseRisks / reportInterpretation / payload.summary}</li>
|
|
|
|
|
+ * </ul>
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p><b>覆盖流</b>(纯 Mockito + 反射,无 Spring 上下文 / 无 DB / 无外部服务,
|
|
|
|
|
+ * 风格对齐同批 {@code TaskCompleteScC02ContractTest} 与 {@code HealthReportAuditContractTest}):</p>
|
|
|
|
|
+ * <ul>
|
|
|
|
|
+ * <li><b>控制器正常流</b>:管理员可见全部报告;本人可见无归属报告(subjectId=null);被授权成员可见;报告自带
|
|
|
|
|
+ * family_id 且 {@code familyId} 属性缺省时以报告家庭为权限作用域;</li>
|
|
|
|
|
+ * <li><b>控制器异常流</b>:reportId 缺失/非数字 → 「reportId不能为空」;报告不存在 → 「报告不存在」;
|
|
|
|
|
+ * 无归属报告且非本人非管理员 → 「无权查看该报告」;有归属但无授权关系 → 「无权查看该报告」;
|
|
|
|
|
+ * 报告无家庭且 {@code familyId} 缺省 → 「无权查看该报告」;</li>
|
|
|
|
|
+ * <li><b>Service 内容契约</b>:响应 Map 必须含 {@code report / indicators / gutFlora / diseaseRisks / blocks};
|
|
|
|
|
+ * 有肠道年龄/类型时生成 {@code reportInterpretation.gutAgeAssessment};
|
|
|
|
|
+ * 有 {@code payload_json} 时解析出嵌套 {@code payload}。</li>
|
|
|
|
|
+ * </ul>
|
|
|
|
|
+ */
|
|
|
|
|
+class HealthReportDetailContractTest {
|
|
|
|
|
+
|
|
|
|
|
+ private static final Long FAMILY_ID = 1L;
|
|
|
|
|
+ private static final Long VIEWER_USER_ID = 7L;
|
|
|
|
|
+ private static final Long SUBJECT_MEMBER_ID = 100L;
|
|
|
|
|
+
|
|
|
|
|
+ private HealthReportController controller;
|
|
|
|
|
+ private HealthReportService service;
|
|
|
|
|
+
|
|
|
|
|
+ /** controller 层转发的 mock(与 service 层真实实例隔离) */
|
|
|
|
|
+ private HealthReportService healthReportServiceMock;
|
|
|
|
|
+ private ReportMemberPermissionService permissionServiceMock;
|
|
|
|
|
+
|
|
|
|
|
+ // service 层真实实例的 mock 依赖
|
|
|
|
|
+ private HealthReportMapper healthReportMapper;
|
|
|
|
|
+ private HealthIndicatorMapper healthIndicatorMapper;
|
|
|
|
|
+ private HealthGutFloraMapper healthGutFloraMapper;
|
|
|
|
|
+ private HealthDiseaseRiskMapper healthDiseaseRiskMapper;
|
|
|
|
|
+ private ReportBlockService reportBlockService;
|
|
|
|
|
+ private com.fasterxml.jackson.databind.ObjectMapper objectMapper;
|
|
|
|
|
+
|
|
|
|
|
+ @BeforeAll
|
|
|
|
|
+ static void initMybatisPlusTableInfo() {
|
|
|
|
|
+ // 纯 Mockito 下 LambdaQueryWrapper 需要 TableInfo 缓存(与 HealthReportAuditContractTest 一致)
|
|
|
|
|
+ MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), "");
|
|
|
|
|
+ TableInfoHelper.initTableInfo(assistant, HealthReport.class);
|
|
|
|
|
+ TableInfoHelper.initTableInfo(assistant, HealthIndicator.class);
|
|
|
|
|
+ TableInfoHelper.initTableInfo(assistant, HealthGutFlora.class);
|
|
|
|
|
+ TableInfoHelper.initTableInfo(assistant, HealthDiseaseRisk.class);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @BeforeEach
|
|
|
|
|
+ void setUp() throws Exception {
|
|
|
|
|
+ // ============ service 层:真实 HealthReportService + mock 依赖 ============
|
|
|
|
|
+ service = new HealthReportService();
|
|
|
|
|
+ injectAllDependencies(service);
|
|
|
|
|
+ healthReportMapper = (HealthReportMapper) readField(service, "healthReportMapper");
|
|
|
|
|
+ healthIndicatorMapper = (HealthIndicatorMapper) readField(service, "healthIndicatorMapper");
|
|
|
|
|
+ healthGutFloraMapper = (HealthGutFloraMapper) readField(service, "healthGutFloraMapper");
|
|
|
|
|
+ healthDiseaseRiskMapper = (HealthDiseaseRiskMapper) readField(service, "healthDiseaseRiskMapper");
|
|
|
|
|
+ reportBlockService = (ReportBlockService) readField(service, "reportBlockService");
|
|
|
|
|
+ objectMapper = (com.fasterxml.jackson.databind.ObjectMapper) readField(service, "objectMapper");
|
|
|
|
|
+
|
|
|
|
|
+ // ============ controller 层:真实 HealthReportController + 仅注入 getReportDetail 依赖 ============
|
|
|
|
|
+ // 注意:不用全字段反射注入——控制器含 @Value String 字段(uploadBaseDir/langgraphBaseUrl),
|
|
|
|
|
+ // Mockito 无法 mock final 类 String;只注入该 handler 触及的两个 @Resource 依赖即可。
|
|
|
|
|
+ controller = new HealthReportController();
|
|
|
|
|
+ healthReportServiceMock = mock(HealthReportService.class);
|
|
|
|
|
+ permissionServiceMock = mock(ReportMemberPermissionService.class);
|
|
|
|
|
+ inject(controller, "healthReportService", healthReportServiceMock);
|
|
|
|
|
+ inject(controller, "reportMemberPermissionService", permissionServiceMock);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 控制器契约 · 正常流(API_REFERENCE §4.59.2) ====================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("正常流-1:管理员/被授权成员查看报告 → Result.success(完整详情 Map)")
|
|
|
|
|
+ void controller_正常流_管理员查看报告_返回完整详情() {
|
|
|
|
|
+ when(healthReportServiceMock.getReportById(1001L))
|
|
|
|
|
+ .thenReturn(report(1001L, SUBJECT_MEMBER_ID, 999L, FAMILY_ID));
|
|
|
|
|
+ when(permissionServiceMock.canViewMemberReports(VIEWER_USER_ID, SUBJECT_MEMBER_ID, FAMILY_ID))
|
|
|
|
|
+ .thenReturn(true);
|
|
|
|
|
+ Map<String, Object> detail = detailMap();
|
|
|
|
|
+ when(healthReportServiceMock.getReportDetail(1001L)).thenReturn(detail);
|
|
|
|
|
+
|
|
|
|
|
+ Result<Map<String, Object>> result = controller.getReportDetail(paramsOf(1001L), VIEWER_USER_ID, FAMILY_ID);
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(Integer.valueOf(200), result.getCode(), "契约:查看有权限的报告应返回 code=200");
|
|
|
|
|
+ assertEquals("success", result.getMessage(), "契约:成功响应 message=success");
|
|
|
|
|
+ assertNotNull(result.getData(), "契约:data 必须为报告详情 Map");
|
|
|
|
|
+ assertSame(detail, result.getData(), "契约:data 必须是 Service 组装的完整详情");
|
|
|
|
|
+ verify(healthReportServiceMock).getReportDetail(1001L);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("正常流-2:本人查看无归属报告(subjectId=null)→ 放行,不触发家庭管理员判定")
|
|
|
|
|
+ void controller_正常流_本人查看无归属报告_返回详情() {
|
|
|
|
|
+ when(healthReportServiceMock.getReportById(1002L))
|
|
|
|
|
+ .thenReturn(report(1002L, null, VIEWER_USER_ID, FAMILY_ID));
|
|
|
|
|
+ when(healthReportServiceMock.getReportDetail(1002L)).thenReturn(detailMap());
|
|
|
|
|
+
|
|
|
|
|
+ Result<Map<String, Object>> result = controller.getReportDetail(paramsOf(1002L), VIEWER_USER_ID, FAMILY_ID);
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(Integer.valueOf(200), result.getCode(), "契约:本人查看自己上传的无归属报告应放行");
|
|
|
|
|
+ verify(healthReportServiceMock).getReportDetail(1002L);
|
|
|
|
|
+ verify(permissionServiceMock, never()).isFamilyAdmin(anyLong(), anyLong());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("正常流-3:被授权成员查看 → canViewMemberReports=true 放行")
|
|
|
|
|
+ void controller_正常流_被授权成员查看_返回详情() {
|
|
|
|
|
+ when(healthReportServiceMock.getReportById(1003L))
|
|
|
|
|
+ .thenReturn(report(1003L, SUBJECT_MEMBER_ID, 999L, FAMILY_ID));
|
|
|
|
|
+ when(permissionServiceMock.canViewMemberReports(VIEWER_USER_ID, SUBJECT_MEMBER_ID, FAMILY_ID))
|
|
|
|
|
+ .thenReturn(true);
|
|
|
|
|
+ when(healthReportServiceMock.getReportDetail(1003L)).thenReturn(detailMap());
|
|
|
|
|
+
|
|
|
|
|
+ Result<Map<String, Object>> result = controller.getReportDetail(paramsOf(1003L), VIEWER_USER_ID, FAMILY_ID);
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(Integer.valueOf(200), result.getCode(), "契约:被授权成员应可见该报告详情");
|
|
|
|
|
+ verify(permissionServiceMock).canViewMemberReports(VIEWER_USER_ID, SUBJECT_MEMBER_ID, FAMILY_ID);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("正常流-4:报告自带 family_id、familyId 属性缺省 → 以报告家庭为权限作用域")
|
|
|
|
|
+ void controller_正常流_报告自带familyId_属性缺省_以报告家庭为作用域() {
|
|
|
|
|
+ Long reportFamily = 5L;
|
|
|
|
|
+ when(healthReportServiceMock.getReportById(1004L))
|
|
|
|
|
+ .thenReturn(report(1004L, SUBJECT_MEMBER_ID, 999L, reportFamily));
|
|
|
|
|
+ when(permissionServiceMock.canViewMemberReports(VIEWER_USER_ID, SUBJECT_MEMBER_ID, reportFamily))
|
|
|
|
|
+ .thenReturn(true);
|
|
|
|
|
+ when(healthReportServiceMock.getReportDetail(1004L)).thenReturn(detailMap());
|
|
|
|
|
+
|
|
|
|
|
+ // @RequestAttribute familyId = null(如跨家庭 token / 取不到上下文)→ 回退报告自身 family_id
|
|
|
|
|
+ Result<Map<String, Object>> result = controller.getReportDetail(paramsOf(1004L), VIEWER_USER_ID, null);
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(Integer.valueOf(200), result.getCode(), "契约:报告自带 family_id 时应以其为作用域放行");
|
|
|
|
|
+ verify(permissionServiceMock).canViewMemberReports(VIEWER_USER_ID, SUBJECT_MEMBER_ID, reportFamily);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 控制器契约 · 异常流 ====================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("异常流-1:reportId 缺失 → 「reportId不能为空」,不触达 Service")
|
|
|
|
|
+ void controller_异常流_reportId缺失_返回reportId不能为空() {
|
|
|
|
|
+ Result<Map<String, Object>> result = controller.getReportDetail(new HashMap<String, Object>(), VIEWER_USER_ID, FAMILY_ID);
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals("reportId不能为空", result.getMessage(), "契约:未传 reportId 必须提示,不得静默查询");
|
|
|
|
|
+ verify(healthReportServiceMock, never()).getReportById(anyLong());
|
|
|
|
|
+ verify(healthReportServiceMock, never()).getReportDetail(anyLong());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("异常流-2:reportId 非数字 → 视为未传,返回「reportId不能为空」")
|
|
|
|
|
+ void controller_异常流_reportId非数字_视为未传() {
|
|
|
|
|
+ Result<Map<String, Object>> result = controller.getReportDetail(paramsOf("abc"), VIEWER_USER_ID, FAMILY_ID);
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals("reportId不能为空", result.getMessage(), "契约:非数字 reportId 应安全解析为未传");
|
|
|
|
|
+ verify(healthReportServiceMock, never()).getReportById(anyLong());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("异常流-3:报告不存在 → 「报告不存在」,不组装详情")
|
|
|
|
|
+ void controller_异常流_报告不存在_返回报告不存在() {
|
|
|
|
|
+ when(healthReportServiceMock.getReportById(9999L)).thenReturn(null);
|
|
|
|
|
+
|
|
|
|
|
+ Result<Map<String, Object>> result = controller.getReportDetail(paramsOf(9999L), VIEWER_USER_ID, FAMILY_ID);
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals("报告不存在", result.getMessage(), "契约:报告不存在必须明确提示");
|
|
|
|
|
+ verify(healthReportServiceMock, never()).getReportDetail(anyLong());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("异常流-4:无归属报告且非本人非管理员 → 「无权查看该报告」")
|
|
|
|
|
+ void controller_异常流_无归属报告非本人非管理员_无权查看() {
|
|
|
|
|
+ when(healthReportServiceMock.getReportById(1005L))
|
|
|
|
|
+ .thenReturn(report(1005L, null, 99L, FAMILY_ID)); // 上传者 99 ≠ 查看者 7
|
|
|
|
|
+ when(permissionServiceMock.isFamilyAdmin(VIEWER_USER_ID, FAMILY_ID)).thenReturn(false);
|
|
|
|
|
+
|
|
|
|
|
+ Result<Map<String, Object>> result = controller.getReportDetail(paramsOf(1005L), VIEWER_USER_ID, FAMILY_ID);
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals("无权查看该报告", result.getMessage(), "契约:无归属报告非本人非管理员必须拒绝");
|
|
|
|
|
+ verify(healthReportServiceMock, never()).getReportDetail(anyLong());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("异常流-5:有归属但无授权关系 → 「无权查看该报告」")
|
|
|
|
|
+ void controller_异常流_有归属无授权关系_无权查看() {
|
|
|
|
|
+ when(healthReportServiceMock.getReportById(1006L))
|
|
|
|
|
+ .thenReturn(report(1006L, SUBJECT_MEMBER_ID, 99L, FAMILY_ID));
|
|
|
|
|
+ when(permissionServiceMock.canViewMemberReports(VIEWER_USER_ID, SUBJECT_MEMBER_ID, FAMILY_ID))
|
|
|
|
|
+ .thenReturn(false);
|
|
|
|
|
+
|
|
|
|
|
+ Result<Map<String, Object>> result = controller.getReportDetail(paramsOf(1006L), VIEWER_USER_ID, FAMILY_ID);
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals("无权查看该报告", result.getMessage(), "契约:无本人/管理员/被授权关系必须拒绝");
|
|
|
|
|
+ verify(healthReportServiceMock, never()).getReportDetail(anyLong());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("异常流-6:报告无家庭且 familyId 属性缺省 → 「无权查看该报告」(无作用域即拒绝)")
|
|
|
|
|
+ void controller_异常流_报告无家庭且属性缺省_无权查看() {
|
|
|
|
|
+ when(healthReportServiceMock.getReportById(1007L))
|
|
|
|
|
+ .thenReturn(report(1007L, SUBJECT_MEMBER_ID, VIEWER_USER_ID, null));
|
|
|
|
|
+
|
|
|
|
|
+ Result<Map<String, Object>> result = controller.getReportDetail(paramsOf(1007L), VIEWER_USER_ID, null);
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals("无权查看该报告", result.getMessage(), "契约:无家庭作用域必须拒绝,不泄露数据");
|
|
|
|
|
+ verify(healthReportServiceMock, never()).getReportDetail(anyLong());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== Service 内容契约(响应 Map 组成) ====================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("内容契约-1:详情 Map 必须含 report/indicators/gutFlora/diseaseRisks/blocks")
|
|
|
|
|
+ void service_返回报告_指标明细_菌群数据_疾病风险_展示块() {
|
|
|
|
|
+ HealthReport report = report(1001L, SUBJECT_MEMBER_ID, VIEWER_USER_ID, FAMILY_ID);
|
|
|
|
|
+ when(healthReportMapper.selectById(1001L)).thenReturn(report);
|
|
|
|
|
+ when(healthIndicatorMapper.selectList(any())).thenReturn(Collections.singletonList(new HealthIndicator()));
|
|
|
|
|
+ when(healthGutFloraMapper.selectList(any())).thenReturn(Collections.singletonList(new HealthGutFlora()));
|
|
|
|
|
+ when(healthDiseaseRiskMapper.selectList(any())).thenReturn(Collections.singletonList(new HealthDiseaseRisk()));
|
|
|
|
|
+ when(reportBlockService.getBlocks(report.getReportType(), 1001L)).thenReturn(new ArrayList<>());
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> detail = service.getReportDetail(1001L);
|
|
|
|
|
+
|
|
|
|
|
+ assertNotNull(detail.get("report"), "契约:详情必须包含 report 实体");
|
|
|
|
|
+ assertNotNull(detail.get("indicators"), "契约:详情必须包含指标明细 indicators");
|
|
|
|
|
+ assertEquals(1, ((List<?>) detail.get("indicators")).size(), "指标明细应原样透传");
|
|
|
|
|
+ assertNotNull(detail.get("gutFlora"), "契约:详情必须包含菌群数据 gutFlora");
|
|
|
|
|
+ assertEquals(1, ((List<?>) detail.get("gutFlora")).size(), "菌群数据应原样透传");
|
|
|
|
|
+ assertNotNull(detail.get("diseaseRisks"), "契约:详情必须包含疾病风险评估 diseaseRisks");
|
|
|
|
|
+ assertEquals(1, ((List<?>) detail.get("diseaseRisks")).size(), "疾病风险应原样透传");
|
|
|
|
|
+ assertNotNull(detail.get("blocks"), "契约:详情必须包含通用展示块 blocks");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("内容契约-2:有肠道年龄/类型 → 生成 reportInterpretation.gutAgeAssessment")
|
|
|
|
|
+ void service_有肠道年龄_生成报告解读() {
|
|
|
|
|
+ HealthReport report = report(1001L, SUBJECT_MEMBER_ID, VIEWER_USER_ID, FAMILY_ID);
|
|
|
|
|
+ report.setGutAge("28");
|
|
|
|
|
+ report.setGutType("平衡型");
|
|
|
|
|
+ when(healthReportMapper.selectById(1001L)).thenReturn(report);
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> detail = service.getReportDetail(1001L);
|
|
|
|
|
+
|
|
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
|
|
+ Map<String, Object> interp = (Map<String, Object>) detail.get("reportInterpretation");
|
|
|
|
|
+ assertNotNull(interp, "契约:有肠道年龄/类型时必须生成 reportInterpretation");
|
|
|
|
|
+ String text = (String) interp.get("gutAgeAssessment");
|
|
|
|
|
+ assertNotNull(text, "契约:reportInterpretation.gutAgeAssessment 不得为空");
|
|
|
|
|
+ assertTrue(text.contains("28"), "解读文本应包含肠道年龄, 实际: " + text);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("内容契约-3:有 payload_json → 解析出嵌套 payload(前端取 payload.summary 补全 gender/age)")
|
|
|
|
|
+ void service_有payloadJson_解析为payload() throws Exception {
|
|
|
|
|
+ HealthReport report = report(1001L, SUBJECT_MEMBER_ID, VIEWER_USER_ID, FAMILY_ID);
|
|
|
|
|
+ report.setPayloadJson("{\"summary\":{\"personName\":\"小李\"}}");
|
|
|
|
|
+ when(healthReportMapper.selectById(1001L)).thenReturn(report);
|
|
|
|
|
+
|
|
|
|
|
+ ParsedReportPayload.Payload payload = new ParsedReportPayload.Payload();
|
|
|
|
|
+ ParsedReportPayload.Summary summary = new ParsedReportPayload.Summary();
|
|
|
|
|
+ summary.setPersonName("小李");
|
|
|
|
|
+ payload.setSummary(summary);
|
|
|
|
|
+ when(objectMapper.readValue(anyString(), eq(ParsedReportPayload.Payload.class))).thenReturn(payload);
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> detail = service.getReportDetail(1001L);
|
|
|
|
|
+
|
|
|
|
|
+ assertSame(payload, detail.get("payload"), "契约:payload_json 必须解析为嵌套 payload 结构");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 辅助构造 ====================
|
|
|
|
|
+
|
|
|
|
|
+ private HealthReport report(Long id, Long subjectId, Long uploaderUserId, Long familyId) {
|
|
|
|
|
+ HealthReport r = new HealthReport();
|
|
|
|
|
+ r.setId(id);
|
|
|
|
|
+ r.setSubjectId(subjectId);
|
|
|
|
|
+ r.setUserId(uploaderUserId);
|
|
|
|
|
+ r.setFamilyId(familyId);
|
|
|
|
|
+ r.setReportType("gut_flora");
|
|
|
|
|
+ return r;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Map<String, Object> paramsOf(Object reportId) {
|
|
|
|
|
+ Map<String, Object> p = new HashMap<>();
|
|
|
|
|
+ p.put("reportId", reportId);
|
|
|
|
|
+ return p;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Map<String, Object> detailMap() {
|
|
|
|
|
+ Map<String, Object> d = new HashMap<>();
|
|
|
|
|
+ d.put("report", new HealthReport());
|
|
|
|
|
+ d.put("indicators", new ArrayList<>());
|
|
|
|
|
+ d.put("gutFlora", new ArrayList<>());
|
|
|
|
|
+ d.put("diseaseRisks", new ArrayList<>());
|
|
|
|
|
+ return d;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Object readField(Object target, String name) throws Exception {
|
|
|
|
|
+ Field f = target.getClass().getDeclaredField(name);
|
|
|
|
|
+ f.setAccessible(true);
|
|
|
|
|
+ return f.get(target);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static void inject(Object target, String field, Object value) throws Exception {
|
|
|
|
|
+ Field f = target.getClass().getDeclaredField(field);
|
|
|
|
|
+ f.setAccessible(true);
|
|
|
|
|
+ f.set(target, value);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static void injectAllDependencies(Object target) throws Exception {
|
|
|
|
|
+ for (Field f : target.getClass().getDeclaredFields()) {
|
|
|
|
|
+ if (Modifier.isStatic(f.getModifiers())) {
|
|
|
|
|
+ continue; // 跳过 log 等 static final
|
|
|
|
|
+ }
|
|
|
|
|
+ f.setAccessible(true);
|
|
|
|
|
+ f.set(target, mock(f.getType()));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|