|
|
@@ -0,0 +1,174 @@
|
|
|
+package com.etotem.cfc.service;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
|
|
+import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
|
|
+import com.etotem.cfc.dto.TaskReviewDTO;
|
|
|
+import com.etotem.cfc.entity.FamilyMember;
|
|
|
+import com.etotem.cfc.entity.FamilyRelationshipScore;
|
|
|
+import com.etotem.cfc.entity.PointsLog;
|
|
|
+import com.etotem.cfc.entity.Task;
|
|
|
+import com.etotem.cfc.mapper.FamilyMemberMapper;
|
|
|
+import com.etotem.cfc.mapper.FamilyRelationshipScoreMapper;
|
|
|
+import com.etotem.cfc.mapper.PointsLogMapper;
|
|
|
+import com.etotem.cfc.mapper.TaskExecutionMapper;
|
|
|
+import com.etotem.cfc.mapper.TaskMapper;
|
|
|
+import org.apache.ibatis.builder.MapperBuilderAssistant;
|
|
|
+import org.junit.jupiter.api.BeforeEach;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.mockito.ArgumentCaptor;
|
|
|
+import org.mockito.InjectMocks;
|
|
|
+import org.mockito.Mock;
|
|
|
+import org.mockito.MockitoAnnotations;
|
|
|
+
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Date;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertNull;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
+import static org.mockito.ArgumentMatchers.any;
|
|
|
+import static org.mockito.ArgumentMatchers.anyLong;
|
|
|
+import static org.mockito.Mockito.never;
|
|
|
+import static org.mockito.Mockito.verify;
|
|
|
+import static org.mockito.Mockito.when;
|
|
|
+
|
|
|
+/**
|
|
|
+ * TDD 覆盖驱动(S-cfc-15 / 能力键 task.parent.review · GREEN 收口):
|
|
|
+ * TaskService.reviewTask 判定「优先级 / 缺省驳回」三处边界契约(服务层)。
|
|
|
+ *
|
|
|
+ * 判定契约(TaskService.reviewTask,TaskService.java:712-743):
|
|
|
+ * <pre>
|
|
|
+ * if (dto.getApproved() != null) { isApproved = dto.getApproved(); }
|
|
|
+ * else if ("pass".equals(dto.getResult())) { isApproved = true; }
|
|
|
+ * else { isApproved = false; } // 含 approved==null 且 result 非 pass(含 null)的缺省驳回
|
|
|
+ * </pre>
|
|
|
+ * S-cfc-13 已覆盖:approved=true(通过)/ approved=false(驳回)/ result="pass"(通过)/
|
|
|
+ * result="reject"(驳回)/ 重复审批幂等 / 非待审 / 不存在。本类仅补三处未显式覆盖的边界:
|
|
|
+ * 1. approved==null && result==null → 缺省驳回(else 兜底);
|
|
|
+ * 2. approved==false && result="pass" → approved 优先,仍驳回;
|
|
|
+ * 3. approved==true && result="reject" → approved 优先,仍通过(发分 + completed)。
|
|
|
+ *
|
|
|
+ * 测试隔离:@InjectMocks TaskService + mock Mapper/Service,无 Spring 上下文 / 无 DB / 无外部服务;
|
|
|
+ * 与 TaskServiceReviewTest 同构(child.userId=null → updateProgress 提前返回;deadline=null →
|
|
|
+ * 积分结算走打卡类基础分分支)。不改 src/main 实现。
|
|
|
+ *
|
|
|
+ * 溯源:SC-P-11 · tests/test-cases/parent.md TC-P-06/TC-P-06-E · TaskService.reviewTask
|
|
|
+ */
|
|
|
+class TaskReviewDecisionBoundaryServiceTest {
|
|
|
+
|
|
|
+ @Mock
|
|
|
+ private TaskMapper taskMapper;
|
|
|
+ @Mock
|
|
|
+ private FamilyMemberMapper familyMemberMapper;
|
|
|
+ @Mock
|
|
|
+ private PointsLogMapper pointsLogMapper;
|
|
|
+ @Mock
|
|
|
+ private StreakService streakService;
|
|
|
+ @Mock
|
|
|
+ private EnergyService energyService;
|
|
|
+ @Mock
|
|
|
+ private CfPathwayEarnService cfPathwayEarnService;
|
|
|
+ @Mock
|
|
|
+ private UserService userService;
|
|
|
+ @Mock
|
|
|
+ private FamilyRelationshipScoreMapper familyRelationshipScoreMapper;
|
|
|
+ @Mock
|
|
|
+ private TaskExecutionMapper taskExecutionMapper;
|
|
|
+
|
|
|
+ @InjectMocks
|
|
|
+ private TaskService taskService;
|
|
|
+
|
|
|
+ @BeforeEach
|
|
|
+ void setUp() {
|
|
|
+ MockitoAnnotations.openMocks(this);
|
|
|
+ TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), Task.class);
|
|
|
+ TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), FamilyMember.class);
|
|
|
+ TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), ""), FamilyRelationshipScore.class);
|
|
|
+ when(taskMapper.selectList(any())).thenReturn(new ArrayList<>());
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 已提交待审核任务(B 已完成,等家长审批)。 */
|
|
|
+ private Task pendingReviewTask(Long id) {
|
|
|
+ Task t = new Task();
|
|
|
+ t.setId(id);
|
|
|
+ t.setFamilyId(10L);
|
|
|
+ t.setFamilyMemberId(11L);
|
|
|
+ t.setCreatorId(5L);
|
|
|
+ t.setTitle("每天读书30分钟");
|
|
|
+ t.setPoints(10);
|
|
|
+ t.setNeedReview(1);
|
|
|
+ t.setStatus("pending_review");
|
|
|
+ t.setCompletedAt(new Date());
|
|
|
+ return t;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 未绑定登录账号的家庭成员(totalPoints 初始 100,跳过成长任务进度更新)。 */
|
|
|
+ private FamilyMember child(Long id) {
|
|
|
+ FamilyMember m = new FamilyMember();
|
|
|
+ m.setId(id);
|
|
|
+ m.setUserId(null);
|
|
|
+ m.setTotalPoints(100);
|
|
|
+ m.setStreakDays(0);
|
|
|
+ m.setPenaltyEnabled(0);
|
|
|
+ return m;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 判定优先级 / 缺省驳回 ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void reviewTask_approvedNull_resultNull_defaultsToReject_noAward() {
|
|
|
+ when(taskMapper.selectById(1L)).thenReturn(pendingReviewTask(1L));
|
|
|
+ TaskReviewDTO dto = new TaskReviewDTO(); // approved=null, result=null → else 兜底驳回
|
|
|
+
|
|
|
+ assertTrue(taskService.reviewTask(1L, dto));
|
|
|
+
|
|
|
+ // 缺省驳回:status → pending、completedAt 清空、不发分、不触碰成员
|
|
|
+ ArgumentCaptor<Task> captor = ArgumentCaptor.forClass(Task.class);
|
|
|
+ verify(taskMapper).updateById(captor.capture());
|
|
|
+ assertEquals("pending", captor.getValue().getStatus());
|
|
|
+ assertNull(captor.getValue().getCompletedAt());
|
|
|
+ verify(pointsLogMapper, never()).insert(any(PointsLog.class));
|
|
|
+ verify(familyMemberMapper, never()).selectById(anyLong());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void reviewTask_approvedFalse_resultPass_approvedWinsReject() {
|
|
|
+ when(taskMapper.selectById(1L)).thenReturn(pendingReviewTask(1L));
|
|
|
+ TaskReviewDTO dto = new TaskReviewDTO();
|
|
|
+ dto.setApproved(false);
|
|
|
+ dto.setResult("pass"); // result 声明通过,但 approved 优先 → 驳回
|
|
|
+
|
|
|
+ assertTrue(taskService.reviewTask(1L, dto));
|
|
|
+
|
|
|
+ ArgumentCaptor<Task> captor = ArgumentCaptor.forClass(Task.class);
|
|
|
+ verify(taskMapper).updateById(captor.capture());
|
|
|
+ assertEquals("pending", captor.getValue().getStatus());
|
|
|
+ assertNull(captor.getValue().getCompletedAt());
|
|
|
+ verify(pointsLogMapper, never()).insert(any(PointsLog.class));
|
|
|
+ verify(familyMemberMapper, never()).selectById(anyLong());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void reviewTask_approvedTrue_resultReject_approvedWinsApprove_awardsPoints() {
|
|
|
+ when(taskMapper.selectById(1L)).thenReturn(pendingReviewTask(1L));
|
|
|
+ FamilyMember member = child(11L);
|
|
|
+ when(familyMemberMapper.selectById(11L)).thenReturn(member);
|
|
|
+ TaskReviewDTO dto = new TaskReviewDTO();
|
|
|
+ dto.setApproved(true);
|
|
|
+ dto.setResult("reject"); // result 声明驳回,但 approved 优先 → 通过
|
|
|
+
|
|
|
+ assertTrue(taskService.reviewTask(1L, dto));
|
|
|
+
|
|
|
+ // 通过:发分(PointsLog amount=10/type=earn)+ 余额 100→110 + 任务 completed
|
|
|
+ ArgumentCaptor<PointsLog> logCaptor = ArgumentCaptor.forClass(PointsLog.class);
|
|
|
+ verify(pointsLogMapper).insert(logCaptor.capture());
|
|
|
+ assertEquals(Integer.valueOf(10), logCaptor.getValue().getAmount());
|
|
|
+ assertEquals("earn", logCaptor.getValue().getType());
|
|
|
+ assertEquals(Long.valueOf(11L), logCaptor.getValue().getMemberId());
|
|
|
+ assertEquals(Integer.valueOf(110), member.getTotalPoints());
|
|
|
+
|
|
|
+ ArgumentCaptor<Task> captor = ArgumentCaptor.forClass(Task.class);
|
|
|
+ verify(taskMapper).updateById(captor.capture());
|
|
|
+ assertEquals("completed", captor.getValue().getStatus());
|
|
|
+ }
|
|
|
+}
|