|
|
@@ -0,0 +1,128 @@
|
|
|
+package com.etotem.cfc.unit;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
|
|
+import com.etotem.cfc.entity.HealthPlan;
|
|
|
+import com.etotem.cfc.entity.Task;
|
|
|
+import com.etotem.cfc.mapper.HealthPlanMapper;
|
|
|
+import com.etotem.cfc.mapper.TaskMapper;
|
|
|
+import com.etotem.cfc.service.impl.HealthPlanServiceImpl;
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.mockito.ArgumentCaptor;
|
|
|
+import org.mockito.InOrder;
|
|
|
+
|
|
|
+import java.lang.reflect.Field;
|
|
|
+import java.util.Arrays;
|
|
|
+import java.util.Collections;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.*;
|
|
|
+import static org.mockito.ArgumentMatchers.any;
|
|
|
+import static org.mockito.Mockito.*;
|
|
|
+
|
|
|
+class HealthPlanDeleteCascadeTest {
|
|
|
+
|
|
|
+ private HealthPlanServiceImpl service;
|
|
|
+ private HealthPlanMapper mapper;
|
|
|
+ private TaskMapper taskMapper;
|
|
|
+
|
|
|
+ @BeforeEach
|
|
|
+ void setUp() throws Exception {
|
|
|
+ service = new HealthPlanServiceImpl();
|
|
|
+ mapper = mock(HealthPlanMapper.class);
|
|
|
+ taskMapper = mock(TaskMapper.class);
|
|
|
+ Field f = HealthPlanServiceImpl.class.getDeclaredField("healthPlanMapper");
|
|
|
+ f.setAccessible(true);
|
|
|
+ f.set(service, mapper);
|
|
|
+ Field tf = HealthPlanServiceImpl.class.getDeclaredField("taskMapper");
|
|
|
+ tf.setAccessible(true);
|
|
|
+ tf.set(service, taskMapper);
|
|
|
+ }
|
|
|
+
|
|
|
+ private HealthPlan planOf(Long id, Long familyId, String status) {
|
|
|
+ HealthPlan p = new HealthPlan();
|
|
|
+ p.setId(id);
|
|
|
+ p.setFamilyId(familyId);
|
|
|
+ p.setStatus(status);
|
|
|
+ return p;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Task taskOf(Long id) {
|
|
|
+ Task t = new Task();
|
|
|
+ t.setId(id);
|
|
|
+ return t;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void 级联删除_先删每日实例再删直接任务并删方案() {
|
|
|
+ when(mapper.selectById(5L)).thenReturn(planOf(5L, 10L, "published"));
|
|
|
+ // 直接任务:1 个周期模板 + 1 个一次性任务
|
|
|
+ when(taskMapper.selectList(any(Wrapper.class)))
|
|
|
+ .thenReturn(Arrays.asList(taskOf(101L), taskOf(102L)));
|
|
|
+ // 第 1 次 delete(实例)影响 3 行;第 2 次 delete(直接任务)影响 2 行
|
|
|
+ when(taskMapper.delete(any(Wrapper.class))).thenReturn(3, 2);
|
|
|
+
|
|
|
+ Map<String, Object> r = service.deletePlanCascade(5L, 10L);
|
|
|
+
|
|
|
+ assertEquals(5, ((Number) r.get("taskCount")).intValue(), "taskCount = 实例3 + 直接任务2");
|
|
|
+ // 两次 delete 的语义断言:第一次按 template_id 删每日实例,第二次按 source_type+source_id 删直接任务
|
|
|
+ ArgumentCaptor<Wrapper> captor = ArgumentCaptor.forClass(Wrapper.class);
|
|
|
+ verify(taskMapper, times(2)).delete(captor.capture());
|
|
|
+ List<Wrapper> wrappers = captor.getAllValues();
|
|
|
+ assertTrue(wrappers.get(0).getTargetSql().contains("template_id"),
|
|
|
+ "第一次删除必须按 template_id(每日实例),实际: " + wrappers.get(0).getTargetSql());
|
|
|
+ String secondSql = wrappers.get(1).getTargetSql();
|
|
|
+ assertTrue(secondSql.contains("source_type") && secondSql.contains("source_id"),
|
|
|
+ "第二次删除必须按 source_type+source_id(直接任务),实际: " + secondSql);
|
|
|
+ // 任务删除必须先于方案删除
|
|
|
+ InOrder inOrder = inOrder(taskMapper, mapper);
|
|
|
+ inOrder.verify(taskMapper, atLeastOnce()).delete(any(Wrapper.class));
|
|
|
+ inOrder.verify(mapper).deleteById(5L);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void 无任务的方案删除时只删方案不触发实例删除() {
|
|
|
+ when(mapper.selectById(7L)).thenReturn(planOf(7L, 10L, "draft"));
|
|
|
+ when(taskMapper.selectList(any(Wrapper.class))).thenReturn(Collections.<Task>emptyList());
|
|
|
+ when(taskMapper.delete(any(Wrapper.class))).thenReturn(0);
|
|
|
+
|
|
|
+ Map<String, Object> r = service.deletePlanCascade(7L, 10L);
|
|
|
+
|
|
|
+ assertEquals(0, ((Number) r.get("taskCount")).intValue());
|
|
|
+ verify(taskMapper, times(1)).delete(any(Wrapper.class)); // 只删直接任务(0 行),不删实例
|
|
|
+ verify(mapper).deleteById(7L);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void 方案不属于该家庭返回null且不删任何数据() {
|
|
|
+ when(mapper.selectById(5L)).thenReturn(planOf(5L, 10L, "published"));
|
|
|
+
|
|
|
+ assertNull(service.deletePlanCascade(5L, 20L), "familyId 不匹配必须整体拒绝");
|
|
|
+ verify(taskMapper, never()).delete(any(Wrapper.class));
|
|
|
+ verify(mapper, never()).deleteById(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void 方案不存在抛方案不存在() {
|
|
|
+ when(mapper.selectById(404L)).thenReturn(null);
|
|
|
+
|
|
|
+ RuntimeException ex = assertThrows(RuntimeException.class,
|
|
|
+ () -> service.deletePlanCascade(404L, 10L));
|
|
|
+ assertEquals("方案不存在", ex.getMessage());
|
|
|
+ verify(mapper, never()).deleteById(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void 管理端删除跳过familyId归属校验() {
|
|
|
+ when(mapper.selectById(5L)).thenReturn(planOf(5L, 10L, "rejected"));
|
|
|
+ when(taskMapper.selectList(any(Wrapper.class))).thenReturn(Arrays.asList(taskOf(300L)));
|
|
|
+ when(taskMapper.delete(any(Wrapper.class))).thenReturn(1, 1);
|
|
|
+
|
|
|
+ Map<String, Object> r = service.deletePlanCascade(5L, null);
|
|
|
+
|
|
|
+ assertNotNull(r);
|
|
|
+ assertEquals(2, ((Number) r.get("taskCount")).intValue());
|
|
|
+ verify(mapper).deleteById(5L);
|
|
|
+ }
|
|
|
+}
|