Commit c572b5515cd16acb746779799f0a03c137ed05a5

Authored by YunaiV
1 parent 6e3e7776

fix(crm): 校验回款提交的数据写权限

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