diff --git a/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/product/ProductRequirementServiceImplTest.java b/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/product/ProductRequirementServiceImplTest.java new file mode 100644 index 0000000..845a0b9 --- /dev/null +++ b/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/product/ProductRequirementServiceImplTest.java @@ -0,0 +1,671 @@ +package com.njcn.rdms.module.project.service.product; + +import com.njcn.rdms.framework.common.exception.ServiceException; +import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils; +import com.njcn.rdms.framework.test.core.ut.BaseMockitoUnitTest; +import com.njcn.rdms.module.project.controller.admin.product.vo.requirement.*; +import com.njcn.rdms.module.project.dal.dataobject.audit.BizAuditLogDO; +import com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementDO; +import com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementModuleDO; +import com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementStatusLogDO; +import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusTransitionDO; +import com.njcn.rdms.module.project.dal.mysql.audit.BizAuditLogMapper; +import com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementMapper; +import com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementModuleMapper; +import com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementStatusLogMapper; +import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper; +import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMapper; +import com.njcn.rdms.module.project.enums.ErrorCodeConstants; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; + +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +/** + * 产品需求 Service 单元测试 + */ +class ProductRequirementServiceImplTest extends BaseMockitoUnitTest { + + @InjectMocks + private ProductRequirementServiceImpl requirementService; + @Mock + private ProductRequirementMapper requirementMapper; + @Mock + private ProductRequirementModuleMapper moduleMapper; + @Mock + private ProductRequirementStatusLogMapper statusLogMapper; + @Mock + private BizAuditLogMapper bizAuditLogMapper; + @Mock + private ObjectStatusTransitionMapper statusTransitionMapper; + @Mock + private ObjectStatusModelMapper statusModelMapper; + + // ========== 创建需求测试 ========== + + @Test + void createRequirement_withoutReview_shouldCreateWithPendingDispatchStatus() { + Long loginUserId = 1001L; + Long defaultModuleId = 50L; + ProductRequirementSaveReqVO reqVO = new ProductRequirementSaveReqVO(); + reqVO.setProductId(100L); + reqVO.setTitle("测试需求"); + reqVO.setDescription("测试描述"); + reqVO.setCategory("function"); + reqVO.setReviewRequired(0); // 不需要评审 + reqVO.setPriority(1); + reqVO.setProposerId(2001L); + reqVO.setCurrentHandlerUserId(2002L); + reqVO.setCompletionDate(LocalDateTime.now()); + + // 模拟"全部需求"模块存在(parentId = 0L 的根模块) + ProductRequirementModuleDO defaultModule = createDefaultModule(defaultModuleId, 100L); + when(moduleMapper.selectByProductIdAndParentId(100L, 0L)).thenReturn(defaultModule); + + try (MockedStatic mockedStatic = mockLoginUser(loginUserId, "测试人")) { + requirementService.createRequirement(reqVO); + } + + ArgumentCaptor captor = ArgumentCaptor.forClass(ProductRequirementDO.class); + verify(requirementMapper, times(1)).insert(captor.capture()); + ProductRequirementDO created = captor.getValue(); + assertEquals("pending_dispatch", created.getStatusCode()); // 不需要评审时初始状态为待分流 + assertEquals("manual", created.getSourceType()); + assertEquals(0L, created.getParentId()); + assertEquals("测试需求", created.getTitle()); + assertEquals(defaultModuleId, created.getModuleId()); // 未选择模块时自动归属到"全部需求"模块 + assertEquals(100L, created.getProductId()); + } + + @Test + void createRequirement_withReview_shouldCreateWithPendingReviewStatus() { + Long loginUserId = 1001L; + Long defaultModuleId = 50L; + ProductRequirementSaveReqVO reqVO = new ProductRequirementSaveReqVO(); + reqVO.setProductId(100L); + reqVO.setTitle("需评审的需求"); + reqVO.setCategory("function"); + reqVO.setReviewRequired(1); // 需要评审 + reqVO.setPriority(2); + reqVO.setProposerId(2001L); + reqVO.setCompletionDate(LocalDateTime.now()); + + // 模拟"全部需求"模块存在(parentId = 0L 的根模块) + ProductRequirementModuleDO defaultModule = createDefaultModule(defaultModuleId, 100L); + when(moduleMapper.selectByProductIdAndParentId(100L, 0L)).thenReturn(defaultModule); + + try (MockedStatic mockedStatic = mockLoginUser(loginUserId, "测试人")) { + requirementService.createRequirement(reqVO); + } + + ArgumentCaptor captor = ArgumentCaptor.forClass(ProductRequirementDO.class); + verify(requirementMapper, times(1)).insert(captor.capture()); + ProductRequirementDO created = captor.getValue(); + assertEquals("pending_review", created.getStatusCode()); // 需要评审时初始状态为待评审 + assertEquals(defaultModuleId, created.getModuleId()); // 未选择模块时自动归属到"全部需求"模块 + } + + // ========== 更新需求测试 ========== + + @Test + void updateRequirement_whenTerminalStatus_shouldThrowException() { + Long requirementId = 1001L; + ProductRequirementDO requirement = createRequirement(requirementId, 100L, "已关闭需求", + "closed", 0L, 0); + ProductRequirementUpdateReqVO reqVO = new ProductRequirementUpdateReqVO(); + reqVO.setId(requirementId); + reqVO.setProductId(100L); + reqVO.setTitle("修改标题"); + reqVO.setCategory("function"); + reqVO.setReviewRequired(0); + reqVO.setPriority(1); + reqVO.setProposerId(2001L); + reqVO.setCompletionDate(LocalDateTime.now()); + + when(requirementMapper.selectById(requirementId)).thenReturn(requirement); + + ServiceException ex = assertThrows(ServiceException.class, + () -> requirementService.updateRequirement(reqVO)); + assertEquals(ErrorCodeConstants.REQUIREMENT_STATUS_NOT_ALLOW_EDIT.getCode(), ex.getCode()); + verify(requirementMapper, never()).updateById(any(ProductRequirementDO.class)); + } + + @Test + void updateRequirement_whenNormalStatus_shouldUpdateSuccessfully() { + Long requirementId = 1002L; + Long loginUserId = 1001L; + Long defaultModuleId = 50L; + ProductRequirementDO requirement = createRequirement(requirementId, 100L, "待分流需求", + "pending_dispatch", 0L, 0); + ProductRequirementUpdateReqVO reqVO = new ProductRequirementUpdateReqVO(); + reqVO.setId(requirementId); + reqVO.setProductId(100L); + reqVO.setTitle("修改后的标题"); + reqVO.setCategory("security"); + reqVO.setReviewRequired(0); + reqVO.setPriority(2); + reqVO.setProposerId(2001L); + reqVO.setCompletionDate(LocalDateTime.now()); + + // 模拟"全部需求"模块存在(未选择模块时自动归属,parentId = 0L 的根模块) + ProductRequirementModuleDO defaultModule = createDefaultModule(defaultModuleId, 100L); + when(moduleMapper.selectByProductIdAndParentId(100L, 0L)).thenReturn(defaultModule); + when(requirementMapper.selectById(requirementId)).thenReturn(requirement); + + try (MockedStatic mockedStatic = mockLoginUser(loginUserId, "测试人")) { + requirementService.updateRequirement(reqVO); + } + + ArgumentCaptor captor = ArgumentCaptor.forClass(ProductRequirementDO.class); + verify(requirementMapper, times(1)).updateById(captor.capture()); + assertEquals(defaultModuleId, captor.getValue().getModuleId()); // 未选择模块时自动归属到"全部需求"模块 + verify(bizAuditLogMapper, times(1)).insert(any(BizAuditLogDO.class)); + } + + // ========== 状态变更测试 ========== + + @Test + void changeRequirementStatus_whenActionAllowed_shouldUpdateStatusAndWriteLogs() { + Long requirementId = 1003L; + Long loginUserId = 1001L; + ProductRequirementDO requirement = createRequirement(requirementId, 100L, "待分流需求", + "pending_dispatch", 0L, 0); + ProductRequirementStatusActionReqVO reqVO = new ProductRequirementStatusActionReqVO(); + reqVO.setId(requirementId); + reqVO.setActionCode("dispatch"); + reqVO.setReason(null); + + when(requirementMapper.selectById(requirementId)).thenReturn(requirement); + when(statusTransitionMapper.selectByObjectTypeAndFromStatusAndAction( + "product_requirement", "pending_dispatch", "dispatch")) + .thenReturn(createTransition("dispatch", "implementing", false)); + when(requirementMapper.updateStatusByIdAndStatus(requirementId, "pending_dispatch", "implementing", null)) + .thenReturn(1); + + try (MockedStatic mockedStatic = mockLoginUser(loginUserId, "测试人")) { + requirementService.changeRequirementStatus(reqVO); + } + + verify(requirementMapper, times(1)) + .updateStatusByIdAndStatus(requirementId, "pending_dispatch", "implementing", null); + verify(statusLogMapper, times(1)).insert(any(ProductRequirementStatusLogDO.class)); + verify(bizAuditLogMapper, times(1)).insert(any(BizAuditLogDO.class)); + } + + @Test + void changeRequirementStatus_whenActionNotAllowed_shouldThrowException() { + Long requirementId = 1004L; + ProductRequirementDO requirement = createRequirement(requirementId, 100L, "已关闭需求", + "closed", 0L, 0); + ProductRequirementStatusActionReqVO reqVO = new ProductRequirementStatusActionReqVO(); + reqVO.setId(requirementId); + reqVO.setActionCode("dispatch"); + + when(requirementMapper.selectById(requirementId)).thenReturn(requirement); + when(statusTransitionMapper.selectByObjectTypeAndFromStatusAndAction( + "product_requirement", "closed", "dispatch")) + .thenReturn(null); + + ServiceException ex = assertThrows(ServiceException.class, + () -> requirementService.changeRequirementStatus(reqVO)); + assertEquals(ErrorCodeConstants.REQUIREMENT_STATUS_ACTION_NOT_ALLOWED.getCode(), ex.getCode()); + } + + @Test + void changeRequirementStatus_whenReasonRequiredButMissing_shouldThrowException() { + Long requirementId = 1005L; + ProductRequirementDO requirement = createRequirement(requirementId, 100L, "待分流需求", + "pending_dispatch", 0L, 0); + ProductRequirementStatusActionReqVO reqVO = new ProductRequirementStatusActionReqVO(); + reqVO.setId(requirementId); + reqVO.setActionCode("cancel"); + reqVO.setReason(" "); // 空原因 + + when(requirementMapper.selectById(requirementId)).thenReturn(requirement); + when(statusTransitionMapper.selectByObjectTypeAndFromStatusAndAction( + "product_requirement", "pending_dispatch", "cancel")) + .thenReturn(createTransition("cancel", "cancelled", true)); + + ServiceException ex = assertThrows(ServiceException.class, + () -> requirementService.changeRequirementStatus(reqVO)); + assertEquals(ErrorCodeConstants.REQUIREMENT_STATUS_ACTION_REASON_REQUIRED.getCode(), ex.getCode()); + } + + @Test + void changeRequirementStatus_whenConcurrentModified_shouldThrowException() { + Long requirementId = 1006L; + Long loginUserId = 1001L; + ProductRequirementDO requirement = createRequirement(requirementId, 100L, "待分流需求", + "pending_dispatch", 0L, 0); + ProductRequirementStatusActionReqVO reqVO = new ProductRequirementStatusActionReqVO(); + reqVO.setId(requirementId); + reqVO.setActionCode("dispatch"); + + when(requirementMapper.selectById(requirementId)).thenReturn(requirement); + when(statusTransitionMapper.selectByObjectTypeAndFromStatusAndAction( + "product_requirement", "pending_dispatch", "dispatch")) + .thenReturn(createTransition("dispatch", "implementing", false)); + when(requirementMapper.updateStatusByIdAndStatus(requirementId, "pending_dispatch", "implementing", null)) + .thenReturn(0); // 并发修改,更新失败 + + try (MockedStatic mockedStatic = mockLoginUser(loginUserId, "测试人")) { + ServiceException ex = assertThrows(ServiceException.class, + () -> requirementService.changeRequirementStatus(reqVO)); + assertEquals(ErrorCodeConstants.REQUIREMENT_STATUS_CONCURRENT_MODIFIED.getCode(), ex.getCode()); + } + verify(statusLogMapper, never()).insert(any(ProductRequirementStatusLogDO.class)); + verify(bizAuditLogMapper, never()).insert(any(BizAuditLogDO.class)); + } + + // ========== 删除需求测试 ========== + + @Test + void deleteRequirement_shouldDeleteByConditionAndWriteLogs() { + Long requirementId = 1007L; + Long loginUserId = 1001L; + ProductRequirementDO requirement = createRequirement(requirementId, 100L, "待分流需求", + "pending_dispatch", 0L, 0); + + when(requirementMapper.selectById(requirementId)).thenReturn(requirement); + when(requirementMapper.deleteByIdAndStatus(requirementId, "pending_dispatch")).thenReturn(1); + + try (MockedStatic mockedStatic = mockLoginUser(loginUserId, "测试人")) { + requirementService.deleteRequirement(requirementId, 100L); + } + + verify(requirementMapper, times(1)).deleteByIdAndStatus(requirementId, "pending_dispatch"); + verify(bizAuditLogMapper, times(1)).insert(any(BizAuditLogDO.class)); + } + + // ========== 拆分需求测试 ========== + + @Test + void splitRequirement_whenParentPendingDispatch_shouldChangeParentToImplementing() { + Long parentId = 1008L; + Long loginUserId = 1001L; + ProductRequirementDO parent = createRequirement(parentId, 100L, "大需求", + "pending_dispatch", 0L, 0); + parent.setModuleId(50L); + parent.setProposerId(2001L); + ProductRequirementSplitReqVO reqVO = new ProductRequirementSplitReqVO(); + reqVO.setParentId(parentId); + reqVO.setProductId(100L); + reqVO.setTitle("子需求"); + reqVO.setCategory("function"); + reqVO.setPriority(1); + reqVO.setCompletionDate(LocalDateTime.now()); + + when(requirementMapper.selectById(parentId)).thenReturn(parent); + when(requirementMapper.updateStatusByIdAndStatus(parentId, "pending_dispatch", "implementing", null)) + .thenReturn(1); + + try (MockedStatic mockedStatic = mockLoginUser(loginUserId, "测试人")) { + requirementService.splitRequirement(reqVO); + } + + // 验证子需求被创建 + ArgumentCaptor childCaptor = ArgumentCaptor.forClass(ProductRequirementDO.class); + verify(requirementMapper, times(1)).insert(childCaptor.capture()); + ProductRequirementDO child = childCaptor.getValue(); + assertEquals(parentId, child.getParentId()); + assertEquals("pending_dispatch", child.getStatusCode()); + assertEquals(parent.getModuleId(), child.getModuleId()); + assertEquals(parent.getProposerId(), child.getProposerId()); + + // 验证父需求状态变为实施中 + verify(requirementMapper, times(1)) + .updateStatusByIdAndStatus(parentId, "pending_dispatch", "implementing", null); + verify(statusLogMapper, times(1)).insert(any(ProductRequirementStatusLogDO.class)); + } + + @Test + void splitRequirement_whenParentImplementing_shouldNotChangeParentStatus() { + Long parentId = 1009L; + Long loginUserId = 1001L; + ProductRequirementDO parent = createRequirement(parentId, 100L, "实施中的大需求", + "implementing", 0L, 0); + parent.setModuleId(50L); + parent.setProposerId(2001L); + ProductRequirementSplitReqVO reqVO = new ProductRequirementSplitReqVO(); + reqVO.setParentId(parentId); + reqVO.setProductId(100L); + reqVO.setTitle("子需求2"); + reqVO.setCategory("function"); + reqVO.setPriority(1); + reqVO.setCompletionDate(LocalDateTime.now()); + + when(requirementMapper.selectById(parentId)).thenReturn(parent); + + try (MockedStatic mockedStatic = mockLoginUser(loginUserId, "测试人")) { + requirementService.splitRequirement(reqVO); + } + + // 父需求已经是实施中,不需要再更新状态 + verify(requirementMapper, never()) + .updateStatusByIdAndStatus(parentId, "implementing", "implementing", null); + } + + @Test + void splitRequirement_whenParentNotAllowSplit_shouldThrowException() { + Long parentId = 1010L; + ProductRequirementDO parent = createRequirement(parentId, 100L, "已验收的大需求", + "accepted", 0L, 0); + ProductRequirementSplitReqVO reqVO = new ProductRequirementSplitReqVO(); + reqVO.setParentId(parentId); + reqVO.setProductId(100L); + reqVO.setTitle("子需求"); + reqVO.setCategory("function"); + reqVO.setPriority(1); + reqVO.setCompletionDate(LocalDateTime.now()); + + when(requirementMapper.selectById(parentId)).thenReturn(parent); + + ServiceException ex = assertThrows(ServiceException.class, + () -> requirementService.splitRequirement(reqVO)); + assertEquals(ErrorCodeConstants.REQUIREMENT_PARENT_NOT_ALLOW_SPLIT.getCode(), ex.getCode()); + verify(requirementMapper, never()).insert(any(ProductRequirementDO.class)); + } + + // ========== 关闭需求测试 ========== + + @Test + void closeRequirement_whenAccepted_shouldCloseSuccessfully() { + Long requirementId = 1011L; + Long loginUserId = 1001L; + ProductRequirementDO requirement = createRequirement(requirementId, 100L, "已验收需求", + "accepted", 0L, 0); + ProductRequirementCloseReqVO reqVO = new ProductRequirementCloseReqVO(); + reqVO.setId(requirementId); + reqVO.setProductId(100L); + reqVO.setReason("需求已完成"); + + when(requirementMapper.selectById(requirementId)).thenReturn(requirement); + when(requirementMapper.selectListByParentId(requirementId)).thenReturn(Collections.emptyList()); + when(requirementMapper.updateStatusByIdAndStatus(requirementId, "accepted", "closed", "需求已完成")) + .thenReturn(1); + + try (MockedStatic mockedStatic = mockLoginUser(loginUserId, "测试人")) { + requirementService.closeRequirement(reqVO); + } + + verify(requirementMapper, times(1)) + .updateStatusByIdAndStatus(requirementId, "accepted", "closed", "需求已完成"); + verify(statusLogMapper, times(1)).insert(any(ProductRequirementStatusLogDO.class)); + verify(bizAuditLogMapper, times(1)).insert(any(BizAuditLogDO.class)); + } + + @Test + void closeRequirement_whenNotAccepted_shouldThrowException() { + Long requirementId = 1012L; + ProductRequirementDO requirement = createRequirement(requirementId, 100L, "实施中需求", + "implementing", 0L, 0); + ProductRequirementCloseReqVO reqVO = new ProductRequirementCloseReqVO(); + reqVO.setId(requirementId); + reqVO.setProductId(100L); + reqVO.setReason("需求已完成"); + + when(requirementMapper.selectById(requirementId)).thenReturn(requirement); + + ServiceException ex = assertThrows(ServiceException.class, + () -> requirementService.closeRequirement(reqVO)); + assertEquals(ErrorCodeConstants.REQUIREMENT_STATUS_NOT_ALLOW_CLOSE.getCode(), ex.getCode()); + } + + @Test + void closeRequirement_withChildren_shouldCascadeCloseAcceptedChildren() { + Long parentId = 1013L; + Long childId = 1014L; + Long loginUserId = 1001L; + ProductRequirementDO parent = createRequirement(parentId, 100L, "父需求", + "accepted", 0L, 0); + ProductRequirementDO child = createRequirement(childId, 100L, "子需求", + "accepted", parentId, 0); + + ProductRequirementCloseReqVO reqVO = new ProductRequirementCloseReqVO(); + reqVO.setId(parentId); + reqVO.setProductId(100L); + reqVO.setReason("全部验收完成"); + + when(requirementMapper.selectById(parentId)).thenReturn(parent); + when(requirementMapper.selectListByParentId(parentId)).thenReturn(List.of(child)); + when(requirementMapper.updateStatusByIdAndStatus(childId, "accepted", "closed", "全部验收完成")) + .thenReturn(1); + when(requirementMapper.updateStatusByIdAndStatus(parentId, "accepted", "closed", "全部验收完成")) + .thenReturn(1); + + try (MockedStatic mockedStatic = mockLoginUser(loginUserId, "测试人")) { + requirementService.closeRequirement(reqVO); + } + + // 子需求被关闭 + verify(requirementMapper, times(1)) + .updateStatusByIdAndStatus(childId, "accepted", "closed", "全部验收完成"); + // 父需求被关闭 + verify(requirementMapper, times(1)) + .updateStatusByIdAndStatus(parentId, "accepted", "closed", "全部验收完成"); + } + + @Test + void closeRequirement_withChildNotAllowClose_shouldThrowException() { + Long parentId = 1015L; + ProductRequirementDO parent = createRequirement(parentId, 100L, "父需求", + "accepted", 0L, 0); + ProductRequirementDO child = createRequirement(1016L, 100L, "实施中子需求", + "implementing", parentId, 0); + + ProductRequirementCloseReqVO reqVO = new ProductRequirementCloseReqVO(); + reqVO.setId(parentId); + reqVO.setProductId(100L); + reqVO.setReason("全部验收完成"); + + when(requirementMapper.selectById(parentId)).thenReturn(parent); + when(requirementMapper.selectListByParentId(parentId)).thenReturn(List.of(child)); + + ServiceException ex = assertThrows(ServiceException.class, + () -> requirementService.closeRequirement(reqVO)); + assertEquals(ErrorCodeConstants.REQUIREMENT_CHILD_NOT_ALLOW_CLOSE.getCode(), ex.getCode()); + } + + // ========== 模块管理测试 ========== + + @Test + void createRequirementModule_whenNameDuplicate_shouldThrowException() { + Long productId = 100L; + ProductRequirementModuleDO existModule = new ProductRequirementModuleDO(); + existModule.setId(50L); + existModule.setProductId(productId); + existModule.setModuleName("核心功能"); + + ProductRequirementModuleSaveReqVO reqVO = new ProductRequirementModuleSaveReqVO(); + reqVO.setProductId(productId); + reqVO.setModuleName("核心功能"); + + when(moduleMapper.selectByProductIdAndModuleName(productId, "核心功能")).thenReturn(existModule); + + ServiceException ex = assertThrows(ServiceException.class, + () -> requirementService.createRequirementModule(reqVO)); + assertEquals(ErrorCodeConstants.REQUIREMENT_MODULE_NAME_DUPLICATE.getCode(), ex.getCode()); + } + + @Test + void deleteRequirementModule_whenHasNonTerminalRequirements_shouldThrowException() { + Long moduleId = 50L; + Long productId = 100L; + ProductRequirementModuleDO module = new ProductRequirementModuleDO(); + module.setId(moduleId); + module.setProductId(productId); + module.setModuleName("核心功能"); + + ProductRequirementDO nonTerminalReq = createRequirement(2001L, productId, "实施中需求", + "implementing", 0L, 0); + nonTerminalReq.setModuleId(moduleId); + + when(moduleMapper.selectById(moduleId)).thenReturn(module); + when(requirementMapper.selectListByModuleId(moduleId)).thenReturn(List.of(nonTerminalReq)); + + ServiceException ex = assertThrows(ServiceException.class, + () -> requirementService.deleteRequirementModule(moduleId, productId)); + assertEquals(ErrorCodeConstants.REQUIREMENT_MODULE_HAS_NON_TERMINAL_REQUIREMENTS.getCode(), ex.getCode()); + verify(moduleMapper, never()).deleteById(moduleId); + } + + @Test + void deleteRequirementModule_whenAllTerminal_shouldDeleteSuccessfully() { + Long moduleId = 50L; + Long productId = 100L; + Long loginUserId = 1001L; + ProductRequirementModuleDO module = new ProductRequirementModuleDO(); + module.setId(moduleId); + module.setProductId(productId); + module.setModuleName("核心功能"); + + ProductRequirementDO closedReq = createRequirement(2001L, productId, "已关闭需求", + "closed", 0L, 0); + closedReq.setModuleId(moduleId); + + when(moduleMapper.selectById(moduleId)).thenReturn(module); + when(requirementMapper.selectListByModuleId(moduleId)).thenReturn(List.of(closedReq)); + + try (MockedStatic mockedStatic = mockLoginUser(loginUserId, "测试人")) { + requirementService.deleteRequirementModule(moduleId, productId); + } + + verify(requirementMapper, times(1)).deleteById(2001L); + verify(moduleMapper, times(1)).deleteById(moduleId); + } + + // ========== resolveModuleId 测试 ========== + + @Test + void resolveModuleId_whenModuleIdIsNull_shouldReturnDefaultModuleId() { + Long defaultModuleId = 50L; + Long productId = 100L; + ProductRequirementModuleDO defaultModule = createDefaultModule(defaultModuleId, productId); + + when(moduleMapper.selectByProductIdAndParentId(productId, 0L)).thenReturn(defaultModule); + + Long result = requirementService.resolveModuleId(null, productId); + + assertEquals(defaultModuleId, result); + } + + @Test + void resolveModuleId_whenModuleIdProvided_shouldReturnSameId() { + Long moduleId = 60L; + Long productId = 100L; + + // 传入有效moduleId时直接返回,不查询数据库 + Long result = requirementService.resolveModuleId(moduleId, productId); + + assertEquals(moduleId, result); + verify(moduleMapper, never()).selectByProductIdAndParentId(any(), any()); + } + + // ========== isAllRequirementsModule 测试 ========== + + @Test + void isAllRequirementsModule_whenRootModule_shouldReturnTrue() { + Long moduleId = 50L; + ProductRequirementModuleDO rootModule = createDefaultModule(moduleId, 100L); + + when(moduleMapper.selectById(moduleId)).thenReturn(rootModule); + + boolean result = requirementService.isAllRequirementsModule(moduleId); + + assertEquals(true, result); + } + + @Test + void isAllRequirementsModule_whenChildModule_shouldReturnFalse() { + Long moduleId = 60L; + ProductRequirementModuleDO childModule = new ProductRequirementModuleDO(); + childModule.setId(moduleId); + childModule.setProductId(100L); + childModule.setParentId(50L); // parentId != 0 + childModule.setModuleName("子模块"); + + when(moduleMapper.selectById(moduleId)).thenReturn(childModule); + + boolean result = requirementService.isAllRequirementsModule(moduleId); + + assertEquals(false, result); + } + + @Test + void isAllRequirementsModule_whenModuleNotExists_shouldReturnFalse() { + Long moduleId = 70L; + + when(moduleMapper.selectById(moduleId)).thenReturn(null); + + boolean result = requirementService.isAllRequirementsModule(moduleId); + + assertEquals(false, result); + } + + @Test + void resolveModuleId_whenDefaultModuleNotExists_shouldThrowException() { + Long productId = 100L; + + when(moduleMapper.selectByProductIdAndParentId(productId, 0L)).thenReturn(null); + + ServiceException ex = assertThrows(ServiceException.class, + () -> requirementService.resolveModuleId(null, productId)); + assertEquals(ErrorCodeConstants.REQUIREMENT_MODULE_NOT_EXISTS.getCode(), ex.getCode()); + } + + // ========== 辅助方法 ========== + + private ProductRequirementDO createRequirement(Long id, Long productId, String title, + String statusCode, Long parentId, Integer reviewRequired) { + ProductRequirementDO requirement = new ProductRequirementDO(); + requirement.setId(id); + requirement.setProductId(productId); + requirement.setTitle(title); + requirement.setStatusCode(statusCode); + requirement.setParentId(parentId); + requirement.setReviewRequired(reviewRequired); + requirement.setCategory("function"); + requirement.setSourceType("manual"); + requirement.setPriority(1); + requirement.setProposerId(2001L); + requirement.setSort(0); + return requirement; + } + + private ProductRequirementModuleDO createDefaultModule(Long id, Long productId) { + ProductRequirementModuleDO module = new ProductRequirementModuleDO(); + module.setId(id); + module.setProductId(productId); + module.setParentId(0L); + module.setModuleName("全部需求"); + module.setRemark("自动创建的模块"); + module.setSort(0); + return module; + } + + private ObjectStatusTransitionDO createTransition(String actionCode, String toStatus, boolean needReason) { + ObjectStatusTransitionDO transition = new ObjectStatusTransitionDO(); + transition.setActionCode(actionCode); + transition.setToStatusCode(toStatus); + transition.setNeedReason(needReason); + return transition; + } + + private MockedStatic mockLoginUser(Long loginUserId, String nickname) { + MockedStatic mockedStatic = mockStatic(SecurityFrameworkUtils.class); + mockedStatic.when(SecurityFrameworkUtils::getLoginUserId).thenReturn(loginUserId); + mockedStatic.when(SecurityFrameworkUtils::getLoginUserNickname).thenReturn(nickname); + return mockedStatic; + } + +} diff --git a/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/product/ProductServiceImplTest.java b/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/product/ProductServiceImplTest.java index 3c8ec17..bbb8252 100644 --- a/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/product/ProductServiceImplTest.java +++ b/rdms-project/rdms-project-boot/src/test/java/com/njcn/rdms/module/project/service/product/ProductServiceImplTest.java @@ -66,6 +66,43 @@ class ProductServiceImplTest extends BaseMockitoUnitTest { private ObjectPermissionApi objectPermissionApi; @Mock private AdminUserApi adminUserApi; + @Mock + private com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementModuleMapper requirementModuleMapper; + + @Test + void createProduct_shouldCreateDefaultRequirementModule() { + Long loginUserId = 3001L; + ProductSaveReqVO reqVO = new ProductSaveReqVO(); + reqVO.setDirectionCode("direction_value"); + reqVO.setName("新产品"); + reqVO.setManagerUserId(2001L); + reqVO.setDescription("产品描述"); + + when(productMapper.selectByName("新产品")).thenReturn(null); + when(adminUserApi.getUser(2001L)).thenReturn( + success(new com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO())); + + try (MockedStatic mockedStatic = mockLoginUser(loginUserId, "测试人")) { + productService.createProduct(reqVO); + } + + // 验证产品已创建 + ArgumentCaptor productCaptor = ArgumentCaptor.forClass(ProductDO.class); + verify(productMapper, times(1)).insert(productCaptor.capture()); + ProductDO createdProduct = productCaptor.getValue(); + assertEquals("新产品", createdProduct.getName()); + + // 验证"全部需求"模块已自动创建 + ArgumentCaptor moduleCaptor = + ArgumentCaptor.forClass(com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementModuleDO.class); + verify(requirementModuleMapper, times(1)).insert(moduleCaptor.capture()); + com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementModuleDO defaultModule = moduleCaptor.getValue(); + assertEquals(0L, defaultModule.getParentId()); + assertEquals(createdProduct.getId(), defaultModule.getProductId()); + assertEquals("全部需求", defaultModule.getModuleName()); + assertEquals("自动创建的模块", defaultModule.getRemark()); + assertEquals(0, defaultModule.getSort()); + } @Test void updateProductBaseInfo_shouldOnlyUpdateBaseInfoAndRecordFieldChanges() {