|
@@ -0,0 +1,422 @@
|
|
|
|
|
+package com.etotem.cfc.unit;
|
|
|
|
|
+
|
|
|
|
|
+import com.etotem.cfc.common.Result;
|
|
|
|
|
+import com.etotem.cfc.controller.guide.PlanReviewController;
|
|
|
|
|
+import com.etotem.cfc.entity.Task;
|
|
|
|
|
+import com.etotem.cfc.entity.TaskPlanInstance;
|
|
|
|
|
+import com.etotem.cfc.entity.User;
|
|
|
|
|
+import com.etotem.cfc.mapper.FamilyMemberMapper;
|
|
|
|
|
+import com.etotem.cfc.mapper.TaskMapper;
|
|
|
|
|
+import com.etotem.cfc.mapper.TaskPlanInstanceMapper;
|
|
|
|
|
+import com.etotem.cfc.mapper.TaskTemplateItemMapper;
|
|
|
|
|
+import com.etotem.cfc.mapper.UserMapper;
|
|
|
|
|
+import com.etotem.cfc.service.PlanActivationService;
|
|
|
|
|
+import com.etotem.cfc.service.PlannerCertificationGuard;
|
|
|
|
|
+import com.etotem.cfc.service.PlannerCertificationService;
|
|
|
|
|
+import com.etotem.cfc.service.TaskPlanService;
|
|
|
|
|
+import org.junit.jupiter.api.DisplayName;
|
|
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
|
|
+import org.springframework.web.bind.annotation.PostMapping;
|
|
|
|
|
+import org.springframework.web.bind.annotation.RequestMapping;
|
|
|
|
|
+
|
|
|
|
|
+import java.util.Collections;
|
|
|
|
|
+import java.util.Date;
|
|
|
|
|
+import java.util.HashMap;
|
|
|
|
|
+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.assertThrows;
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
|
|
+import static org.mockito.ArgumentMatchers.any;
|
|
|
|
|
+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 teacher.plan.activate}(SC-T-05 激活方案拆任务·幂等,⭐ 生成任务)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p><b>契约锚点</b></p>
|
|
|
|
|
+ * <ul>
|
|
|
|
|
+ * <li>需求:{@code tests/user-scenarios/应用场景总览.md} SC-T-05 —— 激活 reviewed→active →
|
|
|
|
|
+ * 生成引导+模板任务 → B 出现启动任务;边界:已激活再激活<b>不重复生成(幂等)</b>、
|
|
|
|
|
+ * 失败回滚不残半任务。</li>
|
|
|
|
|
+ * <li>设计:{@code docs/superpowers/api/API_REFERENCE.md} §4.57 SC-T-05 行 ——
|
|
|
|
|
+ * {@code PlanReviewController POST /api/guide/plans/activate} →
|
|
|
|
|
+ * {@code PlanActivationService.activate(planId, operatorId)},
|
|
|
|
|
+ * 前置 {@code PlannerCertificationGuard} 认证闸门(基础类别)+
|
|
|
|
|
+ * {@code IllegalStateException} 状态守卫(激活幂等)。</li>
|
|
|
|
|
+ * <li>转测试侧(EVD-10 §5 TR-13 #2):重复激活(已 active / 状态非 reviewed)应被
|
|
|
|
|
+ * {@code PlanActivationService} 状态守卫拒绝,不重复拆任务。</li>
|
|
|
|
|
+ * </ul>
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p><b>实现现状(2026-11-13 实测 origin/cfclub)</b>:端点「POST /api/guide/plans/activate
|
|
|
|
|
+ * (PlanReviewController.activatePlan)」+「PlanActivationService.activate(PlannerCertificationGuard
|
|
|
|
|
+ * 认证闸门 + IllegalStateException 状态守卫,激活幂等)」<b>均已落地</b>,且 EVD-8R 契约套件
|
|
|
|
|
+ * (PlanReviewTeacherContractTest / PlanActivationExecutionContractTest)已覆盖 SC-T-05 且
|
|
|
|
|
+ * 全绿。本类按执行要求「实现已存在(测试意外全绿)」交编排侧<b>转测试登记</b>:
|
|
|
|
|
+ * 以单一能力键视角补齐端点映射/认证闸门/状态守卫/拆任务幂等的独立契约证据,不修改 src/main。</p>
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p><b>测试范围</b>:纯 Mockito / 无 Spring / 无 DB / 无外部服务;反射注入依赖字段
|
|
|
|
|
+ * ({@link AssessmentPlanContractTestBase#inject})。三类流:正常流、异常流(状态/参数/成员缺失)、
|
|
|
|
|
+ * 权限流(非 teacher 角色 403、未认证 teacher 403、admin 放行)。</p>
|
|
|
|
|
+ */
|
|
|
|
|
+class TeacherPlanActivateContractTest extends AssessmentPlanContractTestBase {
|
|
|
|
|
+
|
|
|
|
|
+ static {
|
|
|
|
|
+ initTable(Task.class);
|
|
|
|
|
+ initTable(TaskPlanInstance.class);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 端点契约(API_REFERENCE §4.57 SC-T-05 行) ====================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("端点契约:POST /api/guide/plans/activate 映射存在(PlanReviewController.activatePlan)")
|
|
|
|
|
+ void endpointMapping_activate() throws Exception {
|
|
|
|
|
+ Class<PlanReviewController> clazz = PlanReviewController.class;
|
|
|
|
|
+ boolean hasClassMapping = java.util.Arrays.stream(clazz.getDeclaredAnnotations())
|
|
|
|
|
+ .anyMatch(a -> a instanceof RequestMapping
|
|
|
|
|
+ && java.util.Arrays.asList(((RequestMapping) a).value()).contains("/api/guide/plans"));
|
|
|
|
|
+ assertTrue(hasClassMapping, "PlanReviewController 类级映射应为 /api/guide/plans");
|
|
|
|
|
+ boolean hasActivateMapping = java.util.Arrays.stream(clazz.getDeclaredMethods())
|
|
|
|
|
+ .anyMatch(m -> m.getName().equals("activatePlan")
|
|
|
|
|
+ && m.isAnnotationPresent(PostMapping.class)
|
|
|
|
|
+ && java.util.Arrays.asList(m.getAnnotation(PostMapping.class).value()).contains("/activate"));
|
|
|
|
|
+ assertTrue(hasActivateMapping, "PlanReviewController.activatePlan 应为 POST /activate");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 正常流:认证通过的 teacher 激活 ====================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("SC-T-05 正常:认证通过的 teacher 激活方案 → 200 方案已激活 + 拆任务")
|
|
|
|
|
+ void activate_正常_认证teacher() throws Exception {
|
|
|
|
|
+ PlanActivationService actSvc = mock(PlanActivationService.class);
|
|
|
|
|
+ PlannerCertificationGuard certGuard = mock(PlannerCertificationGuard.class);
|
|
|
|
|
+ PlanReviewController c = new PlanReviewController();
|
|
|
|
|
+ inject(c, "planMapper", mock(TaskPlanInstanceMapper.class));
|
|
|
|
|
+ inject(c, "userMapper", mock(UserMapper.class));
|
|
|
|
|
+ inject(c, "planActivationService", actSvc);
|
|
|
|
|
+ inject(c, "plannerCertificationGuard", certGuard);
|
|
|
|
|
+
|
|
|
|
|
+ when(certGuard.assertCertified(7L, PlannerCertificationService.CATEGORY_PLANNER)).thenReturn(null);
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> body = new HashMap<>();
|
|
|
|
|
+ body.put("planId", 88L);
|
|
|
|
|
+ Result<String> r = c.activatePlan(body, 7L, "teacher", null);
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(200, r.getCode());
|
|
|
|
|
+ assertEquals("方案已激活", r.getData());
|
|
|
|
|
+ verify(actSvc).activate(88L, 7L);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 权限流:角色 / 认证闸门 / 未登录 ====================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("SC-T-05 权限:非 teacher/admin 角色激活 → 拒绝,不触达激活服务")
|
|
|
|
|
+ void activate_权限_非teacher() throws Exception {
|
|
|
|
|
+ PlanReviewController c = new PlanReviewController();
|
|
|
|
|
+ inject(c, "planMapper", mock(TaskPlanInstanceMapper.class));
|
|
|
|
|
+ inject(c, "userMapper", mock(UserMapper.class));
|
|
|
|
|
+ PlanActivationService actSvc = mock(PlanActivationService.class);
|
|
|
|
|
+ inject(c, "planActivationService", actSvc);
|
|
|
|
|
+ inject(c, "plannerCertificationGuard", mock(PlannerCertificationGuard.class));
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> body = new HashMap<>();
|
|
|
|
|
+ body.put("planId", 88L);
|
|
|
|
|
+ Result<String> r = c.activatePlan(body, 1L, "parent", null);
|
|
|
|
|
+
|
|
|
|
|
+ assertTrue(r.getCode() != 200 && r.getMessage().contains("无权限"));
|
|
|
|
|
+ verify(actSvc, never()).activate(any(), any());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("SC-T-05 权限:teacher 未完成基础类别认证 → 403 拦截,不拆任务")
|
|
|
|
|
+ void activate_权限_未认证teacher() throws Exception {
|
|
|
|
|
+ PlannerCertificationGuard certGuard = mock(PlannerCertificationGuard.class);
|
|
|
|
|
+ PlanActivationService actSvc = mock(PlanActivationService.class);
|
|
|
|
|
+ PlanReviewController c = new PlanReviewController();
|
|
|
|
|
+ inject(c, "planMapper", mock(TaskPlanInstanceMapper.class));
|
|
|
|
|
+ inject(c, "userMapper", mock(UserMapper.class));
|
|
|
|
|
+ inject(c, "planActivationService", actSvc);
|
|
|
|
|
+ inject(c, "plannerCertificationGuard", certGuard);
|
|
|
|
|
+
|
|
|
|
|
+ when(certGuard.assertCertified(7L, PlannerCertificationService.CATEGORY_PLANNER))
|
|
|
|
|
+ .thenReturn(Result.error(403, "规划师未完成成长规划师基础认证,暂不可提供服务"));
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> body = new HashMap<>();
|
|
|
|
|
+ body.put("planId", 88L);
|
|
|
|
|
+ Result<String> r = c.activatePlan(body, 7L, "teacher", null);
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(403, r.getCode());
|
|
|
|
|
+ assertTrue(r.getMessage().contains("认证"));
|
|
|
|
|
+ verify(actSvc, never()).activate(any(), any());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("SC-T-05 权限:admin 激活跳过认证闸门 → 放行")
|
|
|
|
|
+ void activate_权限_admin放行() throws Exception {
|
|
|
|
|
+ PlannerCertificationGuard certGuard = mock(PlannerCertificationGuard.class);
|
|
|
|
|
+ PlanActivationService actSvc = mock(PlanActivationService.class);
|
|
|
|
|
+ PlanReviewController c = new PlanReviewController();
|
|
|
|
|
+ inject(c, "planMapper", mock(TaskPlanInstanceMapper.class));
|
|
|
|
|
+ inject(c, "userMapper", mock(UserMapper.class));
|
|
|
|
|
+ inject(c, "planActivationService", actSvc);
|
|
|
|
|
+ inject(c, "plannerCertificationGuard", certGuard);
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> body = new HashMap<>();
|
|
|
|
|
+ body.put("planId", 88L);
|
|
|
|
|
+ Result<String> r = c.activatePlan(body, 1L, "admin", Collections.emptyList());
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(200, r.getCode());
|
|
|
|
|
+ verify(actSvc).activate(88L, 1L);
|
|
|
|
|
+ verify(certGuard, never()).assertCertified(any(), any());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 异常流:参数 / 状态守卫 ====================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("SC-T-05 异常:缺 planId → planId required")
|
|
|
|
|
+ void activate_异常_缺planId() throws Exception {
|
|
|
|
|
+ PlanReviewController c = new PlanReviewController();
|
|
|
|
|
+ inject(c, "planMapper", mock(TaskPlanInstanceMapper.class));
|
|
|
|
|
+ inject(c, "userMapper", mock(UserMapper.class));
|
|
|
|
|
+ inject(c, "planActivationService", mock(PlanActivationService.class));
|
|
|
|
|
+ inject(c, "plannerCertificationGuard", mock(PlannerCertificationGuard.class));
|
|
|
|
|
+
|
|
|
|
|
+ Result<String> r = c.activatePlan(new HashMap<>(), 7L, "teacher", null);
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(500, r.getCode());
|
|
|
|
|
+ assertTrue(r.getMessage().contains("planId required"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("SC-T-05 异常:状态守卫 IllegalStateException → 业务提示(不 500 炸栈)")
|
|
|
|
|
+ void activate_异常_状态守卫() throws Exception {
|
|
|
|
|
+ PlanActivationService actSvc = mock(PlanActivationService.class);
|
|
|
|
|
+ PlannerCertificationGuard certGuard = mock(PlannerCertificationGuard.class);
|
|
|
|
|
+ PlanReviewController c = new PlanReviewController();
|
|
|
|
|
+ inject(c, "planMapper", mock(TaskPlanInstanceMapper.class));
|
|
|
|
|
+ inject(c, "userMapper", mock(UserMapper.class));
|
|
|
|
|
+ inject(c, "planActivationService", actSvc);
|
|
|
|
|
+ inject(c, "plannerCertificationGuard", certGuard);
|
|
|
|
|
+
|
|
|
|
|
+ when(certGuard.assertCertified(7L, PlannerCertificationService.CATEGORY_PLANNER)).thenReturn(null);
|
|
|
|
|
+ org.mockito.Mockito.doThrow(new IllegalStateException("方案状态异常,需要先审核通过"))
|
|
|
|
|
+ .when(actSvc).activate(88L, 7L);
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> body = new HashMap<>();
|
|
|
|
|
+ body.put("planId", 88L);
|
|
|
|
|
+ Result<String> r = c.activatePlan(body, 7L, "teacher", null);
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(500, r.getCode());
|
|
|
|
|
+ assertTrue(r.getMessage().contains("方案状态异常"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 服务契约:PlanActivationService.activate 状态守卫(幂等核心) ====================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("SC-T-05 正常:reviewed 方案激活 → 状态 active + 拆任务 + 落库")
|
|
|
|
|
+ void serviceActivate_正常() throws Exception {
|
|
|
|
|
+ TaskPlanInstanceMapper planMapper = mock(TaskPlanInstanceMapper.class);
|
|
|
|
|
+ UserMapper userMapper = mock(UserMapper.class);
|
|
|
|
|
+ FamilyMemberMapper familyMemberMapper = mock(FamilyMemberMapper.class);
|
|
|
|
|
+ TaskPlanService taskPlanSvc = mock(TaskPlanService.class);
|
|
|
|
|
+ PlanActivationService svc = new PlanActivationService();
|
|
|
|
|
+ inject(svc, "planMapper", planMapper);
|
|
|
|
|
+ inject(svc, "userMapper", userMapper);
|
|
|
|
|
+ inject(svc, "familyMemberMapper", familyMemberMapper);
|
|
|
|
|
+ inject(svc, "taskPlanService", taskPlanSvc);
|
|
|
|
|
+
|
|
|
|
|
+ TaskPlanInstance plan = taskPlan(88L, "reviewed");
|
|
|
|
|
+ plan.setMemberId(10L);
|
|
|
|
|
+ when(planMapper.selectById(88L)).thenReturn(plan);
|
|
|
|
|
+ when(userMapper.selectById(10L)).thenReturn(null);
|
|
|
|
|
+ when(familyMemberMapper.selectById(10L)).thenReturn(new com.etotem.cfc.entity.FamilyMember());
|
|
|
|
|
+ when(taskPlanSvc.generateTasksFromTemplate(plan, 7L)).thenReturn(3);
|
|
|
|
|
+
|
|
|
|
|
+ svc.activate(88L, 7L);
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals("active", plan.getStatus());
|
|
|
|
|
+ assertNotNull(plan.getActivatedAt());
|
|
|
|
|
+ assertEquals(14, plan.getTotalDays());
|
|
|
|
|
+ assertEquals(0, plan.getCompletedDays());
|
|
|
|
|
+ verify(taskPlanSvc).generateTasksFromTemplate(plan, 7L);
|
|
|
|
|
+ verify(planMapper).updateById(plan);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("SC-T-05 幂等:已 active 方案重复激活 → 状态守卫拒绝,不重复拆任务")
|
|
|
|
|
+ void serviceActivate_幂等_已激活() throws Exception {
|
|
|
|
|
+ TaskPlanInstanceMapper planMapper = mock(TaskPlanInstanceMapper.class);
|
|
|
|
|
+ TaskPlanService taskPlanSvc = mock(TaskPlanService.class);
|
|
|
|
|
+ PlanActivationService svc = new PlanActivationService();
|
|
|
|
|
+ inject(svc, "planMapper", planMapper);
|
|
|
|
|
+ inject(svc, "userMapper", mock(UserMapper.class));
|
|
|
|
|
+ inject(svc, "familyMemberMapper", mock(FamilyMemberMapper.class));
|
|
|
|
|
+ inject(svc, "taskPlanService", taskPlanSvc);
|
|
|
|
|
+
|
|
|
|
|
+ when(planMapper.selectById(88L)).thenReturn(taskPlan(88L, "active"));
|
|
|
|
|
+
|
|
|
|
|
+ IllegalStateException ex = assertThrows(IllegalStateException.class, () -> svc.activate(88L, 7L));
|
|
|
|
|
+
|
|
|
|
|
+ assertTrue(ex.getMessage().contains("需要先审核通过"));
|
|
|
|
|
+ verify(taskPlanSvc, never()).generateTasksFromTemplate(any(), any());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("SC-T-05 幂等:generated(未审核)方案激活 → 状态守卫拒绝")
|
|
|
|
|
+ void serviceActivate_幂等_未审核() throws Exception {
|
|
|
|
|
+ TaskPlanInstanceMapper planMapper = mock(TaskPlanInstanceMapper.class);
|
|
|
|
|
+ PlanActivationService svc = new PlanActivationService();
|
|
|
|
|
+ inject(svc, "planMapper", planMapper);
|
|
|
|
|
+ inject(svc, "userMapper", mock(UserMapper.class));
|
|
|
|
|
+ inject(svc, "familyMemberMapper", mock(FamilyMemberMapper.class));
|
|
|
|
|
+ inject(svc, "taskPlanService", mock(TaskPlanService.class));
|
|
|
|
|
+
|
|
|
|
|
+ when(planMapper.selectById(88L)).thenReturn(taskPlan(88L, "generated"));
|
|
|
|
|
+
|
|
|
|
|
+ IllegalStateException ex = assertThrows(IllegalStateException.class, () -> svc.activate(88L, 7L));
|
|
|
|
|
+
|
|
|
|
|
+ assertTrue(ex.getMessage().contains("需要先审核通过"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("SC-T-05 异常:方案不存在 → 方案不存在")
|
|
|
|
|
+ void serviceActivate_异常_方案不存在() throws Exception {
|
|
|
|
|
+ TaskPlanInstanceMapper planMapper = mock(TaskPlanInstanceMapper.class);
|
|
|
|
|
+ PlanActivationService svc = new PlanActivationService();
|
|
|
|
|
+ inject(svc, "planMapper", planMapper);
|
|
|
|
|
+ inject(svc, "userMapper", mock(UserMapper.class));
|
|
|
|
|
+ inject(svc, "familyMemberMapper", mock(FamilyMemberMapper.class));
|
|
|
|
|
+ inject(svc, "taskPlanService", mock(TaskPlanService.class));
|
|
|
|
|
+
|
|
|
|
|
+ when(planMapper.selectById(88L)).thenReturn(null);
|
|
|
|
|
+
|
|
|
|
|
+ IllegalStateException ex = assertThrows(IllegalStateException.class, () -> svc.activate(88L, 7L));
|
|
|
|
|
+
|
|
|
|
|
+ assertTrue(ex.getMessage().contains("方案不存在"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("SC-T-05 异常:成员既非 user 也非 family_members → 成员不存在,不拆任务")
|
|
|
|
|
+ void serviceActivate_异常_成员不存在() throws Exception {
|
|
|
|
|
+ TaskPlanInstanceMapper planMapper = mock(TaskPlanInstanceMapper.class);
|
|
|
|
|
+ UserMapper userMapper = mock(UserMapper.class);
|
|
|
|
|
+ FamilyMemberMapper familyMemberMapper = mock(FamilyMemberMapper.class);
|
|
|
|
|
+ TaskPlanService taskPlanSvc = mock(TaskPlanService.class);
|
|
|
|
|
+ PlanActivationService svc = new PlanActivationService();
|
|
|
|
|
+ inject(svc, "planMapper", planMapper);
|
|
|
|
|
+ inject(svc, "userMapper", userMapper);
|
|
|
|
|
+ inject(svc, "familyMemberMapper", familyMemberMapper);
|
|
|
|
|
+ inject(svc, "taskPlanService", taskPlanSvc);
|
|
|
|
|
+
|
|
|
|
|
+ TaskPlanInstance plan = taskPlan(88L, "reviewed");
|
|
|
|
|
+ plan.setMemberId(10L);
|
|
|
|
|
+ when(planMapper.selectById(88L)).thenReturn(plan);
|
|
|
|
|
+ when(userMapper.selectById(10L)).thenReturn(null);
|
|
|
|
|
+ when(familyMemberMapper.selectById(10L)).thenReturn(null);
|
|
|
|
|
+
|
|
|
|
|
+ IllegalStateException ex = assertThrows(IllegalStateException.class, () -> svc.activate(88L, 7L));
|
|
|
|
|
+
|
|
|
|
|
+ assertTrue(ex.getMessage().contains("成员不存在"));
|
|
|
|
|
+ verify(taskPlanSvc, never()).generateTasksFromTemplate(any(), any());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("SC-T-05 兼容:成员为可登录 user(userMapper 命中)→ 正常激活")
|
|
|
|
|
+ void serviceActivate_兼容_user成员() throws Exception {
|
|
|
|
|
+ TaskPlanInstanceMapper planMapper = mock(TaskPlanInstanceMapper.class);
|
|
|
|
|
+ UserMapper userMapper = mock(UserMapper.class);
|
|
|
|
|
+ FamilyMemberMapper familyMemberMapper = mock(FamilyMemberMapper.class);
|
|
|
|
|
+ TaskPlanService taskPlanSvc = mock(TaskPlanService.class);
|
|
|
|
|
+ PlanActivationService svc = new PlanActivationService();
|
|
|
|
|
+ inject(svc, "planMapper", planMapper);
|
|
|
|
|
+ inject(svc, "userMapper", userMapper);
|
|
|
|
|
+ inject(svc, "familyMemberMapper", familyMemberMapper);
|
|
|
|
|
+ inject(svc, "taskPlanService", taskPlanSvc);
|
|
|
|
|
+
|
|
|
|
|
+ TaskPlanInstance plan = taskPlan(88L, "reviewed");
|
|
|
|
|
+ plan.setMemberId(10L);
|
|
|
|
|
+ when(planMapper.selectById(88L)).thenReturn(plan);
|
|
|
|
|
+ User u = new User();
|
|
|
|
|
+ u.setId(10L);
|
|
|
|
|
+ when(userMapper.selectById(10L)).thenReturn(u);
|
|
|
|
|
+ when(taskPlanSvc.generateTasksFromTemplate(plan, 7L)).thenReturn(0);
|
|
|
|
|
+
|
|
|
|
|
+ svc.activate(88L, 7L);
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals("active", plan.getStatus());
|
|
|
|
|
+ verify(planMapper).updateById(plan);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 拆任务幂等:TaskPlanService.generateTasksFromTemplate ====================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("SC-T-05 幂等:已有未取消任务 → 返回 0,不重复拆任务(不重复落库)")
|
|
|
|
|
+ void generateTasks_幂等_已拆过() throws Exception {
|
|
|
|
|
+ TaskMapper taskMapper = mock(TaskMapper.class);
|
|
|
|
|
+ TaskTemplateItemMapper templateItemMapper = mock(TaskTemplateItemMapper.class);
|
|
|
|
|
+ TaskPlanService svc = new TaskPlanService();
|
|
|
|
|
+ inject(svc, "taskMapper", taskMapper);
|
|
|
|
|
+ inject(svc, "templateItemMapper", templateItemMapper);
|
|
|
|
|
+
|
|
|
|
|
+ when(taskMapper.selectCount(any())).thenReturn(3L);
|
|
|
|
|
+
|
|
|
|
|
+ TaskPlanInstance plan = taskPlan(88L, "reviewed");
|
|
|
|
|
+ plan.setPackageId(9L);
|
|
|
|
|
+ int count = svc.generateTasksFromTemplate(plan, 7L);
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(0, count, "已拆过任务应幂等返回 0,不重复生成");
|
|
|
|
|
+ verify(taskMapper, never()).insert(any(Task.class));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("SC-T-05 正常:首次拆任务 = 1 引导任务 + N 模板任务,绑定 plan 来源")
|
|
|
|
|
+ void generateTasks_正常_引导加模板() throws Exception {
|
|
|
|
|
+ TaskMapper taskMapper = mock(TaskMapper.class);
|
|
|
|
|
+ TaskTemplateItemMapper templateItemMapper = mock(TaskTemplateItemMapper.class);
|
|
|
|
|
+ TaskPlanService svc = new TaskPlanService();
|
|
|
|
|
+ inject(svc, "taskMapper", taskMapper);
|
|
|
|
|
+ inject(svc, "templateItemMapper", templateItemMapper);
|
|
|
|
|
+
|
|
|
|
|
+ when(taskMapper.selectCount(any())).thenReturn(0L);
|
|
|
|
|
+
|
|
|
|
|
+ TaskPlanInstance plan = taskPlan(88L, "reviewed");
|
|
|
|
|
+ plan.setPackageId(9L);
|
|
|
|
|
+
|
|
|
|
|
+ com.etotem.cfc.entity.TaskTemplateItem item = new com.etotem.cfc.entity.TaskTemplateItem();
|
|
|
|
|
+ item.setId(501L);
|
|
|
|
|
+ item.setTaskName("晨起运动");
|
|
|
|
|
+ item.setPoints(3);
|
|
|
|
|
+ when(templateItemMapper.selectList(any())).thenReturn(java.util.Arrays.asList(item));
|
|
|
|
|
+
|
|
|
|
|
+ int count = svc.generateTasksFromTemplate(plan, 7L);
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(2, count, "1 引导任务 + 1 模板任务");
|
|
|
|
|
+ org.mockito.ArgumentCaptor<Task> captor = org.mockito.ArgumentCaptor.forClass(Task.class);
|
|
|
|
|
+ verify(taskMapper, org.mockito.Mockito.times(2)).insert(captor.capture());
|
|
|
|
|
+ for (Task t : captor.getAllValues()) {
|
|
|
|
|
+ assertEquals("plan", t.getSourceType());
|
|
|
|
|
+ assertEquals(88L, t.getSourceId());
|
|
|
|
|
+ assertEquals(10L, t.getMemberId());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== helpers ====================
|
|
|
|
|
+
|
|
|
|
|
+ private static TaskPlanInstance taskPlan(Long id, String status) {
|
|
|
|
|
+ TaskPlanInstance p = new TaskPlanInstance();
|
|
|
|
|
+ p.setId(id);
|
|
|
|
|
+ p.setFamilyId(7L);
|
|
|
|
|
+ p.setMemberId(10L);
|
|
|
|
|
+ p.setName("DAN测评-成长方案");
|
|
|
|
|
+ p.setStatus(status);
|
|
|
|
|
+ p.setTotalDays(14);
|
|
|
|
|
+ p.setCompletedDays(0);
|
|
|
|
|
+ p.setCreatedAt(new Date());
|
|
|
|
|
+ p.setUpdatedAt(new Date());
|
|
|
|
|
+ return p;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|