Commit 39c3681d949f53ba4c180c99a89033fc7a0176a2

Authored by YunaiV
1 parent c572b551

fix(crm): 校验合同提交的数据写权限

新增真实权限切面测试,覆盖负责人、协作权限与 CRM 管理员。
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java
@@ -295,6 +295,7 @@ public class CrmContractServiceImpl implements CrmContractService { @@ -295,6 +295,7 @@ public class CrmContractServiceImpl implements CrmContractService {
295 @Transactional(rollbackFor = Exception.class) 295 @Transactional(rollbackFor = Exception.class)
296 @LogRecord(type = CRM_CONTRACT_TYPE, subType = CRM_CONTRACT_SUBMIT_SUB_TYPE, bizNo = "{{#id}}", 296 @LogRecord(type = CRM_CONTRACT_TYPE, subType = CRM_CONTRACT_SUBMIT_SUB_TYPE, bizNo = "{{#id}}",
297 success = CRM_CONTRACT_SUBMIT_SUCCESS) 297 success = CRM_CONTRACT_SUBMIT_SUCCESS)
  298 + @CrmPermission(bizType = CrmBizTypeEnum.CRM_CONTRACT, bizId = "#id", level = CrmPermissionLevelEnum.WRITE)
298 public void submitContract(Long id, Long userId) { 299 public void submitContract(Long id, Long userId) {
299 // 1. 校验合同是否在审批 300 // 1. 校验合同是否在审批
300 CrmContractDO contract = validateContractExists(id); 301 CrmContractDO contract = validateContractExists(id);
yudao-module-crm/src/test/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImplTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.crm.service.contract;
  2 +
  3 +import cn.hutool.extra.spring.SpringUtil;
  4 +import cn.iocoder.yudao.framework.common.biz.system.permission.PermissionCommonApi;
  5 +import cn.iocoder.yudao.framework.security.core.LoginUser;
  6 +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
  7 +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
  8 +import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
  9 +import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
  10 +import cn.iocoder.yudao.module.crm.dal.dataobject.contract.CrmContractDO;
  11 +import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
  12 +import cn.iocoder.yudao.module.crm.dal.mysql.contract.CrmContractMapper;
  13 +import cn.iocoder.yudao.module.crm.dal.mysql.contract.CrmContractProductMapper;
  14 +import cn.iocoder.yudao.module.crm.dal.redis.no.CrmNoRedisDAO;
  15 +import cn.iocoder.yudao.module.crm.enums.common.CrmAuditStatusEnum;
  16 +import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
  17 +import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum;
  18 +import cn.iocoder.yudao.module.crm.framework.permission.core.aop.CrmPermissionAspect;
  19 +import cn.iocoder.yudao.module.crm.service.business.CrmBusinessService;
  20 +import cn.iocoder.yudao.module.crm.service.contact.CrmContactService;
  21 +import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
  22 +import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
  23 +import cn.iocoder.yudao.module.crm.service.product.CrmProductService;
  24 +import cn.iocoder.yudao.module.crm.service.receivable.CrmReceivableService;
  25 +import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
  26 +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
  27 +import cn.iocoder.yudao.module.system.enums.permission.RoleCodeEnum;
  28 +import org.junit.jupiter.api.AfterEach;
  29 +import org.junit.jupiter.api.BeforeEach;
  30 +import org.junit.jupiter.api.Test;
  31 +import org.mockito.InjectMocks;
  32 +import org.mockito.Mock;
  33 +import org.mockito.MockedStatic;
  34 +import org.springframework.aop.aspectj.annotation.AspectJProxyFactory;
  35 +import org.springframework.mock.web.MockHttpServletRequest;
  36 +import org.springframework.security.core.context.SecurityContextHolder;
  37 +import org.springframework.web.context.request.RequestContextHolder;
  38 +import org.springframework.web.context.request.ServletRequestAttributes;
  39 +
  40 +import java.util.Arrays;
  41 +import java.util.Collections;
  42 +import java.util.List;
  43 +import java.util.Objects;
  44 +
  45 +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
  46 +import static cn.iocoder.yudao.module.crm.enums.ErrorCodeConstants.CRM_PERMISSION_DENIED;
  47 +import static java.util.Collections.singleton;
  48 +import static org.mockito.ArgumentMatchers.argThat;
  49 +import static org.mockito.ArgumentMatchers.eq;
  50 +import static org.mockito.Mockito.*;
  51 +
  52 +/**
  53 + * {@link CrmContractServiceImpl} 的单元测试类
  54 + *
  55 + * 通过 {@link AspectJProxyFactory} 代理真实的 Service,并织入真实的 {@link CrmPermissionAspect},校验合同提交的数据权限
  56 + *
  57 + * @author 芋道源码
  58 + */
  59 +public class CrmContractServiceImplTest extends BaseMockitoUnitTest {
  60 +
  61 + private static final Long CONTRACT_ID = 10L;
  62 + private static final Long LOGIN_USER_ID = 100L;
  63 + private static final Long OWNER_USER_ID = 200L;
  64 + private static final Long SUBORDINATE_USER_ID = 300L;
  65 +
  66 + @InjectMocks
  67 + private CrmContractServiceImpl contractService;
  68 + @InjectMocks
  69 + private CrmPermissionAspect crmPermissionAspect;
  70 +
  71 + @Mock
  72 + private CrmContractMapper contractMapper;
  73 + @Mock
  74 + private CrmContractProductMapper contractProductMapper;
  75 + @Mock
  76 + private CrmNoRedisDAO noRedisDAO;
  77 + @Mock
  78 + private CrmPermissionService crmPermissionService;
  79 + @Mock
  80 + private CrmProductService productService;
  81 + @Mock
  82 + private CrmCustomerService customerService;
  83 + @Mock
  84 + private CrmBusinessService businessService;
  85 + @Mock
  86 + private CrmContactService contactService;
  87 + @Mock
  88 + private CrmContractConfigService contractConfigService;
  89 + @Mock
  90 + private CrmReceivableService receivableService;
  91 + @Mock
  92 + private AdminUserApi adminUserApi;
  93 + @Mock
  94 + private BpmProcessInstanceApi bpmProcessInstanceApi;
  95 + @Mock
  96 + private PermissionCommonApi permissionCommonApi;
  97 +
  98 + /**
  99 + * 织入 {@link CrmPermissionAspect} 后的 Service 代理
  100 + */
  101 + private CrmContractService contractServiceProxy;
  102 +
  103 + private MockedStatic<SpringUtil> springUtilMockedStatic;
  104 +
  105 + @BeforeEach
  106 + public void setUp() {
  107 + // 代理真实的 Service,织入真实的数据权限切面
  108 + AspectJProxyFactory proxyFactory = new AspectJProxyFactory(contractService);
  109 + proxyFactory.setProxyTargetClass(true);
  110 + proxyFactory.addAspect(crmPermissionAspect);
  111 + contractServiceProxy = proxyFactory.getProxy();
  112 + // mock CrmPermissionUtils#isCrmAdmin() 使用的 PermissionCommonApi
  113 + springUtilMockedStatic = mockStatic(SpringUtil.class);
  114 + springUtilMockedStatic.when(() -> SpringUtil.getBean(PermissionCommonApi.class)).thenReturn(permissionCommonApi);
  115 + // mock 登录用户
  116 + MockHttpServletRequest request = new MockHttpServletRequest();
  117 + SecurityFrameworkUtils.setLoginUser(new LoginUser().setId(LOGIN_USER_ID), request);
  118 + RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request));
  119 + }
  120 +
  121 + @AfterEach
  122 + public void tearDown() {
  123 + springUtilMockedStatic.close();
  124 + RequestContextHolder.resetRequestAttributes();
  125 + SecurityContextHolder.clearContext();
  126 + }
  127 +
  128 + @Test
  129 + public void testSubmitContract_noPermission() {
  130 + // mock 方法:该合同没有任何数据权限
  131 + mockCrmAdmin(false);
  132 + mockContractPermissions(Collections.emptyList());
  133 +
  134 + // 调用,并断言异常
  135 + assertServiceException(() -> contractServiceProxy.submitContract(CONTRACT_ID, LOGIN_USER_ID),
  136 + CRM_PERMISSION_DENIED, CrmBizTypeEnum.CRM_CONTRACT.getName());
  137 + // 断言:未读写合同,未发起审批流程
  138 + verifyNoInteractions(contractMapper, bpmProcessInstanceApi);
  139 + }
  140 +
  141 + @Test
  142 + public void testSubmitContract_otherOwner() {
  143 + // mock 方法:该合同的负责人是其他人,当前用户不在团队内,也没有下级
  144 + mockCrmAdmin(false);
  145 + mockContractPermissions(Collections.singletonList(
  146 + buildPermission(OWNER_USER_ID, CrmPermissionLevelEnum.OWNER)));
  147 + when(adminUserApi.getUserListBySubordinate(LOGIN_USER_ID)).thenReturn(Collections.emptyList());
  148 +
  149 + // 调用,并断言异常
  150 + assertServiceException(() -> contractServiceProxy.submitContract(CONTRACT_ID, LOGIN_USER_ID),
  151 + CRM_PERMISSION_DENIED, CrmBizTypeEnum.CRM_CONTRACT.getName());
  152 + // 断言:未读写合同,未发起审批流程
  153 + verifyNoInteractions(contractMapper, bpmProcessInstanceApi);
  154 + }
  155 +
  156 + @Test
  157 + public void testSubmitContract_readOnly() {
  158 + // mock 方法:当前用户只有 READ 权限
  159 + mockCrmAdmin(false);
  160 + mockContractPermissions(Arrays.asList(
  161 + buildPermission(OWNER_USER_ID, CrmPermissionLevelEnum.OWNER),
  162 + buildPermission(LOGIN_USER_ID, CrmPermissionLevelEnum.READ)));
  163 + when(adminUserApi.getUserListBySubordinate(LOGIN_USER_ID)).thenReturn(Collections.emptyList());
  164 +
  165 + // 调用,并断言异常
  166 + assertServiceException(() -> contractServiceProxy.submitContract(CONTRACT_ID, LOGIN_USER_ID),
  167 + CRM_PERMISSION_DENIED, CrmBizTypeEnum.CRM_CONTRACT.getName());
  168 + // 断言:未读写合同,未发起审批流程
  169 + verifyNoInteractions(contractMapper, bpmProcessInstanceApi);
  170 + }
  171 +
  172 + @Test
  173 + public void testSubmitContract_write() {
  174 + // mock 方法:当前用户有 WRITE 权限
  175 + mockCrmAdmin(false);
  176 + mockContractPermissions(Arrays.asList(
  177 + buildPermission(OWNER_USER_ID, CrmPermissionLevelEnum.OWNER),
  178 + buildPermission(LOGIN_USER_ID, CrmPermissionLevelEnum.WRITE)));
  179 + mockDraftContractAndProcess();
  180 +
  181 + // 调用
  182 + contractServiceProxy.submitContract(CONTRACT_ID, LOGIN_USER_ID);
  183 + // 断言
  184 + assertContractSubmitted();
  185 + }
  186 +
  187 + @Test
  188 + public void testSubmitContract_owner() {
  189 + // mock 方法:当前用户是负责人
  190 + mockCrmAdmin(false);
  191 + mockContractPermissions(Collections.singletonList(
  192 + buildPermission(LOGIN_USER_ID, CrmPermissionLevelEnum.OWNER)));
  193 + mockDraftContractAndProcess();
  194 +
  195 + // 调用
  196 + contractServiceProxy.submitContract(CONTRACT_ID, LOGIN_USER_ID);
  197 + // 断言
  198 + assertContractSubmitted();
  199 + }
  200 +
  201 + @Test
  202 + public void testSubmitContract_subordinateOwner() {
  203 + // mock 方法:当前用户的下级是负责人
  204 + mockCrmAdmin(false);
  205 + mockContractPermissions(Collections.singletonList(
  206 + buildPermission(SUBORDINATE_USER_ID, CrmPermissionLevelEnum.OWNER)));
  207 + when(adminUserApi.getUserListBySubordinate(LOGIN_USER_ID)).thenReturn(Collections.singletonList(
  208 + new AdminUserRespDTO().setId(SUBORDINATE_USER_ID)));
  209 + mockDraftContractAndProcess();
  210 +
  211 + // 调用
  212 + contractServiceProxy.submitContract(CONTRACT_ID, LOGIN_USER_ID);
  213 + // 断言
  214 + assertContractSubmitted();
  215 + }
  216 +
  217 + @Test
  218 + public void testSubmitContract_crmAdmin() {
  219 + // mock 方法:当前用户是 CRM 管理员,不在团队内
  220 + mockCrmAdmin(true);
  221 + mockContractPermissions(Collections.singletonList(
  222 + buildPermission(OWNER_USER_ID, CrmPermissionLevelEnum.OWNER)));
  223 + mockDraftContractAndProcess();
  224 +
  225 + // 调用
  226 + contractServiceProxy.submitContract(CONTRACT_ID, LOGIN_USER_ID);
  227 + // 断言
  228 + assertContractSubmitted();
  229 + verifyNoInteractions(adminUserApi);
  230 + }
  231 +
  232 + private void mockCrmAdmin(boolean crmAdmin) {
  233 + when(permissionCommonApi.hasAnyRoles(LOGIN_USER_ID, RoleCodeEnum.CRM_ADMIN.getCode())).thenReturn(crmAdmin);
  234 + }
  235 +
  236 + private void mockContractPermissions(List<CrmPermissionDO> permissions) {
  237 + when(crmPermissionService.getPermissionListByBiz(CrmBizTypeEnum.CRM_CONTRACT.getType(), singleton(CONTRACT_ID)))
  238 + .thenReturn(permissions);
  239 + }
  240 +
  241 + private void mockDraftContractAndProcess() {
  242 + when(contractMapper.selectById(CONTRACT_ID)).thenReturn(new CrmContractDO().setId(CONTRACT_ID)
  243 + .setName("合同001").setAuditStatus(CrmAuditStatusEnum.DRAFT.getStatus()));
  244 + when(bpmProcessInstanceApi.createProcessInstance(eq(LOGIN_USER_ID), argThat((BpmProcessInstanceCreateReqDTO reqDTO) ->
  245 + Objects.equals(reqDTO.getProcessDefinitionKey(), CrmContractServiceImpl.BPM_PROCESS_DEFINITION_KEY)
  246 + && Objects.equals(reqDTO.getBusinessKey(), String.valueOf(CONTRACT_ID)))))
  247 + .thenReturn("process-instance-id");
  248 + }
  249 +
  250 + private void assertContractSubmitted() {
  251 + verify(contractMapper).updateById(argThat((CrmContractDO contract) ->
  252 + Objects.equals(contract.getId(), CONTRACT_ID)
  253 + && Objects.equals(contract.getProcessInstanceId(), "process-instance-id")
  254 + && Objects.equals(contract.getAuditStatus(), CrmAuditStatusEnum.PROCESS.getStatus())));
  255 + }
  256 +
  257 + private static CrmPermissionDO buildPermission(Long userId, CrmPermissionLevelEnum level) {
  258 + return new CrmPermissionDO().setBizType(CrmBizTypeEnum.CRM_CONTRACT.getType()).setBizId(CONTRACT_ID)
  259 + .setUserId(userId).setLevel(level.getLevel());
  260 + }
  261 +
  262 +}