feat: 完善 Skill 管理与发布治理
- 实现标准资源存储、能力绑定及双格式导入导出 - 接入分类、可见范围、审批发布与资源权限校验 - 补充并发、租户隔离、安全边界和迁移契约测试
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
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.ApprovalLog;
|
||||
import tech.easyflow.approval.entity.ApprovalTask;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest;
|
||||
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.approval.service.ApprovalMatchService;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.entity.SysAccount;
|
||||
import tech.easyflow.system.service.SysAccountService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* {@link ApprovalInstanceServiceImpl} 审批提交身份授权测试。
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class ApprovalInstanceServiceImplAccessTest {
|
||||
|
||||
@Mock
|
||||
private ApprovalMatchService approvalMatchService;
|
||||
|
||||
@Mock
|
||||
private SysAccountService sysAccountService;
|
||||
|
||||
@Mock
|
||||
private ApprovalInstanceMapper approvalInstanceMapper;
|
||||
|
||||
@Mock
|
||||
private ApprovalTaskMapper approvalTaskMapper;
|
||||
|
||||
@Mock
|
||||
private ApprovalLogMapper approvalLogMapper;
|
||||
|
||||
@Mock
|
||||
private ApprovalActionFacade approvalActionFacade;
|
||||
|
||||
@InjectMocks
|
||||
private ApprovalInstanceServiceImpl service;
|
||||
|
||||
/**
|
||||
* 验证同租户账号也不能代替当前登录人发起审批。
|
||||
*/
|
||||
@Test
|
||||
public void submitApprovalShouldRejectForgedApplicantBeforeMatchingFlow() {
|
||||
LoginAccount loginAccount = new LoginAccount();
|
||||
loginAccount.setId(BigInteger.ONE);
|
||||
loginAccount.setTenantId(BigInteger.valueOf(42));
|
||||
ApprovalSubmitRequest request = new ApprovalSubmitRequest();
|
||||
request.setApplicantId(BigInteger.TWO);
|
||||
|
||||
try (MockedStatic<SaTokenUtil> saToken = mockStatic(SaTokenUtil.class)) {
|
||||
saToken.when(SaTokenUtil::getLoginAccount).thenReturn(loginAccount);
|
||||
BusinessException exception = assertThrows(BusinessException.class,
|
||||
() -> service.submitApproval(request));
|
||||
assertEquals(403, exception.getHttpStatus());
|
||||
}
|
||||
|
||||
verify(sysAccountService, never()).getById(BigInteger.TWO);
|
||||
verify(approvalMatchService, never()).matchFlow(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证申请人可撤回进行中的审批,并同步结束当前任务与恢复资源状态。
|
||||
*/
|
||||
@Test
|
||||
public void revokeShouldCompleteCurrentTaskForApplicant() {
|
||||
BigInteger applicantId = BigInteger.valueOf(7);
|
||||
BigInteger tenantId = BigInteger.valueOf(42);
|
||||
BigInteger instanceId = BigInteger.valueOf(101);
|
||||
SysAccount applicant = tenantAccount(applicantId, tenantId);
|
||||
ApprovalInstance instance = activeInstance(instanceId, applicantId, tenantId);
|
||||
ApprovalTask task = new ApprovalTask();
|
||||
task.setInstanceId(instanceId);
|
||||
task.setStepNo(1);
|
||||
task.setStatus(ApprovalTaskStatus.PENDING.getCode());
|
||||
|
||||
when(sysAccountService.getById(applicantId)).thenReturn(applicant);
|
||||
when(approvalInstanceMapper.selectOneByQuery(any(QueryWrapper.class))).thenReturn(instance);
|
||||
when(approvalTaskMapper.selectOneByQuery(any(QueryWrapper.class))).thenReturn(task);
|
||||
|
||||
service.revoke(instanceId, "内容需要调整", applicantId);
|
||||
|
||||
assertEquals(ApprovalInstanceStatus.REVOKED.getCode(), instance.getStatus());
|
||||
assertNotNull(instance.getFinishedAt());
|
||||
assertEquals(ApprovalTaskStatus.REVOKED.getCode(), task.getStatus());
|
||||
assertEquals(applicantId, task.getActedBy());
|
||||
assertEquals("内容需要调整", task.getComment());
|
||||
verify(approvalLogMapper).insert(any(ApprovalLog.class));
|
||||
verify(approvalActionFacade).handleRevoked(instance, applicantId, "内容需要调整");
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证非申请人即使属于同一租户也不能撤回审批。
|
||||
*/
|
||||
@Test
|
||||
public void revokeShouldRejectSameTenantNonApplicant() {
|
||||
BigInteger applicantId = BigInteger.valueOf(7);
|
||||
BigInteger operatorId = BigInteger.valueOf(8);
|
||||
BigInteger tenantId = BigInteger.valueOf(42);
|
||||
BigInteger instanceId = BigInteger.valueOf(101);
|
||||
|
||||
when(sysAccountService.getById(operatorId)).thenReturn(tenantAccount(operatorId, tenantId));
|
||||
when(approvalInstanceMapper.selectOneByQuery(any(QueryWrapper.class)))
|
||||
.thenReturn(activeInstance(instanceId, applicantId, tenantId));
|
||||
|
||||
BusinessException exception = assertThrows(
|
||||
BusinessException.class,
|
||||
() -> service.revoke(instanceId, "尝试撤回", 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));
|
||||
}
|
||||
|
||||
private ApprovalInstance activeInstance(BigInteger instanceId, BigInteger applicantId, BigInteger tenantId) {
|
||||
ApprovalInstance instance = new ApprovalInstance();
|
||||
instance.setId(instanceId);
|
||||
instance.setApplicantId(applicantId);
|
||||
instance.setTenantId(tenantId);
|
||||
instance.setCurrentStepNo(1);
|
||||
instance.setStatus(ApprovalInstanceStatus.PENDING.getCode());
|
||||
return instance;
|
||||
}
|
||||
|
||||
private SysAccount tenantAccount(BigInteger accountId, BigInteger tenantId) {
|
||||
SysAccount account = new SysAccount();
|
||||
account.setId(accountId);
|
||||
account.setTenantId(tenantId);
|
||||
return account;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
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.enums.ApprovalInstanceStatus;
|
||||
import tech.easyflow.approval.mapper.ApprovalInstanceMapper;
|
||||
import tech.easyflow.approval.mapper.ApprovalTaskMapper;
|
||||
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.assertThrows;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* {@link ApprovalInstanceServiceImpl} 审批决策并发互斥测试。
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class ApprovalInstanceServiceImplConcurrencyTest {
|
||||
|
||||
@Mock
|
||||
private ApprovalInstanceMapper approvalInstanceMapper;
|
||||
@Mock
|
||||
private ApprovalTaskMapper approvalTaskMapper;
|
||||
@Mock
|
||||
private SysAccountService sysAccountService;
|
||||
@InjectMocks
|
||||
private ApprovalInstanceServiceImpl service;
|
||||
|
||||
/**
|
||||
* 审批实例与当前任务必须在状态判断前加行锁,避免重复执行同一决策。
|
||||
*/
|
||||
@Test
|
||||
public void approvalDecisionLocksInstanceAndCurrentTask() {
|
||||
BigInteger instanceId = BigInteger.valueOf(101);
|
||||
BigInteger tenantId = BigInteger.valueOf(42);
|
||||
ApprovalInstance instance = new ApprovalInstance();
|
||||
instance.setId(instanceId);
|
||||
instance.setTenantId(tenantId);
|
||||
instance.setStatus(ApprovalInstanceStatus.PENDING.getCode());
|
||||
instance.setCurrentStepNo(1);
|
||||
SysAccount operator = new SysAccount();
|
||||
operator.setId(BigInteger.ONE);
|
||||
operator.setTenantId(tenantId);
|
||||
when(sysAccountService.getById(BigInteger.ONE)).thenReturn(operator);
|
||||
when(approvalInstanceMapper.selectOneByQuery(any(QueryWrapper.class))).thenReturn(instance);
|
||||
when(approvalTaskMapper.selectOneByQuery(any(QueryWrapper.class))).thenReturn(null);
|
||||
|
||||
assertThrows(BusinessException.class,
|
||||
() -> service.approve(instanceId, "通过", BigInteger.ONE));
|
||||
|
||||
ArgumentCaptor<QueryWrapper> instanceQuery = ArgumentCaptor.forClass(QueryWrapper.class);
|
||||
ArgumentCaptor<QueryWrapper> taskQuery = ArgumentCaptor.forClass(QueryWrapper.class);
|
||||
verify(approvalInstanceMapper).selectOneByQuery(instanceQuery.capture());
|
||||
verify(approvalTaskMapper).selectOneByQuery(taskQuery.capture());
|
||||
assertTrue(instanceQuery.getValue().toSQL().toLowerCase(Locale.ROOT).contains("for update"));
|
||||
assertTrue(instanceQuery.getValue().toSQL().toLowerCase(Locale.ROOT).contains("tenant_id"));
|
||||
assertTrue(taskQuery.getValue().toSQL().toLowerCase(Locale.ROOT).contains("for update"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package tech.easyflow.approval.service.impl;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* V32 审批实例租户迁移契约测试。
|
||||
*/
|
||||
public class ApprovalInstanceTenantMigrationContractTest {
|
||||
|
||||
/**
|
||||
* 验证历史实例从申请人账号回填租户,且空租户会阻断迁移。
|
||||
*
|
||||
* @throws Exception 迁移文件不可读时抛出
|
||||
*/
|
||||
@Test
|
||||
public void migrationShouldBackfillAndGuardApprovalTenant() throws Exception {
|
||||
String sql = migrationSql();
|
||||
|
||||
assertTrue(sql.contains("ADD COLUMN `tenant_id` BIGINT UNSIGNED NULL"));
|
||||
assertTrue(sql.contains("LEFT JOIN `tb_sys_account` applicant ON applicant.`id` = approval.`applicant_id`"));
|
||||
assertTrue(sql.contains("applicant.`id` IS NULL OR applicant.`tenant_id` IS NULL"));
|
||||
assertTrue(sql.contains("JOIN `tb_sys_account` applicant ON applicant.`id` = approval.`applicant_id`"));
|
||||
assertTrue(sql.contains("SET approval.`tenant_id` = applicant.`tenant_id`"));
|
||||
assertTrue(sql.contains("tmp_approval_instance_tenant_guard"));
|
||||
assertTrue(sql.indexOf("tmp_approval_instance_tenant_guard") < sql.indexOf("ADD COLUMN `tenant_id`"));
|
||||
assertTrue(sql.contains("MODIFY COLUMN `tenant_id` BIGINT UNSIGNED NOT NULL"));
|
||||
assertTrue(sql.contains("`tenant_id`, `status`, `submitted_at`"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取工作区中的 V32 MySQL 迁移。
|
||||
*
|
||||
* @return 迁移 SQL
|
||||
* @throws Exception 迁移文件不存在或不可读时抛出
|
||||
*/
|
||||
private String migrationSql() throws Exception {
|
||||
Path root = Path.of(System.getProperty("maven.multiModuleProjectDirectory",
|
||||
Path.of(System.getProperty("user.dir")).toAbsolutePath().toString()));
|
||||
while (root != null) {
|
||||
Path migration = root.resolve("easyflow-starter/easyflow-starter-all/src/main/resources/"
|
||||
+ "db/migration/mysql/V32__mysql_approval_instance_tenant.sql");
|
||||
if (Files.isRegularFile(migration)) {
|
||||
return Files.readString(migration, StandardCharsets.UTF_8);
|
||||
}
|
||||
root = root.getParent();
|
||||
}
|
||||
throw new IllegalStateException("找不到 V32 审批实例租户迁移");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
package tech.easyflow.approval.service.impl;
|
||||
|
||||
import com.mybatisflex.core.paginate.Page;
|
||||
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.MockedStatic;
|
||||
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.entity.vo.ApprovalInstanceDetailVo;
|
||||
import tech.easyflow.approval.entity.vo.ApprovalInstancePageVo;
|
||||
import tech.easyflow.approval.enums.ApprovalAssigneeType;
|
||||
import tech.easyflow.approval.enums.ApprovalEventType;
|
||||
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.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.entity.SysAccount;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
import tech.easyflow.system.service.SysAccountService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThrows;
|
||||
import static org.junit.Assert.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;
|
||||
import static org.mockito.Mockito.mockStatic;
|
||||
|
||||
/**
|
||||
* {@link ApprovalQueryServiceImpl} 审批详情租户和主体授权回归测试。
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class ApprovalQueryServiceImplAccessTest {
|
||||
|
||||
private static final BigInteger INSTANCE_ID = BigInteger.valueOf(101);
|
||||
private static final BigInteger RESOURCE_ID = BigInteger.valueOf(501);
|
||||
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;
|
||||
|
||||
/**
|
||||
* 验证详情查询显式带租户条件,并拒绝 Mapper 异常返回的跨租户实例。
|
||||
*/
|
||||
@Test
|
||||
public void detailShouldRejectCrossTenantInstanceBeforeReadingSnapshot() {
|
||||
LoginAccount account = account(7, 42);
|
||||
ApprovalInstance instance = instance(99, 99);
|
||||
when(approvalInstanceMapper.selectOneByQuery(any(QueryWrapper.class))).thenReturn(instance);
|
||||
|
||||
try (MockedStatic<SaTokenUtil> saToken = mockStatic(SaTokenUtil.class)) {
|
||||
saToken.when(SaTokenUtil::getLoginAccount).thenReturn(account);
|
||||
BusinessException exception = assertThrows(BusinessException.class,
|
||||
() -> service.detail(INSTANCE_ID));
|
||||
assertEquals(404, exception.getHttpStatus());
|
||||
}
|
||||
|
||||
ArgumentCaptor<QueryWrapper> query = ArgumentCaptor.forClass(QueryWrapper.class);
|
||||
verify(approvalInstanceMapper).selectOneByQuery(query.capture());
|
||||
assertTrue(query.getValue().toSQL().toLowerCase(Locale.ROOT).contains("tenant_id"));
|
||||
verify(approvalTaskMapper, never()).selectListByQuery(any(QueryWrapper.class));
|
||||
verify(approvalLogMapper, never()).selectListByQuery(any(QueryWrapper.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证同租户普通用户不能仅凭审批查询操作权限读取完整资源快照。
|
||||
*/
|
||||
@Test
|
||||
public void detailShouldRejectSameTenantNonParticipant() {
|
||||
LoginAccount account = account(7, 42);
|
||||
ApprovalInstance instance = instance(8, 42);
|
||||
when(approvalInstanceMapper.selectOneByQuery(any(QueryWrapper.class))).thenReturn(instance);
|
||||
when(approvalTaskMapper.selectListByQuery(any(QueryWrapper.class))).thenReturn(List.of());
|
||||
when(approvalAssigneeService.getAvailableRoleIds(account.getId())).thenReturn(Set.of());
|
||||
when(approvalActionFacade.canAccessApprovalDetail("SKILL", RESOURCE_ID)).thenReturn(false);
|
||||
|
||||
try (MockedStatic<SaTokenUtil> saToken = mockStatic(SaTokenUtil.class)) {
|
||||
saToken.when(SaTokenUtil::getLoginAccount).thenReturn(account);
|
||||
BusinessException exception = assertThrows(BusinessException.class,
|
||||
() -> service.detail(INSTANCE_ID));
|
||||
assertEquals(403, exception.getHttpStatus());
|
||||
}
|
||||
|
||||
verify(approvalLogMapper, never()).selectListByQuery(any(QueryWrapper.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证申请人仍可读取自己发起的审批快照。
|
||||
*/
|
||||
@Test
|
||||
public void detailShouldAllowApplicant() {
|
||||
LoginAccount account = account(7, 42);
|
||||
ApprovalInstance instance = instance(7, 42);
|
||||
Map<String, Object> snapshot = instance.getSnapshotJson();
|
||||
stubAuthorizedDetail(instance, account, List.of());
|
||||
|
||||
try (MockedStatic<SaTokenUtil> saToken = mockStatic(SaTokenUtil.class)) {
|
||||
saToken.when(SaTokenUtil::getLoginAccount).thenReturn(account);
|
||||
ApprovalInstanceDetailVo detail = service.detail(INSTANCE_ID);
|
||||
assertSame(snapshot, detail.getSnapshotJson());
|
||||
assertFalse(detail.isCanApprove());
|
||||
assertFalse(detail.isCanReject());
|
||||
assertTrue(detail.isCanRevoke());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证当前待办处理人可查看审批详情。
|
||||
*/
|
||||
@Test
|
||||
public void detailShouldAllowCurrentTaskHandler() {
|
||||
LoginAccount account = account(7, 42);
|
||||
ApprovalInstance instance = instance(8, 42);
|
||||
ApprovalTask task = task(ApprovalTaskStatus.PENDING.getCode(), null);
|
||||
task.setAssigneeType(ApprovalAssigneeType.USER.getCode());
|
||||
task.setAssigneeTargetId(account.getId());
|
||||
stubAuthorizedDetail(instance, account, 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);
|
||||
assertEquals(INSTANCE_ID, detail.getId());
|
||||
assertTrue(detail.isCanApprove());
|
||||
assertTrue(detail.isCanReject());
|
||||
assertFalse(detail.isCanRevoke());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证实际处理过历史步骤的用户仍可查看审批详情。
|
||||
*/
|
||||
@Test
|
||||
public void detailShouldAllowHistoricalActor() {
|
||||
LoginAccount account = account(7, 42);
|
||||
ApprovalInstance instance = instance(8, 42);
|
||||
ApprovalTask task = task(ApprovalTaskStatus.APPROVED.getCode(), account.getId());
|
||||
stubAuthorizedDetail(instance, account, List.of(task));
|
||||
|
||||
try (MockedStatic<SaTokenUtil> saToken = mockStatic(SaTokenUtil.class)) {
|
||||
saToken.when(SaTokenUtil::getLoginAccount).thenReturn(account);
|
||||
assertEquals(INSTANCE_ID, service.detail(INSTANCE_ID).getId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证审批说明在详情、审批任务和提交日志中完整透传。
|
||||
*/
|
||||
@Test
|
||||
public void detailShouldExposeApplicationReasonAcrossRelatedViews() {
|
||||
LoginAccount account = account(7, 42);
|
||||
ApprovalInstance instance = instance(7, 42);
|
||||
instance.setApplicationReason("发布新的问答流程");
|
||||
ApprovalTask task = task(ApprovalTaskStatus.PENDING.getCode(), null);
|
||||
ApprovalLog log = new ApprovalLog();
|
||||
log.setEventType(ApprovalEventType.SUBMITTED.getCode());
|
||||
stubAuthorizedDetail(instance, account, List.of(task));
|
||||
when(approvalLogMapper.selectListByQuery(any(QueryWrapper.class))).thenReturn(List.of(log));
|
||||
|
||||
try (MockedStatic<SaTokenUtil> saToken = mockStatic(SaTokenUtil.class)) {
|
||||
saToken.when(SaTokenUtil::getLoginAccount).thenReturn(account);
|
||||
ApprovalInstanceDetailVo detail = service.detail(INSTANCE_ID);
|
||||
assertEquals("发布新的问答流程", detail.getApplicationReason());
|
||||
assertEquals("发布新的问答流程", detail.getTasks().get(0).getApplicationReason());
|
||||
assertEquals("发布新的问答流程", detail.getLogs().get(0).getApplicationReason());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证审批分页列表返回申请人填写的审批说明和账号信息。
|
||||
*/
|
||||
@Test
|
||||
public void initiatedPageShouldExposeApplicationReasonAndApplicant() {
|
||||
LoginAccount account = account(7, 42);
|
||||
ApprovalInstance instance = instance(7, 42);
|
||||
instance.setApplicationReason("发布新的问答流程");
|
||||
SysAccount applicant = new SysAccount();
|
||||
applicant.setId(account.getId());
|
||||
applicant.setNickname("陈子默");
|
||||
applicant.setLoginName("czm");
|
||||
Page<ApprovalInstance> page = new Page<>(List.of(instance), 1L, 10L, 1L);
|
||||
when(approvalInstanceMapper.paginate(anyLong(), anyLong(), any(QueryWrapper.class))).thenReturn(page);
|
||||
when(sysAccountService.list(any(QueryWrapper.class))).thenReturn(List.of(applicant));
|
||||
|
||||
try (MockedStatic<SaTokenUtil> saToken = mockStatic(SaTokenUtil.class)) {
|
||||
saToken.when(SaTokenUtil::getLoginAccount).thenReturn(account);
|
||||
ApprovalInstancePageVo item = service.initiatedPage(null, null, null, 1L, 10L)
|
||||
.getRecords()
|
||||
.get(0);
|
||||
assertEquals("发布新的问答流程", item.getApplicationReason());
|
||||
assertEquals("陈子默", item.getApplicantName());
|
||||
assertEquals("czm", item.getApplicantAccount());
|
||||
assertTrue(item.isCanRevoke());
|
||||
assertFalse(item.isCanApprove());
|
||||
assertFalse(item.isCanReject());
|
||||
}
|
||||
}
|
||||
|
||||
private void stubAuthorizedDetail(ApprovalInstance instance, LoginAccount account, List<ApprovalTask> tasks) {
|
||||
when(approvalInstanceMapper.selectOneByQuery(any(QueryWrapper.class))).thenReturn(instance);
|
||||
when(approvalTaskMapper.selectListByQuery(any(QueryWrapper.class))).thenReturn(tasks);
|
||||
when(approvalAssigneeService.getAvailableRoleIds(account.getId())).thenReturn(Set.of());
|
||||
when(approvalLogMapper.selectListByQuery(any(QueryWrapper.class))).thenReturn(List.of());
|
||||
when(sysAccountService.list(any(QueryWrapper.class))).thenReturn(List.of());
|
||||
}
|
||||
|
||||
private ApprovalInstance instance(long applicantId, long tenantId) {
|
||||
ApprovalInstance instance = new ApprovalInstance();
|
||||
instance.setId(INSTANCE_ID);
|
||||
instance.setTenantId(BigInteger.valueOf(tenantId));
|
||||
instance.setFlowId(BigInteger.valueOf(301));
|
||||
instance.setFlowVersion(1);
|
||||
instance.setResourceType("SKILL");
|
||||
instance.setResourceId(RESOURCE_ID);
|
||||
instance.setActionType("PUBLISH");
|
||||
instance.setStatus(ApprovalInstanceStatus.PENDING.getCode());
|
||||
instance.setCurrentStepNo(1);
|
||||
instance.setApplicantId(BigInteger.valueOf(applicantId));
|
||||
instance.setSnapshotJson(Map.of(
|
||||
"resourceSnapshot", Map.of("skillContent", "private prompt"),
|
||||
"steps", List.of(Map.of(
|
||||
"stepNo", 1,
|
||||
"stepName", "审核",
|
||||
"assigneeType", ApprovalAssigneeType.USER.getCode(),
|
||||
"assigneeTargetId", 7))));
|
||||
return instance;
|
||||
}
|
||||
|
||||
private ApprovalTask task(String status, BigInteger actedBy) {
|
||||
ApprovalTask task = new ApprovalTask();
|
||||
task.setInstanceId(INSTANCE_ID);
|
||||
task.setStepNo(1);
|
||||
task.setStatus(status);
|
||||
task.setActedBy(actedBy);
|
||||
return task;
|
||||
}
|
||||
|
||||
private LoginAccount account(long accountId, long tenantId) {
|
||||
LoginAccount account = new LoginAccount();
|
||||
account.setId(BigInteger.valueOf(accountId));
|
||||
account.setTenantId(BigInteger.valueOf(tenantId));
|
||||
return account;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user