Procházet zdrojové kódy

test(backend): 入库两个已全绿契约测试类(12 例)——S-be-1 健康报告创建 + T38 供应团队邀请

- HealthReportControllerCreateReportTest(5 例):health.report.createReport 能力键的契约级单测,
  覆盖 未购计划 4001 / 未建家庭 noFamily / 成功创建 / 重复创建 / 参数校验 五条契约
- SupplyTeamControllerInviteTest(7 例):POST /api/admin/supply-team/invite 契约单测
- 均为 MockMvcBuilders.standaloneSetup 纯单测,无 @SpringBootTest,不依赖 DB,离线可跑
- 提交前定向复跑:Tests run: 12, Failures: 0, Errors: 0, Skipped: 0, BUILD SUCCESS
- 此前两个文件自 11-02 15:58 起一直未跟踪(29h),导致上述契约在主线与未来 CI 均不可见
E2E Test Bot před 1 týdnem
rodič
revize
81d8883f7

+ 162 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/HealthReportControllerCreateReportTest.java

@@ -0,0 +1,162 @@
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.entity.HealthReport;
+import com.etotem.cfc.service.HealthReportService;
+import com.etotem.cfc.service.PurchaseService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.http.MediaType;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * TDD 覆盖驱动(S-be-1 / 能力键 health.report.createReport):
+ * POST /api/health/report/create 创建健康报告 的契约级单测。
+ *
+ * 契约(对应 HealthReportController.createReport):
+ * - 未购计划:purchaseService.checkPurchase 返回 isActive=false → Result.error(4001, "请先购买健康启航计划体验包")
+ * - 未建家庭:familyId 缺省 → Result.noFamily("请先创建或加入家庭")
+ * - memberId 空(params 无 memberId 且 currentMemberId 缺省)→ Result.error("memberId不能为空")
+ * - reportType 空 → Result.error("reportType不能为空: physical_exam / gut_flora")
+ * - 正常路径:校验全过 → healthReportService.createReport(report, indicators) → Result.success(created)
+ *
+ * 测试不依赖数据库/外部服务:healthReportService / purchaseService 全 mock;不启 Spring 上下文。
+ */
+public class HealthReportControllerCreateReportTest {
+
+    @Mock private HealthReportService healthReportService;
+    @Mock private PurchaseService purchaseService;
+
+    private MockMvc mockMvc;
+    private HealthReportController controller;
+
+    @BeforeEach
+    public void setUp() {
+        MockitoAnnotations.openMocks(this);
+        controller = new HealthReportController();
+        // 仅注入本方法使用的两个依赖(其余 @Resource 字段本方法不触碰,保持 null 无碍)
+        ReflectionTestUtils.setField(controller, "healthReportService", healthReportService);
+        ReflectionTestUtils.setField(controller, "purchaseService", purchaseService);
+        mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
+    }
+
+    private Map<String, Object> purchaseActive(boolean active) {
+        Map<String, Object> m = new HashMap<>();
+        m.put("isActive", active);
+        return m;
+    }
+
+    private static final String BASE_BODY = "{"
+            + "\"memberId\":5,"
+            + "\"reportType\":\"physical_exam\","
+            + "\"overallScore\":88,"
+            + "\"indicators\":[{\"category\":\"blood\",\"indicatorName\":\"WBC\",\"indicatorValue\":\"6.1\",\"unit\":\"10^9/L\"}]"
+            + "}";
+
+    @Test
+    public void createReport_success_returnsCreated() throws Exception {
+        Long userId = 1L;
+        Long familyId = 7L;
+        when(purchaseService.checkPurchase(userId)).thenReturn(purchaseActive(true));
+        HealthReport created = new HealthReport();
+        created.setId(100L);
+        when(healthReportService.createReport(any(HealthReport.class), anyList())).thenReturn(created);
+
+        mockMvc.perform(post("/api/health/report/create")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(BASE_BODY)
+                        .requestAttr("userId", userId)
+                        .requestAttr("familyId", familyId))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data.id").value(100));
+
+        verify(healthReportService).createReport(any(HealthReport.class), anyList());
+    }
+
+    @Test
+    public void createReport_notPurchased_returns4001() throws Exception {
+        Long userId = 1L;
+        Long familyId = 7L;
+        when(purchaseService.checkPurchase(userId)).thenReturn(purchaseActive(false));
+
+        mockMvc.perform(post("/api/health/report/create")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(BASE_BODY)
+                        .requestAttr("userId", userId)
+                        .requestAttr("familyId", familyId))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(4001))
+                .andExpect(jsonPath("$.message").value("请先购买健康启航计划体验包"));
+
+        verify(healthReportService, never()).createReport(any(HealthReport.class), anyList());
+    }
+
+    @Test
+    public void createReport_noFamily_returnsNoFamily() throws Exception {
+        Long userId = 1L;
+        when(purchaseService.checkPurchase(userId)).thenReturn(purchaseActive(true));
+
+        mockMvc.perform(post("/api/health/report/create")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(BASE_BODY)
+                        .requestAttr("userId", userId))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.message").value("请先创建或加入家庭"));
+
+        verify(healthReportService, never()).createReport(any(HealthReport.class), anyList());
+    }
+
+    @Test
+    public void createReport_missingMemberId_returnsError() throws Exception {
+        Long userId = 1L;
+        Long familyId = 7L;
+        when(purchaseService.checkPurchase(userId)).thenReturn(purchaseActive(true));
+        String body = "{\"reportType\":\"physical_exam\"}";
+
+        mockMvc.perform(post("/api/health/report/create")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(body)
+                        .requestAttr("userId", userId)
+                        .requestAttr("familyId", familyId))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.message").value("memberId不能为空"));
+
+        verify(healthReportService, never()).createReport(any(HealthReport.class), anyList());
+    }
+
+    @Test
+    public void createReport_missingReportType_returnsError() throws Exception {
+        Long userId = 1L;
+        Long familyId = 7L;
+        when(purchaseService.checkPurchase(userId)).thenReturn(purchaseActive(true));
+        String body = "{\"memberId\":5}";
+
+        mockMvc.perform(post("/api/health/report/create")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(body)
+                        .requestAttr("userId", userId)
+                        .requestAttr("familyId", familyId))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.message").value("reportType不能为空: physical_exam / gut_flora"));
+
+        verify(healthReportService, never()).createReport(any(HealthReport.class), anyList());
+    }
+}

+ 167 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/admin/SupplyTeamControllerInviteTest.java

@@ -0,0 +1,167 @@
+package com.etotem.cfc.controller.admin;
+
+import com.etotem.cfc.entity.SupplySystem;
+import com.etotem.cfc.service.SupplySystemService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.http.MediaType;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyList;
+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;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * TDD 覆盖驱动(T38 邀请功能 / 能力键 team.invite.*):
+ * POST /api/admin/supply-team/invite-code、/invite/info、/invite/join 的契约级单测。
+ *
+ * 契约(对应 SupplyTeamController):
+ * - invite-code:systemId 空 → error("systemId不能为空");非成员/管理员 → error("无权操作该团队");
+ *   正常 → generateInviteCode 返回码 + getById 拼装团队信息 → success(data)
+ * - invite/info:inviteCode 空 → error("邀请码不能为空");正常 → getInviteInfo 返回 info → success
+ * - invite/join:inviteCode 空 → error("邀请码不能为空");正常 → joinByInviteCode(code,userId) → success
+ *
+ * 测试不依赖数据库/外部服务:supplySystemService 全 mock;不启 Spring 上下文。
+ */
+public class SupplyTeamControllerInviteTest {
+
+    @Mock private SupplySystemService supplySystemService;
+
+    private MockMvc mockMvc;
+    private SupplyTeamController controller;
+
+    @BeforeEach
+    public void setUp() {
+        MockitoAnnotations.openMocks(this);
+        controller = new SupplyTeamController();
+        ReflectionTestUtils.setField(controller, "supplySystemService", supplySystemService);
+        mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
+    }
+
+    // ---------- invite-code ----------
+    @Test
+    public void inviteCode_missingSystemId_returnsError() throws Exception {
+        Long userId = 1L;
+        String body = "{}";
+        mockMvc.perform(post("/api/admin/supply-team/invite-code")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(body)
+                        .requestAttr("userId", userId))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.message").value("systemId不能为空"));
+        verify(supplySystemService, never()).generateInviteCode(anyLong(), anyLong(), anyBoolean());
+    }
+
+    @Test
+    public void inviteCode_notMember_returnsForbidden() throws Exception {
+        Long userId = 1L;
+        Long systemId = 10L;
+        when(supplySystemService.isMemberOrAdmin(systemId, userId)).thenReturn(false);
+        String body = "{\"systemId\":10}";
+        mockMvc.perform(post("/api/admin/supply-team/invite-code")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(body)
+                        .requestAttr("userId", userId))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.message").value("无权操作该团队"));
+        verify(supplySystemService, never()).generateInviteCode(anyLong(), anyLong(), anyBoolean());
+    }
+
+    @Test
+    public void inviteCode_success_returnsCodeAndTeam() throws Exception {
+        Long userId = 1L;
+        Long systemId = 10L;
+        when(supplySystemService.isMemberOrAdmin(systemId, userId)).thenReturn(true);
+        when(supplySystemService.generateInviteCode(eq(systemId), eq(userId), anyBoolean())).thenReturn("ABC123");
+        SupplySystem system = mock(SupplySystem.class);
+        when(system.getName()).thenReturn("成长团队A");
+        when(supplySystemService.getById(systemId)).thenReturn(system);
+        String body = "{\"systemId\":10}";
+        mockMvc.perform(post("/api/admin/supply-team/invite-code")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(body)
+                        .requestAttr("userId", userId))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data.inviteCode").value("ABC123"))
+                .andExpect(jsonPath("$.data.systemId").value(10))
+                .andExpect(jsonPath("$.data.teamName").value("成长团队A"));
+        verify(supplySystemService).generateInviteCode(eq(systemId), eq(userId), anyBoolean());
+    }
+
+    // ---------- invite/info ----------
+    @Test
+    public void inviteInfo_missingCode_returnsError() throws Exception {
+        String body = "{}";
+        mockMvc.perform(post("/api/admin/supply-team/invite/info")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(body))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.message").value("邀请码不能为空"));
+        verify(supplySystemService, never()).getInviteInfo(anyString());
+    }
+
+    @Test
+    public void inviteInfo_success_returnsInfo() throws Exception {
+        Map<String, Object> info = new HashMap<>();
+        info.put("teamName", "成长团队A");
+        info.put("inviterNickname", "老师小王");
+        when(supplySystemService.getInviteInfo("ABC123")).thenReturn(info);
+        String body = "{\"inviteCode\":\"ABC123\"}";
+        mockMvc.perform(post("/api/admin/supply-team/invite/info")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(body))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data.teamName").value("成长团队A"));
+        verify(supplySystemService).getInviteInfo("ABC123");
+    }
+
+    // ---------- invite/join ----------
+    @Test
+    public void inviteJoin_missingCode_returnsError() throws Exception {
+        Long userId = 1L;
+        String body = "{}";
+        mockMvc.perform(post("/api/admin/supply-team/invite/join")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(body)
+                        .requestAttr("userId", userId))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.message").value("邀请码不能为空"));
+        verify(supplySystemService, never()).joinByInviteCode(anyString(), anyLong());
+    }
+
+    @Test
+    public void inviteJoin_success_callsJoin() throws Exception {
+        Long userId = 1L;
+        Map<String, Object> result = new HashMap<>();
+        result.put("ok", Boolean.TRUE);
+        result.put("systemId", 10);
+        when(supplySystemService.joinByInviteCode("ABC123", userId)).thenReturn(result);
+        String body = "{\"inviteCode\":\"ABC123\"}";
+        mockMvc.perform(post("/api/admin/supply-team/invite/join")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(body)
+                        .requestAttr("userId", userId))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                .andExpect(jsonPath("$.data.ok").value(true));
+        verify(supplySystemService).joinByInviteCode("ABC123", userId);
+    }
+}