feat: 支持发起人撤回审批
- 在我发起和审批详情中提供申请人撤回操作并恢复资源状态 - 补齐审批说明列表展示、动作权限校验和防重复提交 - 为审批决策增加行锁并补充撤回权限回归测试
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
package tech.easyflow.approval.service.impl;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import tech.easyflow.approval.entity.ApprovalInstance;
|
||||
import tech.easyflow.approval.entity.ApprovalTask;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalInstanceDetailVo;
|
||||
import tech.easyflow.approval.enums.ApprovalInstanceStatus;
|
||||
import tech.easyflow.approval.enums.ApprovalTaskStatus;
|
||||
import tech.easyflow.approval.mapper.ApprovalFlowStepMapper;
|
||||
import tech.easyflow.approval.mapper.ApprovalInstanceMapper;
|
||||
import tech.easyflow.approval.mapper.ApprovalLogMapper;
|
||||
import tech.easyflow.approval.mapper.ApprovalTaskMapper;
|
||||
import tech.easyflow.approval.service.ApprovalActionFacade;
|
||||
import tech.easyflow.approval.service.ApprovalAssigneeService;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
import tech.easyflow.system.service.SysAccountService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* 审批撤回入口查询测试。
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class ApprovalRevokeQueryTest {
|
||||
|
||||
private static final BigInteger INSTANCE_ID = BigInteger.valueOf(101);
|
||||
private static final BigInteger TENANT_ID = BigInteger.valueOf(42);
|
||||
|
||||
@Mock
|
||||
private ApprovalInstanceMapper approvalInstanceMapper;
|
||||
|
||||
@Mock
|
||||
private ApprovalTaskMapper approvalTaskMapper;
|
||||
|
||||
@Mock
|
||||
private ApprovalLogMapper approvalLogMapper;
|
||||
|
||||
@Mock
|
||||
private ApprovalFlowStepMapper approvalFlowStepMapper;
|
||||
|
||||
@Mock
|
||||
private ApprovalAssigneeService approvalAssigneeService;
|
||||
|
||||
@Mock
|
||||
private ApprovalActionFacade approvalActionFacade;
|
||||
|
||||
@Mock
|
||||
private CategoryPermissionService categoryPermissionService;
|
||||
|
||||
@Mock
|
||||
private SysAccountService sysAccountService;
|
||||
|
||||
@InjectMocks
|
||||
private ApprovalQueryServiceImpl service;
|
||||
|
||||
/**
|
||||
* 验证活动审批的发起人只能看到撤回入口。
|
||||
*/
|
||||
@Test
|
||||
public void detailShouldExposeRevokeOnlyToApplicant() {
|
||||
LoginAccount account = account(7);
|
||||
ApprovalInstance instance = instance(7);
|
||||
stubDetail(instance, List.of());
|
||||
|
||||
try (MockedStatic<SaTokenUtil> saToken = mockStatic(SaTokenUtil.class)) {
|
||||
saToken.when(SaTokenUtil::getLoginAccount).thenReturn(account);
|
||||
ApprovalInstanceDetailVo detail = service.detail(INSTANCE_ID);
|
||||
assertTrue(detail.isCanRevoke());
|
||||
assertFalse(detail.isCanApprove());
|
||||
assertFalse(detail.isCanReject());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证当前审批人只能看到通过和驳回入口,不能撤回他人申请。
|
||||
*/
|
||||
@Test
|
||||
public void detailShouldNotExposeRevokeToCurrentReviewer() {
|
||||
LoginAccount account = account(7);
|
||||
ApprovalInstance instance = instance(8);
|
||||
ApprovalTask task = new ApprovalTask();
|
||||
task.setInstanceId(INSTANCE_ID);
|
||||
task.setStepNo(1);
|
||||
task.setStatus(ApprovalTaskStatus.PENDING.getCode());
|
||||
stubDetail(instance, List.of(task));
|
||||
when(approvalAssigneeService.canHandleTask(task, account.getId(), Set.of())).thenReturn(true);
|
||||
|
||||
try (MockedStatic<SaTokenUtil> saToken = mockStatic(SaTokenUtil.class)) {
|
||||
saToken.when(SaTokenUtil::getLoginAccount).thenReturn(account);
|
||||
ApprovalInstanceDetailVo detail = service.detail(INSTANCE_ID);
|
||||
assertTrue(detail.isCanApprove());
|
||||
assertTrue(detail.isCanReject());
|
||||
assertFalse(detail.isCanRevoke());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置详情查询所需的兼容桩。
|
||||
*
|
||||
* @param instance 审批实例
|
||||
* @param tasks 审批任务
|
||||
*/
|
||||
private void stubDetail(ApprovalInstance instance, List<ApprovalTask> tasks) {
|
||||
lenient().when(approvalInstanceMapper.selectOneById(INSTANCE_ID)).thenReturn(instance);
|
||||
lenient().when(approvalInstanceMapper.selectOneByQuery(any(QueryWrapper.class))).thenReturn(instance);
|
||||
when(approvalTaskMapper.selectListByQuery(any(QueryWrapper.class))).thenReturn(tasks);
|
||||
when(approvalLogMapper.selectListByQuery(any(QueryWrapper.class))).thenReturn(List.of());
|
||||
when(approvalAssigneeService.getAvailableRoleIds(any(BigInteger.class))).thenReturn(Set.of());
|
||||
lenient().when(sysAccountService.listByIds(any())).thenReturn(List.of());
|
||||
lenient().when(sysAccountService.list(any(QueryWrapper.class))).thenReturn(List.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造活动审批实例。
|
||||
*
|
||||
* @param applicantId 申请人 ID
|
||||
* @return 活动审批实例
|
||||
*/
|
||||
private ApprovalInstance instance(long applicantId) {
|
||||
ApprovalInstance instance = new ApprovalInstance();
|
||||
instance.setId(INSTANCE_ID);
|
||||
instance.setTenantId(TENANT_ID);
|
||||
instance.setFlowId(BigInteger.valueOf(301));
|
||||
instance.setFlowVersion(1);
|
||||
instance.setResourceType("KNOWLEDGE");
|
||||
instance.setResourceId(BigInteger.valueOf(501));
|
||||
instance.setActionType("PUBLISH");
|
||||
instance.setStatus(ApprovalInstanceStatus.PENDING.getCode());
|
||||
instance.setCurrentStepNo(1);
|
||||
instance.setApplicantId(BigInteger.valueOf(applicantId));
|
||||
instance.setSnapshotJson(Map.of(
|
||||
"steps", List.of(Map.of(
|
||||
"stepNo", 1,
|
||||
"stepName", "审核"
|
||||
))
|
||||
));
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造当前登录账号。
|
||||
*
|
||||
* @param accountId 账号 ID
|
||||
* @return 登录账号
|
||||
*/
|
||||
private LoginAccount account(long accountId) {
|
||||
LoginAccount account = new LoginAccount();
|
||||
account.setId(BigInteger.valueOf(accountId));
|
||||
account.setTenantId(TENANT_ID);
|
||||
return account;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package tech.easyflow.approval.service.impl;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import tech.easyflow.approval.entity.ApprovalInstance;
|
||||
import tech.easyflow.approval.entity.ApprovalLog;
|
||||
import tech.easyflow.approval.entity.ApprovalTask;
|
||||
import tech.easyflow.approval.enums.ApprovalInstanceStatus;
|
||||
import tech.easyflow.approval.enums.ApprovalTaskStatus;
|
||||
import tech.easyflow.approval.mapper.ApprovalInstanceMapper;
|
||||
import tech.easyflow.approval.mapper.ApprovalLogMapper;
|
||||
import tech.easyflow.approval.mapper.ApprovalTaskMapper;
|
||||
import tech.easyflow.approval.service.ApprovalActionFacade;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.entity.SysAccount;
|
||||
import tech.easyflow.system.service.SysAccountService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* 审批发起人撤回行为测试。
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class ApprovalRevokeServiceTest {
|
||||
|
||||
private static final BigInteger APPLICANT_ID = BigInteger.valueOf(7);
|
||||
private static final BigInteger INSTANCE_ID = BigInteger.valueOf(101);
|
||||
private static final BigInteger TENANT_ID = BigInteger.valueOf(42);
|
||||
|
||||
@Mock
|
||||
private ApprovalInstanceMapper approvalInstanceMapper;
|
||||
|
||||
@Mock
|
||||
private ApprovalTaskMapper approvalTaskMapper;
|
||||
|
||||
@Mock
|
||||
private ApprovalLogMapper approvalLogMapper;
|
||||
|
||||
@Mock
|
||||
private ApprovalActionFacade approvalActionFacade;
|
||||
|
||||
@Mock
|
||||
private SysAccountService sysAccountService;
|
||||
|
||||
@InjectMocks
|
||||
private ApprovalInstanceServiceImpl service;
|
||||
|
||||
/**
|
||||
* 验证发起人撤回后结束当前任务、记录日志并恢复资源状态。
|
||||
*/
|
||||
@Test
|
||||
public void revokeShouldCompleteCurrentTaskForApplicant() {
|
||||
ApprovalInstance instance = activeInstance(APPLICANT_ID);
|
||||
ApprovalTask task = pendingTask();
|
||||
lenient().when(sysAccountService.getById(APPLICANT_ID)).thenReturn(tenantAccount(APPLICANT_ID));
|
||||
when(approvalInstanceMapper.selectOneByQuery(any(QueryWrapper.class))).thenReturn(instance);
|
||||
when(approvalTaskMapper.selectOneByQuery(any(QueryWrapper.class))).thenReturn(task);
|
||||
|
||||
service.revoke(INSTANCE_ID, "内容需要调整", APPLICANT_ID);
|
||||
|
||||
assertEquals(ApprovalInstanceStatus.REVOKED.getCode(), instance.getStatus());
|
||||
assertNotNull(instance.getFinishedAt());
|
||||
assertEquals(ApprovalTaskStatus.REVOKED.getCode(), task.getStatus());
|
||||
assertEquals(APPLICANT_ID, task.getActedBy());
|
||||
assertEquals("内容需要调整", task.getComment());
|
||||
verify(approvalLogMapper).insert(any(ApprovalLog.class));
|
||||
verify(approvalActionFacade).handleRevoked(instance, APPLICANT_ID, "内容需要调整");
|
||||
|
||||
ArgumentCaptor<QueryWrapper> queryCaptor = ArgumentCaptor.forClass(QueryWrapper.class);
|
||||
verify(approvalInstanceMapper).selectOneByQuery(queryCaptor.capture());
|
||||
assertTrue(queryCaptor.getValue().toSQL().toLowerCase(Locale.ROOT).contains("for update"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证当前审批人或其他同租户账号不能代替发起人撤回。
|
||||
*/
|
||||
@Test
|
||||
public void revokeShouldRejectNonApplicant() {
|
||||
BigInteger operatorId = BigInteger.valueOf(8);
|
||||
lenient().when(sysAccountService.getById(operatorId)).thenReturn(tenantAccount(operatorId));
|
||||
when(approvalInstanceMapper.selectOneByQuery(any(QueryWrapper.class)))
|
||||
.thenReturn(activeInstance(APPLICANT_ID));
|
||||
|
||||
BusinessException exception = assertThrows(
|
||||
BusinessException.class,
|
||||
() -> service.revoke(INSTANCE_ID, "尝试撤回", operatorId)
|
||||
);
|
||||
|
||||
assertEquals(403, exception.getHttpStatus());
|
||||
verify(approvalTaskMapper, never()).selectOneByQuery(any(QueryWrapper.class));
|
||||
verify(approvalActionFacade, never())
|
||||
.handleRevoked(any(ApprovalInstance.class), any(BigInteger.class), any(String.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造活动审批实例。
|
||||
*
|
||||
* @param applicantId 申请人 ID
|
||||
* @return 活动审批实例
|
||||
*/
|
||||
private ApprovalInstance activeInstance(BigInteger applicantId) {
|
||||
ApprovalInstance instance = new ApprovalInstance();
|
||||
instance.setId(INSTANCE_ID);
|
||||
instance.setTenantId(TENANT_ID);
|
||||
instance.setApplicantId(applicantId);
|
||||
instance.setCurrentStepNo(1);
|
||||
instance.setStatus(ApprovalInstanceStatus.PENDING.getCode());
|
||||
return instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造待处理任务。
|
||||
*
|
||||
* @return 待处理任务
|
||||
*/
|
||||
private ApprovalTask pendingTask() {
|
||||
ApprovalTask task = new ApprovalTask();
|
||||
task.setInstanceId(INSTANCE_ID);
|
||||
task.setStepNo(1);
|
||||
task.setStatus(ApprovalTaskStatus.PENDING.getCode());
|
||||
return task;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造具有租户归属的账号。
|
||||
*
|
||||
* @param accountId 账号 ID
|
||||
* @return 账号实体
|
||||
*/
|
||||
private SysAccount tenantAccount(BigInteger accountId) {
|
||||
SysAccount account = new SysAccount();
|
||||
account.setId(accountId);
|
||||
account.setTenantId(TENANT_ID);
|
||||
return account;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user