body = setting.getBody() == null
+ ? new ArrayList<>() : new ArrayList<>(setting.getBody());
+ body.add(new BpmSimpleModelNodeVO.HttpRequestParam()
.setKey("taskDefineKey") // 重要:回调请求 taskDefineKey 需要传给被调用方,用于回调执行
.setType(BpmHttpRequestParamTypeEnum.FIXED_VALUE.getType()).setValue(setting.getCallbackTaskDefineKey()));
BpmHttpRequestUtils.executeBpmHttpRequest(processInstance,
- setting.getUrl(), setting.getHeader(), setting.getBody(), false, null);
+ setting.getUrl(), setting.getHeader(), body, false, null);
}
}
diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BaseFlowableUnitTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BaseFlowableUnitTest.java
new file mode 100644
index 0000000..324e1ab
--- /dev/null
+++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BaseFlowableUnitTest.java
@@ -0,0 +1,157 @@
+package cn.iocoder.yudao.module.bpm.framework.flowable;
+
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
+import cn.iocoder.yudao.module.bpm.dal.redis.BpmProcessIdRedisDAO;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum;
+import cn.iocoder.yudao.module.bpm.framework.flowable.config.BpmFlowableConfiguration;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.listener.BpmProcessInstanceEventListener;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.listener.BpmTaskEventListener;
+import cn.iocoder.yudao.module.bpm.service.comment.BpmCommentService;
+import cn.iocoder.yudao.module.bpm.service.definition.BpmFormService;
+import cn.iocoder.yudao.module.bpm.service.definition.BpmModelService;
+import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
+import cn.iocoder.yudao.module.bpm.service.message.BpmMessageService;
+import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceCopyService;
+import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceServiceImpl;
+import cn.iocoder.yudao.module.bpm.service.task.BpmTaskServiceImpl;
+import cn.iocoder.yudao.module.bpm.service.task.listener.BpmCallActivityListener;
+import cn.iocoder.yudao.module.system.api.dept.DeptApi;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import jakarta.annotation.Resource;
+import org.flowable.engine.RepositoryService;
+import org.flowable.spring.boot.ProcessEngineAutoConfiguration;
+import org.flowable.spring.boot.ProcessEngineServicesAutoConfiguration;
+import org.junit.jupiter.api.BeforeEach;
+import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
+import org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Import;
+import org.springframework.context.annotation.Primary;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+
+import java.util.Collection;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * 依赖 Flowable 引擎的单元测试
+ *
+ * 使用 H2 数据源,Flowable 表由测试环境的 Flowable 自动配置创建;需要业务表的测试自行导入对应 SQL。
+ * 加载真实的 {@link BpmTaskServiceImpl}、{@link BpmProcessInstanceServiceImpl} 及 Flowable 事件监听器,
+ * 保证事务代理、getSelf() 自调用、任务/流程实例状态回调与生产一致;其它模块、DB 相关的 Service 统一 Mock。
+ * 注意:Mock 统一声明在基类,保证子类共享同一个 Spring 上下文(同一个 Flowable 引擎),子类不要再额外声明 {@link MockitoBean}。
+ *
+ * @author HUIHUI
+ */
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE,
+ classes = BaseFlowableUnitTest.Application.class)
+@ActiveProfiles("unit-test")
+@TestPropertySource(properties = {
+ // Flowable 8 的 H2 脚本使用旧式 identity 语法,LEGACY 模式兼容该语法;业务表脚本在需要时由测试显式导入。
+ "spring.datasource.url=jdbc:h2:mem:bpm-flowable-testdb;MODE=LEGACY;DATABASE_TO_UPPER=false;NON_KEYWORDS=value",
+ "spring.sql.init.mode=never",
+ // 关闭异步执行器,定时器由测试显式触发,避免后台线程与测试交错执行
+ "flowable.async-executor-activate=false"
+})
+public abstract class BaseFlowableUnitTest {
+
+ @MockitoBean
+ protected BpmProcessDefinitionService processDefinitionService;
+ @MockitoBean
+ protected BpmModelService modelService;
+ @MockitoBean
+ protected BpmCommentService commentService;
+ @MockitoBean
+ protected BpmMessageService messageService;
+ @MockitoBean
+ protected BpmFormService formService;
+ @MockitoBean
+ protected BpmProcessInstanceCopyService processInstanceCopyService;
+ @MockitoBean
+ protected BpmProcessIdRedisDAO processIdRedisDAO;
+ @MockitoBean
+ protected AdminUserApi adminUserApi;
+ @MockitoBean
+ protected DeptApi deptApi;
+
+ @Import({BaseDbUnitTest.Application.class, BpmFlowableConfiguration.class,
+ BpmTaskServiceImpl.class, BpmProcessInstanceServiceImpl.class,
+ BpmTaskEventListener.class, BpmProcessInstanceEventListener.class})
+ @ImportAutoConfiguration({ProcessEngineAutoConfiguration.class, ProcessEngineServicesAutoConfiguration.class,
+ TransactionAutoConfiguration.class})
+ public static class Application {
+
+ @Bean("testBpmTaskCandidateInvoker")
+ @Primary
+ public BpmTaskCandidateInvoker bpmTaskCandidateInvoker() {
+ BpmTaskCandidateInvoker invoker = mock(BpmTaskCandidateInvoker.class);
+ when(invoker.calculateUsersByTask(any())).thenReturn(Set.of(1L));
+ return invoker;
+ }
+
+ /**
+ * 子流程发起人监听器:BPMN 通过 ${bpmCallActivityListener} 引用,需与组件扫描的 Bean 名称一致
+ */
+ @Bean("bpmCallActivityListener")
+ public BpmCallActivityListener bpmCallActivityListener() {
+ return new BpmCallActivityListener();
+ }
+ }
+
+ /**
+ * 审批人计算的 Mock,属于上下文单例(非 {@link MockitoBean}),每个测试前重置,避免测试间相互影响
+ */
+ @Resource(name = "testBpmTaskCandidateInvoker")
+ protected BpmTaskCandidateInvoker taskCandidateInvoker;
+
+ @BeforeEach
+ public void resetTaskCandidateInvoker() {
+ // 默认:任务的审批人为用户 1
+ reset(taskCandidateInvoker);
+ when(taskCandidateInvoker.calculateUsersByTask(any())).thenReturn(Set.of(1L));
+ }
+
+ /**
+ * 将流程定义、模型、用户相关的 Mock 委托给真实 Flowable 引擎,模拟业务表中已有对应数据
+ *
+ * 流程定义信息默认为允许撤回的 BPMN 流程;用户编号为 id 时,昵称为「用户 + id」。
+ */
+ @SuppressWarnings("unchecked")
+ protected void mockFlowableBackedServices(RepositoryService repositoryService) {
+ when(processDefinitionService.getActiveProcessDefinition(anyString())).thenAnswer(invocation ->
+ repositoryService.createProcessDefinitionQuery().processDefinitionKey(invocation.getArgument(0))
+ .latestVersion().singleResult());
+ when(processDefinitionService.getProcessDefinition(anyString())).thenAnswer(invocation ->
+ repositoryService.createProcessDefinitionQuery().processDefinitionId(invocation.getArgument(0))
+ .singleResult());
+ when(processDefinitionService.getProcessDefinitionBpmnModel(anyString())).thenAnswer(invocation ->
+ repositoryService.getBpmnModel(invocation.getArgument(0)));
+ when(processDefinitionService.getProcessDefinitionInfo(anyString())).thenAnswer(invocation ->
+ new BpmProcessDefinitionInfoDO().setProcessDefinitionId(invocation.getArgument(0))
+ .setModelType(BpmModelTypeEnum.BPMN.getType()).setAllowWithdrawTask(true));
+ when(processDefinitionService.canUserStartProcessDefinition(any(), any())).thenReturn(true);
+ when(modelService.getBpmnModelByDefinitionId(anyString())).thenAnswer(invocation ->
+ repositoryService.getBpmnModel(invocation.getArgument(0)));
+ when(adminUserApi.getUser(any())).thenAnswer(invocation -> buildUser(invocation.getArgument(0)));
+ when(adminUserApi.getUserList(anyCollection())).thenAnswer(invocation ->
+ ((Collection) invocation.getArgument(0)).stream().map(BaseFlowableUnitTest::buildUser).toList());
+ when(adminUserApi.getUserMap(anyCollection())).thenAnswer(invocation ->
+ ((Collection) invocation.getArgument(0)).stream()
+ .collect(Collectors.toMap(Function.identity(), BaseFlowableUnitTest::buildUser)));
+ }
+
+ private static AdminUserRespDTO buildUser(Long id) {
+ return new AdminUserRespDTO().setId(id).setNickname("用户" + id);
+ }
+
+}
diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BpmModelDeployFlowableTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BpmModelDeployFlowableTest.java
new file mode 100644
index 0000000..2172b02
--- /dev/null
+++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BpmModelDeployFlowableTest.java
@@ -0,0 +1,297 @@
+package cn.iocoder.yudao.module.bpm.framework.flowable;
+
+import cn.iocoder.yudao.framework.common.exception.ErrorCode;
+import cn.iocoder.yudao.framework.common.exception.ServiceException;
+import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelSaveReqVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskApproveReqVO;
+import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
+import cn.iocoder.yudao.module.bpm.dal.mysql.definition.BpmProcessDefinitionInfoMapper;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskApproveMethodEnum;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskApproveTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
+import cn.iocoder.yudao.module.bpm.service.definition.BpmModelServiceImpl;
+import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionServiceImpl;
+import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceServiceImpl;
+import cn.iocoder.yudao.module.bpm.service.task.BpmTaskServiceImpl;
+import jakarta.annotation.Resource;
+import org.flowable.common.engine.impl.db.SuspensionState;
+import org.flowable.engine.HistoryService;
+import org.flowable.engine.RepositoryService;
+import org.flowable.engine.RuntimeService;
+import org.flowable.engine.TaskService;
+import org.flowable.engine.repository.Model;
+import org.flowable.engine.repository.ProcessDefinition;
+import org.flowable.task.api.Task;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.test.context.jdbc.Sql;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
+import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.MODEL_UPDATE_FAIL_NOT_MANAGER;
+import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_DEFINITION_NOT_EXISTS;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.AdditionalAnswers.delegatesTo;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.verify;
+
+/**
+ * 流程模型部署、挂起、排序、清理、删除的 Flowable 集成测试
+ *
+ * 使用真实的 {@link BpmModelServiceImpl}、{@link BpmProcessDefinitionServiceImpl}(Flowable + bpm_process_definition_info 表),
+ * 并让流程实例 Service 依赖的 {@link #processDefinitionService} 委托给真实实现,验证【模型部署 → 发起 → 审批】的完整闭环。
+ *
+ * @author HUIHUI
+ */
+@Sql(scripts = "/sql/flowable/create_tables.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
+@Sql(scripts = "/sql/flowable/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
+public class BpmModelDeployFlowableTest extends BaseFlowableUnitTest {
+
+ private static final String MODEL_KEY = "modelDeploy";
+
+ @Resource
+ private RepositoryService repositoryService;
+ @Resource
+ private RuntimeService runtimeService;
+ @Resource
+ private HistoryService historyService;
+ @Resource
+ private TaskService flowableTaskService;
+ @Resource
+ private BpmProcessDefinitionInfoMapper processDefinitionMapper;
+
+ @Resource
+ private BpmTaskServiceImpl taskService;
+ @Resource
+ private BpmProcessInstanceServiceImpl processInstanceService;
+
+ private final BpmProcessDefinitionServiceImpl processDefinitionServiceImpl = new BpmProcessDefinitionServiceImpl();
+ private final BpmModelServiceImpl modelServiceImpl = new BpmModelServiceImpl();
+
+ @BeforeEach
+ public void setUp() {
+ // 真实的流程定义、流程模型 Service,依赖的 Flowable、Mapper 使用 Spring 中的真实 Bean
+ ReflectionTestUtils.setField(processDefinitionServiceImpl, "repositoryService", repositoryService);
+ ReflectionTestUtils.setField(processDefinitionServiceImpl, "processDefinitionMapper", processDefinitionMapper);
+ ReflectionTestUtils.setField(processDefinitionServiceImpl, "adminUserApi", adminUserApi);
+ ReflectionTestUtils.setField(modelServiceImpl, "repositoryService", repositoryService);
+ ReflectionTestUtils.setField(modelServiceImpl, "runtimeService", runtimeService);
+ ReflectionTestUtils.setField(modelServiceImpl, "historyService", historyService);
+ ReflectionTestUtils.setField(modelServiceImpl, "taskService", flowableTaskService);
+ ReflectionTestUtils.setField(modelServiceImpl, "processDefinitionService", processDefinitionServiceImpl);
+ ReflectionTestUtils.setField(modelServiceImpl, "bpmFormService", formService);
+ ReflectionTestUtils.setField(modelServiceImpl, "taskCandidateInvoker", taskCandidateInvoker);
+ ReflectionTestUtils.setField(modelServiceImpl, "processInstanceCopyService", processInstanceCopyService);
+
+ // 发起、审批流程时,流程定义相关的查询委托给真实实现,读取部署后写入的 bpm_process_definition_info
+ mockFlowableBackedServices(repositoryService);
+ doAnswer(delegatesTo(processDefinitionServiceImpl)).when(processDefinitionService)
+ .getActiveProcessDefinition(anyString());
+ doAnswer(delegatesTo(processDefinitionServiceImpl)).when(processDefinitionService)
+ .getProcessDefinition(anyString());
+ doAnswer(delegatesTo(processDefinitionServiceImpl)).when(processDefinitionService)
+ .getProcessDefinitionInfo(anyString());
+ doAnswer(delegatesTo(processDefinitionServiceImpl)).when(processDefinitionService)
+ .canUserStartProcessDefinition(any(), any());
+ }
+
+ @AfterEach
+ public void tearDown() {
+ repositoryService.createDeploymentQuery().deploymentKey(MODEL_KEY).list()
+ .forEach(deployment -> repositoryService.deleteDeployment(deployment.getId(), true));
+ repositoryService.createModelQuery().modelKey(MODEL_KEY).list()
+ .forEach(model -> repositoryService.deleteModel(model.getId()));
+ }
+
+ @Test
+ public void testDeployModel_redeploy() {
+ // 准备参数:创建 Simple 模型
+ String modelId = createModel();
+
+ // 调用:首次部署
+ modelServiceImpl.deployModel(1L, modelId);
+ // 断言:生成流程定义及拓展信息,模型关联最新部署
+ ProcessDefinition definition1 = getActiveProcessDefinition();
+ assertEquals("模型部署", definition1.getName());
+ assertEquals("OA", definition1.getCategory());
+ assertEquals(definition1.getDeploymentId(), repositoryService.getModel(modelId).getDeploymentId());
+ BpmProcessDefinitionInfoDO definitionInfo1 = processDefinitionMapper.selectByProcessDefinitionId(definition1.getId());
+ assertEquals(modelId, definitionInfo1.getModelId());
+ assertEquals(BpmModelTypeEnum.SIMPLE.getType(), definitionInfo1.getModelType());
+ assertEquals(List.of(1L), definitionInfo1.getManagerUserIds());
+ assertNotNull(definitionInfo1.getSimpleModel());
+ // 断言:可基于该流程定义发起流程,停留在审批节点
+ String processInstanceId1 = startProcessInstance();
+ Task task1 = getRunningTask(processInstanceId1);
+ assertEquals(definition1.getId(), task1.getProcessDefinitionId());
+ assertEquals("approve", task1.getTaskDefinitionKey());
+
+ // 调用:再次部署
+ modelServiceImpl.deployModel(1L, modelId);
+ // 断言:老的流程定义被挂起,新发起的流程使用新的流程定义
+ ProcessDefinition definition2 = getActiveProcessDefinition();
+ assertNotEquals(definition1.getId(), definition2.getId());
+ assertTrue(repositoryService.getProcessDefinition(definition1.getId()).isSuspended());
+ assertEquals(definition2.getDeploymentId(), repositoryService.getModel(modelId).getDeploymentId());
+ String processInstanceId2 = startProcessInstance();
+ assertEquals(definition2.getId(), getRunningTask(processInstanceId2).getProcessDefinitionId());
+
+ // 调用 + 断言:老流程定义上进行中的流程不受挂起影响,仍可审批通过
+ approveTask(task1);
+ assertProcessInstanceStatus(processInstanceId1, BpmProcessInstanceStatusEnum.APPROVE.getStatus());
+ approveTask(getRunningTask(processInstanceId2));
+ assertProcessInstanceStatus(processInstanceId2, BpmProcessInstanceStatusEnum.APPROVE.getStatus());
+
+ // 调用 + 断言:调整排序,同一模型的所有流程定义拓展信息同步更新
+ modelServiceImpl.updateModelSortBatch(1L, List.of(modelId));
+ Long sort = processDefinitionMapper.selectByProcessDefinitionId(definition2.getId()).getSort();
+ assertEquals(sort, processDefinitionMapper.selectByProcessDefinitionId(definition1.getId()).getSort());
+ assertTrue(sort > definitionInfo1.getSort());
+ }
+
+ @Test
+ public void testDeployModel_notManager() {
+ // 准备参数
+ String modelId = createModel();
+
+ // 调用,并断言异常:非模型管理员不允许部署
+ assertServiceException(() -> modelServiceImpl.deployModel(2L, modelId), MODEL_UPDATE_FAIL_NOT_MANAGER, "模型部署");
+ assertEquals(0, repositoryService.createProcessDefinitionQuery().processDefinitionKey(MODEL_KEY).count());
+ }
+
+ @Test
+ public void testUpdateModelState_suspendAndActive() {
+ // 准备参数
+ String modelId = createModel();
+ modelServiceImpl.deployModel(1L, modelId);
+ String processDefinitionId = getActiveProcessDefinition().getId();
+
+ // 调用 + 断言:挂起后不允许发起
+ modelServiceImpl.updateModelState(1L, modelId, SuspensionState.SUSPENDED.getStateCode());
+ assertTrue(repositoryService.getProcessDefinition(processDefinitionId).isSuspended());
+ assertStartProcessInstanceFail(PROCESS_DEFINITION_NOT_EXISTS);
+ // 调用 + 断言:激活后恢复发起
+ modelServiceImpl.updateModelState(1L, modelId, SuspensionState.ACTIVE.getStateCode());
+ assertFalse(repositoryService.getProcessDefinition(processDefinitionId).isSuspended());
+ assertEquals("approve", getRunningTask(startProcessInstance()).getTaskDefinitionKey());
+ }
+
+ @Test
+ public void testCleanModel_success() {
+ // 准备参数:一个进行中的流程,一个已结束的流程
+ String modelId = createModel();
+ modelServiceImpl.deployModel(1L, modelId);
+ String runningId = startProcessInstance();
+ String finishedId = startProcessInstance();
+ approveTask(getRunningTask(finishedId));
+ assertProcessInstanceStatus(finishedId, BpmProcessInstanceStatusEnum.APPROVE.getStatus());
+
+ // 调用
+ modelServiceImpl.cleanModel(1L, modelId);
+ // 断言:流程实例、历史、任务、抄送全部清理,模型与流程定义保留
+ assertEquals(0, runtimeService.createProcessInstanceQuery().processDefinitionKey(MODEL_KEY).count());
+ assertEquals(0, historyService.createHistoricProcessInstanceQuery().processDefinitionKey(MODEL_KEY).count());
+ assertEquals(0, flowableTaskService.createTaskQuery().processDefinitionKey(MODEL_KEY).count());
+ verify(processInstanceCopyService).deleteProcessInstanceCopy(runningId);
+ verify(processInstanceCopyService).deleteProcessInstanceCopy(finishedId);
+ assertNotNull(repositoryService.getModel(modelId));
+ assertNotNull(getActiveProcessDefinition());
+ }
+
+ @Test
+ public void testDeleteModel_success() {
+ // 准备参数:一个进行中的流程
+ String modelId = createModel();
+ modelServiceImpl.deployModel(1L, modelId);
+ String processDefinitionId = getActiveProcessDefinition().getId();
+ String processInstanceId = startProcessInstance();
+
+ // 调用
+ modelServiceImpl.deleteModel(1L, modelId);
+ // 断言:模型被删除,流程定义被挂起,不允许再发起
+ assertNull(repositoryService.getModel(modelId));
+ assertTrue(repositoryService.getProcessDefinition(processDefinitionId).isSuspended());
+ assertStartProcessInstanceFail(PROCESS_DEFINITION_NOT_EXISTS);
+ // 断言:进行中的流程不受影响,仍可审批通过
+ approveTask(getRunningTask(processInstanceId));
+ assertProcessInstanceStatus(processInstanceId, BpmProcessInstanceStatusEnum.APPROVE.getStatus());
+ }
+
+ // ========== 私有方法 ==========
+
+ /**
+ * 创建 Simple 模型:发起人 -> 审批(用户 1)-> 结束,管理员为用户 1
+ */
+ private String createModel() {
+ BpmSimpleModelNodeVO approveNode = new BpmSimpleModelNodeVO().setId("approve").setName("审批")
+ .setType(BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType())
+ .setApproveType(BpmUserTaskApproveTypeEnum.USER.getType())
+ .setApproveMethod(BpmUserTaskApproveMethodEnum.RANDOM.getMethod())
+ .setCandidateStrategy(BpmTaskCandidateStrategyEnum.USER.getStrategy()).setCandidateParam("1")
+ .setChildNode(new BpmSimpleModelNodeVO().setId("end").setType(BpmSimpleModelNodeTypeEnum.END_NODE.getType()));
+ BpmSimpleModelNodeVO startUserNode = new BpmSimpleModelNodeVO().setId("start-user").setName("发起人")
+ .setType(BpmSimpleModelNodeTypeEnum.START_USER_NODE.getType())
+ .setChildNode(approveNode);
+ BpmModelSaveReqVO reqVO = new BpmModelSaveReqVO();
+ reqVO.setKey(MODEL_KEY);
+ reqVO.setName("模型部署");
+ reqVO.setCategory("OA");
+ reqVO.setSimpleModel(startUserNode);
+ reqVO.setType(BpmModelTypeEnum.SIMPLE.getType());
+ reqVO.setFormType(BpmModelFormTypeEnum.CUSTOM.getType());
+ reqVO.setFormCustomCreatePath("/bpm/oa/leave/create");
+ reqVO.setFormCustomViewPath("/bpm/oa/leave/detail");
+ reqVO.setVisible(true);
+ reqVO.setManagerUserIds(List.of(1L));
+ return modelServiceImpl.createModel(reqVO);
+ }
+
+ private ProcessDefinition getActiveProcessDefinition() {
+ ProcessDefinition definition = processDefinitionServiceImpl.getActiveProcessDefinition(MODEL_KEY);
+ assertNotNull(definition);
+ return definition;
+ }
+
+ private String startProcessInstance() {
+ return processInstanceService.createProcessInstance(1L, new BpmProcessInstanceCreateReqDTO()
+ .setProcessDefinitionKey(MODEL_KEY).setBusinessKey("business-1"));
+ }
+
+ /**
+ * 发起流程失败:ServiceException 被 FlowableUtils#executeAuthenticatedUserId 包装,由全局异常处理器解包
+ */
+ private void assertStartProcessInstanceFail(ErrorCode errorCode) {
+ RuntimeException exception = assertThrows(RuntimeException.class, this::startProcessInstance);
+ ServiceException serviceException = assertInstanceOf(ServiceException.class, exception.getCause());
+ assertEquals(errorCode.getCode(), serviceException.getCode());
+ }
+
+ private Task getRunningTask(String processInstanceId) {
+ List tasks = taskService.getRunningTaskListByProcessInstanceId(processInstanceId, null, null);
+ assertEquals(1, tasks.size());
+ return tasks.get(0);
+ }
+
+ private void approveTask(Task task) {
+ taskService.approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId())
+ .setReason("同意"));
+ }
+
+ private void assertProcessInstanceStatus(String processInstanceId, Integer status) {
+ assertEquals(status, FlowableUtils.getProcessInstanceStatus(
+ processInstanceService.getHistoricProcessInstance(processInstanceId)));
+ }
+
+}
diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BpmMultiInstanceFlowableTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BpmMultiInstanceFlowableTest.java
new file mode 100644
index 0000000..445af49
--- /dev/null
+++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BpmMultiInstanceFlowableTest.java
@@ -0,0 +1,338 @@
+package cn.iocoder.yudao.module.bpm.framework.flowable;
+
+import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
+import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskApproveReqVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskRejectReqVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmChildProcessMultiInstanceSourceTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmChildProcessStartUserEmptyTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmChildProcessStartUserTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskApproveMethodEnum;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskApproveTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum;
+import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum;
+import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior.BpmParallelMultiInstanceBehavior;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior.BpmSequentialMultiInstanceBehavior;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.SimpleModelUtils;
+import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceServiceImpl;
+import cn.iocoder.yudao.module.bpm.service.task.BpmTaskServiceImpl;
+import jakarta.annotation.Resource;
+import org.flowable.bpmn.model.BpmnModel;
+import org.flowable.engine.HistoryService;
+import org.flowable.engine.RepositoryService;
+import org.flowable.engine.RuntimeService;
+import org.flowable.engine.delegate.DelegateExecution;
+import org.flowable.engine.history.HistoricProcessInstance;
+import org.flowable.engine.runtime.ProcessInstance;
+import org.flowable.task.api.Task;
+import org.flowable.task.api.history.HistoricTaskInstance;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+/**
+ * 多实例(会签、或签、依次审批、子流程多实例)的 Flowable 集成测试
+ *
+ * 覆盖 {@link BpmParallelMultiInstanceBehavior}、{@link BpmSequentialMultiInstanceBehavior} 在真实引擎中的实例创建、
+ * 审批人分配,以及 {@link BpmTaskServiceImpl} 审批后完成条件的流转。
+ *
+ * @author HUIHUI
+ */
+public class BpmMultiInstanceFlowableTest extends BaseFlowableUnitTest {
+
+ @Resource
+ private RepositoryService repositoryService;
+ @Resource
+ private RuntimeService runtimeService;
+ @Resource
+ private HistoryService historyService;
+
+ @Resource
+ private BpmTaskServiceImpl taskService;
+ @Resource
+ private BpmProcessInstanceServiceImpl processInstanceService;
+
+ private final List deploymentIds = new ArrayList<>();
+
+ @BeforeEach
+ public void setUp() {
+ deploymentIds.add(repositoryService.createDeployment()
+ .addClasspathResource("bpmn/multi-instance/user-task.bpmn20.xml")
+ .addClasspathResource("bpmn/multi-instance/call-activity.bpmn20.xml")
+ .deploy().getId());
+ mockFlowableBackedServices(repositoryService);
+ // 多人审批的审批人:用户 1、用户 2(有序)
+ when(taskCandidateInvoker.calculateUsersByTask(any())).thenReturn(new LinkedHashSet<>(List.of(1L, 2L)));
+ }
+
+ @AfterEach
+ public void tearDown() {
+ deploymentIds.forEach(id -> repositoryService.deleteDeployment(id, true));
+ }
+
+ // ========== 多人审批 ==========
+
+ @Test
+ public void testApproveTask_any() {
+ // 准备参数
+ String processInstanceId = startProcessInstance("multiAny", null);
+ List tasks = getRunningTasks(processInstanceId);
+ assertEquals(List.of("1", "2"), convertList(tasks, Task::getAssignee));
+
+ // 调用:用户 1 审批通过
+ approveTask(tasks.get(0));
+ // 断言:或签一人通过即可,用户 2 的任务被取消,流程审批通过
+ assertTaskStatus(tasks.get(1).getId(), BpmTaskStatusEnum.CANCEL.getStatus(),
+ BpmReasonEnum.CANCEL_BY_SYSTEM.getReason());
+ assertProcessInstanceStatus(processInstanceId, BpmProcessInstanceStatusEnum.APPROVE.getStatus());
+ }
+
+ @Test
+ public void testRejectTask_any() {
+ // 准备参数
+ String processInstanceId = startProcessInstance("multiAny", null);
+ List tasks = getRunningTasks(processInstanceId);
+
+ // 调用:用户 1 审批不通过
+ taskService.rejectTask(1L, new BpmTaskRejectReqVO().setId(tasks.get(0).getId()).setReason("不同意"));
+ // 断言:或签一人不通过即可,用户 2 的任务被取消,流程不通过
+ assertTaskStatus(tasks.get(0).getId(), BpmTaskStatusEnum.REJECT.getStatus(), "不同意");
+ assertTaskStatus(tasks.get(1).getId(), BpmTaskStatusEnum.CANCEL.getStatus(),
+ BpmReasonEnum.CANCEL_BY_SYSTEM.getReason());
+ assertTrue(getRunningTasks(processInstanceId).isEmpty());
+ assertProcessInstanceStatus(processInstanceId, BpmProcessInstanceStatusEnum.REJECT.getStatus());
+ }
+
+ @Test
+ public void testApproveTask_ratio() {
+ // 准备参数
+ String processInstanceId = startProcessInstance("multiRatio", null);
+ List tasks = getRunningTasks(processInstanceId);
+ assertEquals(List.of("1", "2"), convertList(tasks, Task::getAssignee));
+
+ // 调用 + 断言:用户 1 审批通过,仍需等待用户 2 审批
+ approveTask(tasks.get(0));
+ assertEquals(List.of(tasks.get(1).getId()), convertList(getRunningTasks(processInstanceId), Task::getId));
+ // 调用 + 断言:用户 2 审批通过,流程审批通过
+ approveTask(tasks.get(1));
+ assertProcessInstanceStatus(processInstanceId, BpmProcessInstanceStatusEnum.APPROVE.getStatus());
+ }
+
+ @Test
+ public void testApproveTask_ratioPartial() {
+ // 准备参数:通过比例 60%,审批人为用户 1、用户 2、用户 3
+ when(taskCandidateInvoker.calculateUsersByTask(any())).thenReturn(new LinkedHashSet<>(List.of(1L, 2L, 3L)));
+ String processInstanceId = startProcessInstance("multiRatioPartial", null);
+ List tasks = getRunningTasks(processInstanceId);
+ assertEquals(List.of("1", "2", "3"), convertList(tasks, Task::getAssignee));
+
+ // 调用 + 断言:用户 1 审批通过(1/3 < 60%),仍需等待其他人审批
+ approveTask(tasks.get(0));
+ assertEquals(List.of(tasks.get(1).getId(), tasks.get(2).getId()),
+ convertList(getRunningTasks(processInstanceId), Task::getId));
+ // 调用 + 断言:用户 2 审批通过(2/3 >= 60%),用户 3 的任务被取消,流程审批通过
+ approveTask(tasks.get(1));
+ assertTaskStatus(tasks.get(2).getId(), BpmTaskStatusEnum.CANCEL.getStatus(),
+ BpmReasonEnum.CANCEL_BY_SYSTEM.getReason());
+ assertProcessInstanceStatus(processInstanceId, BpmProcessInstanceStatusEnum.APPROVE.getStatus());
+ }
+
+ @Test
+ public void testRejectTask_ratio() {
+ // 准备参数:用户 1 已审批通过
+ String processInstanceId = startProcessInstance("multiRatio", null);
+ List tasks = getRunningTasks(processInstanceId);
+ approveTask(tasks.get(0));
+
+ // 调用:用户 2 审批不通过
+ taskService.rejectTask(2L, new BpmTaskRejectReqVO().setId(tasks.get(1).getId()).setReason("不同意"));
+ // 断言:会签任一人不通过,流程不通过
+ assertTaskStatus(tasks.get(0).getId(), BpmTaskStatusEnum.APPROVE.getStatus(), "同意");
+ assertTaskStatus(tasks.get(1).getId(), BpmTaskStatusEnum.REJECT.getStatus(), "不同意");
+ assertProcessInstanceStatus(processInstanceId, BpmProcessInstanceStatusEnum.REJECT.getStatus());
+ }
+
+ @Test
+ public void testRejectTask_ratioReturn() {
+ // 准备参数:发起审批节点由用户 1 审批,会签节点由用户 1、用户 2 审批
+ when(taskCandidateInvoker.calculateUsersByTask(any())).thenAnswer(invocation ->
+ "multiReturn-task".equals(((DelegateExecution) invocation.getArgument(0)).getCurrentActivityId())
+ ? new LinkedHashSet<>(List.of(1L, 2L)) : Set.of(1L));
+ String processInstanceId = startProcessInstance("multiReturn", null);
+ approveTask(getRunningTask(processInstanceId));
+ List tasks = getRunningTasks(processInstanceId);
+ assertEquals(List.of("1", "2"), convertList(tasks, Task::getAssignee));
+
+ // 调用:用户 2 审批不通过,按配置退回到发起审批节点
+ taskService.rejectTask(2L, new BpmTaskRejectReqVO().setId(tasks.get(1).getId()).setReason("退回修改"));
+ // 断言:用户 2 的任务标记为退回,用户 1 的会签任务被取消,流程回到发起审批节点
+ assertTaskStatus(tasks.get(1).getId(), BpmTaskStatusEnum.RETURN.getStatus(), "退回修改");
+ assertTaskStatus(tasks.get(0).getId(), BpmTaskStatusEnum.CANCEL.getStatus(),
+ BpmReasonEnum.CANCEL_BY_SYSTEM.getReason());
+ Task firstTask = getRunningTask(processInstanceId);
+ assertEquals("multiReturn-first", firstTask.getTaskDefinitionKey());
+
+ // 调用 + 断言:再次审批发起审批节点后,重新生成用户 1、用户 2 的会签任务
+ approveTask(firstTask);
+ List newTasks = getRunningTasks(processInstanceId);
+ assertEquals(List.of("1", "2"), convertList(newTasks, Task::getAssignee));
+ assertEquals(List.of("multiReturn-task", "multiReturn-task"), convertList(newTasks, Task::getTaskDefinitionKey));
+ // 调用 + 断言:全部审批通过后,流程审批通过
+ newTasks.forEach(this::approveTask);
+ assertProcessInstanceStatus(processInstanceId, BpmProcessInstanceStatusEnum.APPROVE.getStatus());
+ }
+
+ @Test
+ public void testApproveTask_sequential() {
+ // 准备参数
+ String processInstanceId = startProcessInstance("multiSequential", null);
+ Task task1 = getRunningTask(processInstanceId);
+ assertEquals("1", task1.getAssignee());
+
+ // 调用 + 断言:用户 1 审批通过后,才轮到用户 2
+ approveTask(task1);
+ Task task2 = getRunningTask(processInstanceId);
+ assertEquals("2", task2.getAssignee());
+ // 调用 + 断言:用户 2 审批通过,流程审批通过
+ approveTask(task2);
+ assertProcessInstanceStatus(processInstanceId, BpmProcessInstanceStatusEnum.APPROVE.getStatus());
+ }
+
+ // ========== 子流程多实例 ==========
+
+ @Test
+ public void testCallActivity_fixedQuantity() {
+ // 调用
+ String processInstanceId = startProcessInstance("multiChildFixedQuantity", null);
+ // 断言:并行发起 2 个子流程,全部审批通过后,主流程结束
+ List children = getChildProcessInstances(processInstanceId);
+ assertEquals(2, children.size());
+ children.forEach(child -> approveTask(getRunningTask(child.getId())));
+ assertProcessInstanceStatus(processInstanceId, BpmProcessInstanceStatusEnum.APPROVE.getStatus());
+ }
+
+ @Test
+ public void testCallActivity_numberForm() {
+ // 调用:表单 count = 3
+ String processInstanceId = startProcessInstance("multiChildNumberForm", new HashMap<>(Map.of("count", 3)));
+ // 断言:并行发起 3 个子流程,全部审批通过后,主流程结束
+ List children = getChildProcessInstances(processInstanceId);
+ assertEquals(3, children.size());
+ children.forEach(child -> approveTask(getRunningTask(child.getId())));
+ assertProcessInstanceStatus(processInstanceId, BpmProcessInstanceStatusEnum.APPROVE.getStatus());
+ }
+
+ @Test
+ public void testCallActivity_multipleFormSequential() {
+ // 调用:表单 users 选择了 2 个用户
+ String processInstanceId = startProcessInstance("multiChildMultipleForm",
+ new HashMap<>(Map.of("users", List.of(1L, 2L))));
+ // 断言:依次发起子流程,前一个子流程结束后才发起下一个
+ for (int i = 0; i < 2; i++) {
+ List children = getChildProcessInstances(processInstanceId);
+ assertEquals(1, children.size());
+ approveTask(getRunningTask(children.get(0).getId()));
+ }
+ assertEquals(2, historyService.createHistoricProcessInstanceQuery()
+ .superProcessInstanceId(processInstanceId).count());
+ assertProcessInstanceStatus(processInstanceId, BpmProcessInstanceStatusEnum.APPROVE.getStatus());
+ }
+
+ // ========== Simple 设计器模型 ==========
+
+ @Test
+ public void testSimpleModel_ratioApproveThenChildProcess() {
+ // 准备参数:由 SimpleModelUtils 生成【会签 -> 子流程(固定数量 2 个)】的流程,与 Simple 设计器保存后部署的模型一致
+ BpmSimpleModelNodeVO childProcessNode = new BpmSimpleModelNodeVO().setId("child").setName("子流程")
+ .setType(BpmSimpleModelNodeTypeEnum.CHILD_PROCESS.getType())
+ .setChildProcessSetting(new BpmSimpleModelNodeVO.ChildProcessSetting()
+ .setCalledProcessDefinitionKey("multiChild").setCalledProcessDefinitionName("子流程")
+ .setAsync(false).setSkipStartUserNode(false)
+ .setStartUserSetting(new BpmSimpleModelNodeVO.ChildProcessSetting.StartUserSetting()
+ .setType(BpmChildProcessStartUserTypeEnum.MAIN_PROCESS_START_USER.getType())
+ .setEmptyType(BpmChildProcessStartUserEmptyTypeEnum.MAIN_PROCESS_START_USER.getType()))
+ .setMultiInstanceSetting(new BpmSimpleModelNodeVO.ChildProcessSetting.MultiInstanceSetting()
+ .setEnable(true).setSequential(false).setApproveRatio(100)
+ .setSourceType(BpmChildProcessMultiInstanceSourceTypeEnum.FIXED_QUANTITY.getType())
+ .setSource("2")))
+ .setChildNode(new BpmSimpleModelNodeVO().setId("end").setType(BpmSimpleModelNodeTypeEnum.END_NODE.getType()));
+ BpmSimpleModelNodeVO approveNode = new BpmSimpleModelNodeVO().setId("approve").setName("会签审批")
+ .setType(BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType())
+ .setApproveType(BpmUserTaskApproveTypeEnum.USER.getType())
+ .setApproveMethod(BpmUserTaskApproveMethodEnum.RATIO.getMethod()).setApproveRatio(100)
+ .setCandidateStrategy(BpmTaskCandidateStrategyEnum.USER.getStrategy()).setCandidateParam("1,2")
+ .setChildNode(childProcessNode);
+ BpmnModel bpmnModel = SimpleModelUtils.buildBpmnModel("simpleMulti", "Simple 多实例", approveNode);
+ deploymentIds.add(repositoryService.createDeployment().addBpmnModel("simpleMulti.bpmn20.xml", bpmnModel)
+ .deploy().getId());
+ String processInstanceId = startProcessInstance("simpleMulti", null);
+
+ // 调用 + 断言:会签需要用户 1、用户 2 都审批通过
+ List tasks = getRunningTasks(processInstanceId);
+ assertEquals(List.of("1", "2"), convertList(tasks, Task::getAssignee));
+ tasks.forEach(this::approveTask);
+ // 调用 + 断言:并行发起 2 个子流程,全部审批通过后,主流程结束
+ List children = getChildProcessInstances(processInstanceId);
+ assertEquals(2, children.size());
+ children.forEach(child -> approveTask(getRunningTask(child.getId())));
+ assertProcessInstanceStatus(processInstanceId, BpmProcessInstanceStatusEnum.APPROVE.getStatus());
+ }
+
+ // ========== 私有方法 ==========
+
+ private String startProcessInstance(String processDefinitionKey, Map variables) {
+ return processInstanceService.createProcessInstance(1L, new BpmProcessInstanceCreateReqDTO()
+ .setProcessDefinitionKey(processDefinitionKey).setVariables(variables));
+ }
+
+ private List getChildProcessInstances(String processInstanceId) {
+ return runtimeService.createProcessInstanceQuery().superProcessInstanceId(processInstanceId).list();
+ }
+
+ private List getRunningTasks(String processInstanceId) {
+ // 按审批人排序,便于断言
+ return taskService.getRunningTaskListByProcessInstanceId(processInstanceId, null, null).stream()
+ .sorted(Comparator.comparing(Task::getAssignee)).toList();
+ }
+
+ private Task getRunningTask(String processInstanceId) {
+ List tasks = getRunningTasks(processInstanceId);
+ assertEquals(1, tasks.size());
+ return tasks.get(0);
+ }
+
+ private void approveTask(Task task) {
+ taskService.approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId())
+ .setReason("同意"));
+ }
+
+ private void assertTaskStatus(String taskId, Integer status, String reason) {
+ HistoricTaskInstance task = taskService.getHistoricTask(taskId);
+ assertEquals(status, FlowableUtils.getTaskStatus(task));
+ assertEquals(reason, FlowableUtils.getTaskReason(task));
+ }
+
+ private void assertProcessInstanceStatus(String processInstanceId, Integer status) {
+ HistoricProcessInstance processInstance = processInstanceService.getHistoricProcessInstance(processInstanceId);
+ assertNotNull(processInstance.getEndTime());
+ assertEquals(status, FlowableUtils.getProcessInstanceStatus(processInstance));
+ }
+
+}
diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BpmProcessDefinitionServiceFlowableTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BpmProcessDefinitionServiceFlowableTest.java
new file mode 100644
index 0000000..b180cc1
--- /dev/null
+++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BpmProcessDefinitionServiceFlowableTest.java
@@ -0,0 +1,61 @@
+package cn.iocoder.yudao.module.bpm.framework.flowable;
+
+import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionServiceImpl;
+import jakarta.annotation.Resource;
+import org.flowable.engine.RepositoryService;
+import org.flowable.engine.repository.ProcessDefinition;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * {@link BpmProcessDefinitionServiceImpl} 依赖 Flowable 引擎的集成测试
+ *
+ * 基类将 BpmProcessDefinitionService 声明为 Mock,这里直接构造真实实现,并注入真实的 {@link RepositoryService}。
+ *
+ * @author HUIHUI
+ */
+public class BpmProcessDefinitionServiceFlowableTest extends BaseFlowableUnitTest {
+
+ @Resource
+ private RepositoryService repositoryService;
+
+ private final BpmProcessDefinitionServiceImpl processDefinitionServiceImpl = new BpmProcessDefinitionServiceImpl();
+
+ private String deploymentId;
+
+ @BeforeEach
+ public void setUp() {
+ ReflectionTestUtils.setField(processDefinitionServiceImpl, "repositoryService", repositoryService);
+ deploymentId = repositoryService.createDeployment()
+ .addClasspathResource("bpmn/simple-approve.bpmn20.xml")
+ .deploy().getId();
+ }
+
+ @AfterEach
+ public void tearDown() {
+ repositoryService.deleteDeployment(deploymentId, true);
+ }
+
+ @Test
+ public void testGetProcessDefinition_success() {
+ // 准备参数
+ String id = repositoryService.createProcessDefinitionQuery().deploymentId(deploymentId).singleResult().getId();
+
+ // 调用
+ ProcessDefinition processDefinition = processDefinitionServiceImpl.getProcessDefinition(id);
+ // 断言
+ assertEquals("simpleApprove", processDefinition.getKey());
+ }
+
+ @Test
+ public void testGetProcessDefinition_notExists() {
+ // 调用,并断言:不存在时返回 null,而不是抛出 Flowable 异常,调用方据此提示“流程定义不存在”
+ assertNull(processDefinitionServiceImpl.getProcessDefinition("not-exists"));
+ assertNull(processDefinitionServiceImpl.getProcessDefinition(null));
+ }
+
+}
diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BpmProcessInstanceQueryFlowableTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BpmProcessInstanceQueryFlowableTest.java
new file mode 100644
index 0000000..3e628ea
--- /dev/null
+++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BpmProcessInstanceQueryFlowableTest.java
@@ -0,0 +1,332 @@
+package cn.iocoder.yudao.module.bpm.framework.flowable;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
+import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailReqVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailRespVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailRespVO.ActivityNode;
+import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceBpmnModelViewRespVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstancePageReqVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskApproveReqVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskRejectReqVO;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum;
+import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
+import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceServiceImpl;
+import cn.iocoder.yudao.module.bpm.service.task.BpmTaskServiceImpl;
+import jakarta.annotation.Resource;
+import org.flowable.engine.RepositoryService;
+import org.flowable.engine.RuntimeService;
+import org.flowable.engine.TaskService;
+import org.flowable.engine.history.HistoricProcessInstance;
+import org.flowable.engine.repository.Deployment;
+import org.flowable.engine.runtime.ProcessInstance;
+import org.flowable.task.api.Task;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.time.LocalDateTime;
+import java.util.*;
+
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
+import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_DEFINITION_NOT_EXISTS;
+import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_INSTANCE_NOT_EXISTS;
+import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.START_USER_NODE_ID;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+/**
+ * {@link BpmProcessInstanceServiceImpl} 查询相关方法的 Flowable 集成测试
+ *
+ * 流程实例、任务的状态由真实的 Flowable 事件监听器维护,覆盖审批详情、流程图进度、分页、下一节点预测等基于历史数据的查询。
+ *
+ * @author HUIHUI
+ */
+public class BpmProcessInstanceQueryFlowableTest extends BaseFlowableUnitTest {
+
+ @Resource
+ private RepositoryService repositoryService;
+ @Resource
+ private RuntimeService runtimeService;
+ @Resource
+ private TaskService flowableTaskService;
+
+ @Resource
+ private BpmTaskServiceImpl taskService;
+ @Resource
+ private BpmProcessInstanceServiceImpl processInstanceService;
+
+ private final List deploymentIds = new ArrayList<>();
+
+ @BeforeEach
+ public void setUp() {
+ // 两级审批流程:task1 -> task2;审批人均为用户 1,候选人预测同样为用户 1
+ deploy("bpmn/two-step-approve.bpmn20.xml");
+ mockFlowableBackedServices(repositoryService);
+ when(taskCandidateInvoker.calculateUsersByActivity(any(), any(), any(), any(), any()))
+ .thenReturn(new LinkedHashSet<>(List.of(1L)));
+ }
+
+ @AfterEach
+ public void tearDown() {
+ deploymentIds.forEach(id -> repositoryService.deleteDeployment(id, true));
+ }
+
+ // ========== getApprovalDetail ==========
+
+ @Test
+ public void testGetApprovalDetail_running() {
+ // 准备参数:task1 已审批,task2 待审批
+ String processInstanceId = startProcessInstance(1L, null);
+ approveRunningTask(processInstanceId);
+ Task task2 = getRunningTask(processInstanceId);
+
+ // 调用
+ BpmApprovalDetailRespVO respVO = processInstanceService.getApprovalDetail(1L,
+ new BpmApprovalDetailReqVO().setProcessInstanceId(processInstanceId));
+ // 断言:已结束(发起人、task1) + 进行中(task2) + 预测(结束节点)
+ assertEquals(BpmProcessInstanceStatusEnum.RUNNING.getStatus(), respVO.getStatus());
+ List nodes = respVO.getActivityNodes();
+ assertEquals(List.of(START_USER_NODE_ID, "task1", "task2", "end"), convertList(nodes, ActivityNode::getId));
+ assertEquals(List.of(BpmTaskStatusEnum.APPROVE.getStatus(), BpmTaskStatusEnum.APPROVE.getStatus(),
+ BpmTaskStatusEnum.RUNNING.getStatus(), BpmTaskStatusEnum.NOT_START.getStatus()),
+ convertList(nodes, ActivityNode::getStatus));
+ assertEquals(BpmSimpleModelNodeTypeEnum.START_USER_NODE.getType(), nodes.get(0).getNodeType());
+ assertEquals(1L, nodes.get(0).getTasks().get(0).getAssignee());
+ assertEquals("同意", nodes.get(1).getTasks().get(0).getReason());
+ assertEquals("用户1", nodes.get(2).getTasks().get(0).getAssigneeUser().getNickname());
+ assertEquals(BpmSimpleModelNodeTypeEnum.END_NODE.getType(), nodes.get(3).getNodeType());
+ // 断言:当前用户的待办为 task2,并返回流程实例信息
+ assertEquals(task2.getId(), respVO.getTodoTask().getId());
+ assertEquals(processInstanceId, respVO.getProcessInstance().getId());
+ assertEquals("用户1", respVO.getProcessInstance().getStartUser().getNickname());
+ }
+
+ @Test
+ public void testGetApprovalDetail_approved() {
+ // 准备参数:task1、task2 均审批通过
+ String processInstanceId = startProcessInstance(1L, null);
+ approveRunningTask(processInstanceId);
+ approveRunningTask(processInstanceId);
+
+ // 调用
+ BpmApprovalDetailRespVO respVO = processInstanceService.getApprovalDetail(1L,
+ new BpmApprovalDetailReqVO().setProcessInstanceId(processInstanceId));
+ // 断言:流程已结束,不再预测;最后为审批通过的结束节点
+ assertEquals(BpmProcessInstanceStatusEnum.APPROVE.getStatus(), respVO.getStatus());
+ List nodes = respVO.getActivityNodes();
+ assertEquals(4, nodes.size());
+ assertEquals(List.of(START_USER_NODE_ID, "task1", "task2"), convertList(nodes.subList(0, 3), ActivityNode::getId));
+ ActivityNode endNode = nodes.get(3);
+ assertEquals(BpmSimpleModelNodeTypeEnum.END_NODE.getType(), endNode.getNodeType());
+ assertEquals(BpmProcessInstanceStatusEnum.APPROVE.getStatus(), endNode.getStatus());
+ assertNotNull(endNode.getEndTime());
+ assertNull(respVO.getTodoTask());
+ }
+
+ @Test
+ public void testGetApprovalDetail_rejected() {
+ // 准备参数:task1 审批不通过
+ String processInstanceId = startProcessInstance(1L, null);
+ Task task1 = getRunningTask(processInstanceId);
+ taskService.rejectTask(1L, new BpmTaskRejectReqVO().setId(task1.getId()).setReason("不同意"));
+
+ // 调用
+ BpmApprovalDetailRespVO respVO = processInstanceService.getApprovalDetail(1L,
+ new BpmApprovalDetailReqVO().setProcessInstanceId(processInstanceId));
+ // 断言:不通过时不展示结束节点
+ assertEquals(BpmProcessInstanceStatusEnum.REJECT.getStatus(), respVO.getStatus());
+ List nodes = respVO.getActivityNodes();
+ assertEquals(List.of(START_USER_NODE_ID, "task1"), convertList(nodes, ActivityNode::getId));
+ assertEquals(BpmTaskStatusEnum.REJECT.getStatus(), nodes.get(1).getStatus());
+ assertEquals("不同意", nodes.get(1).getTasks().get(0).getReason());
+ assertNull(respVO.getTodoTask());
+ }
+
+ @Test
+ public void testGetApprovalDetail_runningCallActivity() {
+ // 准备参数:主流程停留在子流程节点
+ deploy("bpmn/lifecycle/child.bpmn20.xml");
+ deploy("bpmn/lifecycle/child-parent.bpmn20.xml");
+ String processInstanceId = processInstanceService.createProcessInstance(1L, new BpmProcessInstanceCreateReqDTO()
+ .setProcessDefinitionKey("childParent"));
+ ProcessInstance child = runtimeService.createProcessInstanceQuery()
+ .superProcessInstanceId(processInstanceId).singleResult();
+
+ // 调用
+ BpmApprovalDetailRespVO respVO = processInstanceService.getApprovalDetail(1L,
+ new BpmApprovalDetailReqVO().setProcessInstanceId(processInstanceId));
+ // 断言:进行中的子流程节点关联子流程实例(CallActivity 没有自身的任务)
+ // 注意:BPMN 设计器的预测(BpmnModelUtils#simulateProcess)不会越过 CallActivity,因此不断言后续预测节点
+ List nodes = respVO.getActivityNodes();
+ assertEquals(List.of(START_USER_NODE_ID, "child"), convertList(nodes.subList(0, 2), ActivityNode::getId));
+ ActivityNode childNode = nodes.get(1);
+ assertEquals(BpmSimpleModelNodeTypeEnum.CHILD_PROCESS.getType(), childNode.getNodeType());
+ assertEquals(BpmTaskStatusEnum.RUNNING.getStatus(), childNode.getStatus());
+ assertEquals(child.getId(), childNode.getProcessInstanceId());
+ assertTrue(childNode.getTasks().isEmpty());
+ // 断言:用户 1 在主流程中没有待办
+ assertNull(respVO.getTodoTask());
+ }
+
+ @Test
+ public void testGetApprovalDetail_notExists() {
+ // 调用,并断言异常
+ assertServiceException(() -> processInstanceService.getApprovalDetail(1L,
+ new BpmApprovalDetailReqVO().setProcessInstanceId("not-exists")), PROCESS_INSTANCE_NOT_EXISTS);
+ }
+
+ @Test
+ public void testGetApprovalDetail_definitionNotExists() {
+ // 调用,并断言异常:发起前预览审批详情时,流程定义已被删除
+ assertServiceException(() -> processInstanceService.getApprovalDetail(1L,
+ new BpmApprovalDetailReqVO().setProcessDefinitionId("not-exists")), PROCESS_DEFINITION_NOT_EXISTS);
+ }
+
+ // ========== getProcessInstanceBpmnModelView ==========
+
+ @Test
+ public void testGetProcessInstanceBpmnModelView_running() {
+ // 准备参数:task1 已审批,task2 待审批
+ String processInstanceId = startProcessInstance(1L, null);
+ approveRunningTask(processInstanceId);
+
+ // 调用
+ BpmProcessInstanceBpmnModelViewRespVO respVO = processInstanceService.getProcessInstanceBpmnModelView(processInstanceId);
+ // 断言:进度信息
+ assertEquals(Set.of("task2"), respVO.getUnfinishedTaskActivityIds());
+ assertTrue(respVO.getFinishedTaskActivityIds().containsAll(Set.of("start", "task1")));
+ assertFalse(respVO.getFinishedTaskActivityIds().contains("task2"));
+ assertEquals(Set.of("flow-start", "flow-task1"), respVO.getFinishedSequenceFlowActivityIds());
+ assertTrue(respVO.getRejectedTaskActivityIds().isEmpty());
+ // 断言:基础信息
+ assertEquals(BpmProcessInstanceStatusEnum.RUNNING.getStatus(), respVO.getProcessInstance().getStatus());
+ assertEquals("用户1", respVO.getProcessInstance().getStartUser().getNickname());
+ assertEquals(List.of(BpmTaskStatusEnum.APPROVE.getStatus(), BpmTaskStatusEnum.RUNNING.getStatus()),
+ convertList(respVO.getTasks(), task -> task.getStatus()));
+ assertTrue(respVO.getBpmnXml().contains("twoStepApprove"));
+ assertNull(respVO.getSimpleModel());
+ }
+
+ @Test
+ public void testGetProcessInstanceBpmnModelView_rejected() {
+ // 准备参数:task1 已审批,task2 审批不通过(驳回到 task1),task1 再次审批不通过
+ String processInstanceId = startProcessInstance(1L, null);
+ approveRunningTask(processInstanceId);
+ Task task = getRunningTask(processInstanceId);
+ taskService.rejectTask(1L, new BpmTaskRejectReqVO().setId(task.getId()).setReason("驳回"));
+ task = getRunningTask(processInstanceId);
+ assertEquals("task1", task.getTaskDefinitionKey());
+ taskService.rejectTask(1L, new BpmTaskRejectReqVO().setId(task.getId()).setReason("不同意"));
+
+ // 调用
+ BpmProcessInstanceBpmnModelViewRespVO respVO = processInstanceService.getProcessInstanceBpmnModelView(processInstanceId);
+ // 断言:只取最后一个不通过的节点
+ assertEquals(BpmProcessInstanceStatusEnum.REJECT.getStatus(), respVO.getProcessInstance().getStatus());
+ assertEquals(Set.of("task1"), respVO.getRejectedTaskActivityIds());
+ assertFalse(respVO.getFinishedTaskActivityIds().contains("task1"));
+ assertTrue(respVO.getUnfinishedTaskActivityIds().isEmpty());
+ }
+
+ @Test
+ public void testGetProcessInstanceBpmnModelView_notExists() {
+ assertNull(processInstanceService.getProcessInstanceBpmnModelView("not-exists"));
+ }
+
+ // ========== getProcessInstancePage ==========
+
+ @Test
+ public void testGetProcessInstancePage() {
+ // 准备参数:用户 1 发起并审批通过;用户 2 发起且审批中,表单 day = 3
+ String approvedId = startProcessInstance(1L, null);
+ approveRunningTask(approvedId);
+ approveRunningTask(approvedId);
+ String runningId = startProcessInstance(2L, new HashMap<>(Map.of("day", 3)));
+ LocalDateTime now = LocalDateTime.now();
+
+ // 调用 + 断言:【我的流程】按发起人过滤
+ assertPageIds(List.of(approvedId), processInstanceService.getProcessInstancePage(1L, new BpmProcessInstancePageReqVO()));
+ // 调用 + 断言:【管理流程】按 startUserId 过滤
+ assertPageIds(List.of(runningId), processInstanceService.getProcessInstancePage(null,
+ (BpmProcessInstancePageReqVO) new BpmProcessInstancePageReqVO().setStartUserId(2L)));
+ // 调用 + 断言:名称、流程标识、发起时间,按发起时间倒序
+ BpmProcessInstancePageReqVO reqVO = new BpmProcessInstancePageReqVO();
+ reqVO.setName("两级").setProcessDefinitionKey("twoStepApprove")
+ .setCreateTime(new LocalDateTime[]{now.minusHours(1), now.plusHours(1)});
+ assertEquals(Set.of(approvedId, runningId),
+ new HashSet<>(convertList(processInstanceService.getProcessInstancePage(null, reqVO).getList(),
+ HistoricProcessInstance::getId)));
+ // 调用 + 断言:状态、结束时间
+ reqVO = new BpmProcessInstancePageReqVO();
+ reqVO.setStatus(BpmProcessInstanceStatusEnum.APPROVE.getStatus())
+ .setEndTime(new LocalDateTime[]{now.minusHours(1), now.plusHours(1)});
+ assertPageIds(List.of(approvedId), processInstanceService.getProcessInstancePage(null, reqVO));
+ // 调用 + 断言:表单字段,空值的字段被忽略
+ reqVO = new BpmProcessInstancePageReqVO();
+ reqVO.setFormFieldsParams("{\"day\": 3, \"reason\": \"\"}");
+ assertPageIds(List.of(runningId), processInstanceService.getProcessInstancePage(null, reqVO));
+ // 调用 + 断言:分类不匹配时返回空
+ reqVO = new BpmProcessInstancePageReqVO();
+ reqVO.setCategory("not-exists");
+ PageResult pageResult = processInstanceService.getProcessInstancePage(null, reqVO);
+ assertEquals(0, pageResult.getTotal());
+ assertTrue(pageResult.getList().isEmpty());
+ }
+
+ // ========== getNextApprovalNodes ==========
+
+ @Test
+ public void testGetNextApprovalNodes() {
+ // 准备参数
+ String processInstanceId = startProcessInstance(1L, null);
+ Task task1 = getRunningTask(processInstanceId);
+
+ // 调用 + 断言:task1 的下一个节点为 task2,并预测审批人
+ List nodes = processInstanceService.getNextApprovalNodes(1L,
+ new BpmApprovalDetailReqVO().setTaskId(task1.getId()));
+ assertEquals(1, nodes.size());
+ assertEquals("task2", nodes.get(0).getId());
+ assertEquals(BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType(), nodes.get(0).getNodeType());
+ assertEquals(BpmTaskStatusEnum.RUNNING.getStatus(), nodes.get(0).getStatus());
+ assertEquals(List.of(1L), nodes.get(0).getCandidateUserIds());
+ assertEquals("用户1", nodes.get(0).getCandidateUsers().get(0).getNickname());
+
+ // 调用 + 断言:task2 的下一个节点为结束节点,无需审批
+ approveRunningTask(processInstanceId);
+ Task task2 = getRunningTask(processInstanceId);
+ assertTrue(processInstanceService.getNextApprovalNodes(1L,
+ new BpmApprovalDetailReqVO().setTaskId(task2.getId())).isEmpty());
+ }
+
+ // ========== 私有方法 ==========
+
+ private void deploy(String resource) {
+ Deployment deployment = repositoryService.createDeployment().addClasspathResource(resource).deploy();
+ deploymentIds.add(deployment.getId());
+ }
+
+ private String startProcessInstance(Long userId, Map variables) {
+ return processInstanceService.createProcessInstance(userId, new BpmProcessInstanceCreateReqDTO()
+ .setProcessDefinitionKey("twoStepApprove").setVariables(variables));
+ }
+
+ private Task getRunningTask(String processInstanceId) {
+ List tasks = flowableTaskService.createTaskQuery().processInstanceId(processInstanceId).list();
+ assertEquals(1, tasks.size());
+ return tasks.get(0);
+ }
+
+ private void approveRunningTask(String processInstanceId) {
+ Task task = getRunningTask(processInstanceId);
+ taskService.approveTask(1L, new BpmTaskApproveReqVO().setId(task.getId()).setReason("同意"));
+ }
+
+ private static void assertPageIds(List expected, PageResult pageResult) {
+ assertEquals(expected.size(), pageResult.getTotal());
+ assertEquals(expected, convertList(pageResult.getList(), HistoricProcessInstance::getId));
+ }
+
+}
diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BpmProcessLifecycleFlowableTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BpmProcessLifecycleFlowableTest.java
new file mode 100644
index 0000000..6c3446d
--- /dev/null
+++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BpmProcessLifecycleFlowableTest.java
@@ -0,0 +1,245 @@
+package cn.iocoder.yudao.module.bpm.framework.flowable;
+
+import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
+import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum;
+import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum;
+import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.listener.BpmTaskEventListener;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
+import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceServiceImpl;
+import cn.iocoder.yudao.module.bpm.service.task.BpmTaskServiceImpl;
+import cn.iocoder.yudao.module.bpm.service.task.listener.BpmCallActivityListener;
+import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import jakarta.annotation.Resource;
+import org.flowable.engine.HistoryService;
+import org.flowable.engine.RepositoryService;
+import org.flowable.engine.RuntimeService;
+import org.flowable.engine.TaskService;
+import org.flowable.engine.history.HistoricProcessInstance;
+import org.flowable.engine.runtime.ProcessInstance;
+import org.flowable.task.api.Task;
+import org.flowable.task.api.history.HistoricTaskInstance;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.START_USER_NODE_ID;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * 任务创建、分配事件后自动处理,以及子流程发起的 Flowable 集成测试
+ *
+ * 补充 {@link BpmTaskServiceFlowableTest} 未覆盖的 {@link BpmTaskServiceImpl#processTaskCreated}、
+ * {@link BpmTaskServiceImpl#processTaskAssigned} 分支,以及 {@link BpmCallActivityListener}、
+ * {@link BpmProcessInstanceServiceImpl#processProcessInstanceCreated} 在真实事务提交后的回调。
+ * 事件均由 {@link BpmTaskEventListener} 等真实监听器触发。
+ *
+ * @author HUIHUI
+ */
+public class BpmProcessLifecycleFlowableTest extends BaseFlowableUnitTest {
+
+ @Resource
+ private RepositoryService repositoryService;
+ @Resource
+ private RuntimeService runtimeService;
+ @Resource
+ private HistoryService historyService;
+ @Resource
+ private TaskService flowableTaskService;
+
+ @Resource
+ private BpmProcessInstanceServiceImpl processInstanceService;
+
+ private final List deploymentIds = new ArrayList<>();
+
+ @BeforeEach
+ public void setUp() {
+ mockFlowableBackedServices(repositoryService);
+ }
+
+ @AfterEach
+ public void tearDown() {
+ // 级联删除流程定义、流程实例与历史数据
+ deploymentIds.forEach(id -> repositoryService.deleteDeployment(id, true));
+ }
+
+ // ========== 任务创建后的自动处理 ==========
+
+ @Test
+ public void testProcessTaskCreated_assignEmptyApprove() {
+ // 准备参数:人工审批,计算不到审批人时自动通过
+ deploy("bpmn/lifecycle/assign-empty-approve.bpmn20.xml");
+ when(taskCandidateInvoker.calculateUsersByTask(any())).thenReturn(Set.of());
+
+ // 调用
+ String processInstanceId = startProcessInstance("assignEmptyApprove", 1L);
+ // 断言:任务没有审批人,在事务完成后自动通过,且不会发送待办通知
+ HistoricTaskInstance task = getHistoricTask(processInstanceId, "task");
+ assertNull(task.getAssignee());
+ assertTaskStatus(task.getId(), BpmTaskStatusEnum.APPROVE.getStatus(), BpmReasonEnum.ASSIGN_EMPTY_APPROVE.getReason());
+ assertProcessInstanceStatus(processInstanceId, BpmProcessInstanceStatusEnum.APPROVE.getStatus());
+ verify(messageService, never()).sendMessageWhenTaskAssigned(any());
+ }
+
+ @Test
+ public void testProcessTaskCreated_assignEmptyReject() {
+ // 准备参数:人工审批,计算不到审批人时自动拒绝
+ deploy("bpmn/lifecycle/assign-empty-reject.bpmn20.xml");
+ when(taskCandidateInvoker.calculateUsersByTask(any())).thenReturn(Set.of());
+
+ // 调用
+ String processInstanceId = startProcessInstance("assignEmptyReject", 1L);
+ // 断言:任务没有审批人,在事务完成后自动拒绝,流程不通过
+ HistoricTaskInstance task = getHistoricTask(processInstanceId, "task");
+ assertNull(task.getAssignee());
+ assertTaskStatus(task.getId(), BpmTaskStatusEnum.REJECT.getStatus(), BpmReasonEnum.ASSIGN_EMPTY_REJECT.getReason());
+ assertProcessInstanceStatus(processInstanceId, BpmProcessInstanceStatusEnum.REJECT.getStatus());
+ verify(messageService, never()).sendMessageWhenTaskAssigned(any());
+ }
+
+ // ========== 任务分配后的自动处理 ==========
+
+ @Test
+ public void testProcessTaskAssigned_startUserNodeAutoApprove() {
+ // 准备参数
+ deploy("bpmn/lifecycle/start-user-node.bpmn20.xml");
+
+ // 调用
+ String processInstanceId = startProcessInstance("startUserNode", 1L);
+ // 断言:发起人节点首次自动通过,流转到下一个审批节点
+ assertTaskStatus(getHistoricTask(processInstanceId, START_USER_NODE_ID).getId(),
+ BpmTaskStatusEnum.APPROVE.getStatus(),
+ BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_SKIP_START_USER_NODE.getReason());
+ Task task = getRunningTask(processInstanceId);
+ assertEquals("task", task.getTaskDefinitionKey());
+ assertEquals(BpmTaskStatusEnum.RUNNING.getStatus(), FlowableUtils.getTaskStatus(task));
+ }
+
+ @Test
+ public void testProcessTaskAssigned_startUserSkip() {
+ // 准备参数:审批人与发起人相同时,自动跳过
+ deploy("bpmn/lifecycle/start-user-skip.bpmn20.xml");
+
+ // 调用 + 断言:发起人 1 = 审批人 1,自动通过
+ String processInstanceId = startProcessInstance("startUserSkip", 1L);
+ assertTaskStatus(getHistoricTask(processInstanceId, "task").getId(), BpmTaskStatusEnum.APPROVE.getStatus(),
+ BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_SKIP.getReason());
+ assertProcessInstanceStatus(processInstanceId, BpmProcessInstanceStatusEnum.APPROVE.getStatus());
+
+ // 调用 + 断言:发起人 2 ≠ 审批人 1,正常待审批并发送待办通知
+ String otherProcessInstanceId = startProcessInstance("startUserSkip", 2L);
+ Task task = getRunningTask(otherProcessInstanceId);
+ assertEquals("1", task.getAssignee());
+ assertEquals(BpmTaskStatusEnum.RUNNING.getStatus(), FlowableUtils.getTaskStatus(task));
+ verify(messageService).sendMessageWhenTaskAssigned(argThat(reqDTO -> task.getId().equals(reqDTO.getTaskId())));
+ }
+
+ @Test
+ public void testProcessTaskAssigned_startUserTransferDeptLeader() {
+ // 准备参数:审批人与发起人相同时转交部门负责人;发起人 1 的部门负责人为 2
+ deploy("bpmn/lifecycle/start-user-transfer-dept-leader.bpmn20.xml");
+ when(adminUserApi.getUser(1L)).thenReturn(new AdminUserRespDTO().setId(1L).setNickname("用户1").setDeptId(10L));
+ when(deptApi.getDept(10L)).thenReturn(new DeptRespDTO().setId(10L).setLeaderUserId(2L));
+
+ // 调用
+ String processInstanceId = startProcessInstance("startUserTransferDeptLeader", 1L);
+ // 断言:任务转交给部门负责人,原审批人成为 owner,仅向部门负责人发送待办通知
+ Task task = getRunningTask(processInstanceId);
+ assertEquals("2", task.getAssignee());
+ assertEquals("1", task.getOwner());
+ verify(messageService).sendMessageWhenTaskAssigned(argThat(reqDTO ->
+ task.getId().equals(reqDTO.getTaskId()) && reqDTO.getAssigneeUserId().equals(2L)));
+ verify(messageService, times(1)).sendMessageWhenTaskAssigned(any());
+ }
+
+ @Test
+ public void testProcessTaskAssigned_startUserDeptLeaderNotFound() {
+ // 准备参数:审批人与发起人相同时转交部门负责人;发起人 1 的部门没有负责人
+ deploy("bpmn/lifecycle/start-user-transfer-dept-leader.bpmn20.xml");
+ when(adminUserApi.getUser(1L)).thenReturn(new AdminUserRespDTO().setId(1L).setNickname("用户1").setDeptId(10L));
+ when(deptApi.getDept(10L)).thenReturn(new DeptRespDTO().setId(10L));
+
+ // 调用
+ String processInstanceId = startProcessInstance("startUserTransferDeptLeader", 1L);
+ // 断言:找不到部门负责人时自动通过
+ assertTaskStatus(getHistoricTask(processInstanceId, "task").getId(), BpmTaskStatusEnum.APPROVE.getStatus(),
+ BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_DEPT_LEADER_NOT_FOUND.getReason());
+ assertProcessInstanceStatus(processInstanceId, BpmProcessInstanceStatusEnum.APPROVE.getStatus());
+ }
+
+ // ========== 子流程发起 ==========
+
+ @Test
+ public void testCallActivity_startUserAndTitle() {
+ // 准备参数:开启自定义标题
+ deploy("bpmn/lifecycle/child.bpmn20.xml");
+ deploy("bpmn/lifecycle/child-parent.bpmn20.xml");
+ when(processDefinitionService.getProcessDefinitionInfo(anyString())).thenAnswer(invocation ->
+ new BpmProcessDefinitionInfoDO().setProcessDefinitionId(invocation.getArgument(0))
+ .setModelType(BpmModelTypeEnum.BPMN.getType())
+ .setTitleSetting(new BpmModelMetaInfoVO.TitleSetting().setEnable(true)
+ .setTitle("{PROCESS_DEFINITION_NAME}-{PROCESS_START_USER_ID}")));
+
+ // 调用:用户 2 发起主流程
+ String processInstanceId = startProcessInstance("childParent", 2L);
+ // 断言:主流程标题在发起时生成
+ assertEquals("主流程-用户2", processInstanceService.getProcessInstance(processInstanceId).getName());
+ // 断言:子流程发起人同主流程(BpmCallActivityListener),标题在 PROCESS_CREATED 事务提交后生成
+ ProcessInstance child = runtimeService.createProcessInstanceQuery()
+ .superProcessInstanceId(processInstanceId).singleResult();
+ assertNotNull(child);
+ assertEquals("2", child.getStartUserId());
+ assertEquals("子流程-用户2", processInstanceService.getProcessInstance(child.getId()).getName());
+ Task childTask = getRunningTask(child.getId());
+ assertEquals("task", childTask.getTaskDefinitionKey());
+ assertEquals("子流程审批", childTask.getName());
+ }
+
+ // ========== 私有方法 ==========
+
+ private void deploy(String resource) {
+ deploymentIds.add(repositoryService.createDeployment().addClasspathResource(resource).deploy().getId());
+ }
+
+ private String startProcessInstance(String processDefinitionKey, Long userId) {
+ return processInstanceService.createProcessInstance(userId, new BpmProcessInstanceCreateReqDTO()
+ .setProcessDefinitionKey(processDefinitionKey).setBusinessKey("business-1"));
+ }
+
+ private Task getRunningTask(String processInstanceId) {
+ List tasks = flowableTaskService.createTaskQuery().processInstanceId(processInstanceId)
+ .includeTaskLocalVariables().list();
+ assertEquals(1, tasks.size());
+ return tasks.get(0);
+ }
+
+ private HistoricTaskInstance getHistoricTask(String processInstanceId, String taskDefinitionKey) {
+ return historyService.createHistoricTaskInstanceQuery().processInstanceId(processInstanceId)
+ .taskDefinitionKey(taskDefinitionKey).includeTaskLocalVariables().singleResult();
+ }
+
+ private void assertTaskStatus(String taskId, Integer status, String reason) {
+ HistoricTaskInstance task = historyService.createHistoricTaskInstanceQuery().taskId(taskId)
+ .includeTaskLocalVariables().singleResult();
+ assertEquals(status, FlowableUtils.getTaskStatus(task));
+ assertEquals(reason, FlowableUtils.getTaskReason(task));
+ }
+
+ private void assertProcessInstanceStatus(String processInstanceId, Integer status) {
+ HistoricProcessInstance processInstance = historyService.createHistoricProcessInstanceQuery()
+ .processInstanceId(processInstanceId).includeProcessVariables().singleResult();
+ assertNotNull(processInstance.getEndTime());
+ assertEquals(status, FlowableUtils.getProcessInstanceStatus(processInstance));
+ }
+
+}
diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BpmTaskServiceFlowableTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BpmTaskServiceFlowableTest.java
new file mode 100644
index 0000000..fd4bdf8
--- /dev/null
+++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BpmTaskServiceFlowableTest.java
@@ -0,0 +1,1221 @@
+package cn.iocoder.yudao.module.bpm.framework.flowable;
+
+import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent;
+import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCancelReqVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCreateReqVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.*;
+import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmAutoApproveTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskApproveTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskTimeoutHandlerTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.task.BpmAttachmentTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.task.BpmCommentTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum;
+import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum;
+import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskSignTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
+import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceServiceImpl;
+import cn.iocoder.yudao.module.bpm.service.task.BpmTaskServiceImpl;
+import jakarta.annotation.Resource;
+import org.flowable.bpmn.model.BpmnModel;
+import org.flowable.bpmn.model.FlowElement;
+import org.flowable.bpmn.model.UserTask;
+import org.flowable.engine.HistoryService;
+import org.flowable.engine.ManagementService;
+import org.flowable.engine.RepositoryService;
+import org.flowable.engine.RuntimeService;
+import org.flowable.engine.TaskService;
+import org.flowable.engine.delegate.DelegateExecution;
+import org.flowable.engine.repository.Deployment;
+import org.flowable.engine.runtime.ProcessInstance;
+import org.flowable.engine.task.Attachment;
+import org.flowable.job.api.Job;
+import org.flowable.task.api.DelegationState;
+import org.flowable.task.api.Task;
+import org.flowable.task.api.history.HistoricTaskInstance;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.test.context.event.ApplicationEvents;
+import org.springframework.test.context.event.RecordApplicationEvents;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
+import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * {@link BpmTaskServiceImpl}、{@link BpmProcessInstanceServiceImpl} 的 Flowable 集成测试
+ *
+ * @author HUIHUI
+ */
+@RecordApplicationEvents
+public class BpmTaskServiceFlowableTest extends BaseFlowableUnitTest {
+
+ @Resource
+ private RepositoryService repositoryService;
+ @Resource
+ private RuntimeService runtimeService;
+ @Resource
+ private HistoryService historyService;
+ @Resource
+ private TaskService flowableTaskService;
+ @Resource
+ private ManagementService managementService;
+
+ @Resource
+ private BpmTaskServiceImpl bpmTaskService;
+ @Resource
+ private BpmProcessInstanceServiceImpl processInstanceService;
+
+ @Resource
+ private ApplicationEvents applicationEvents;
+
+ private Deployment deployment;
+ /**
+ * 单个测试额外部署的流程,在 {@link #tearDown()} 中先于 {@link #deployment} 删除
+ */
+ private final List extraDeploymentIds = new ArrayList<>();
+
+ @BeforeEach
+ public void setUp() {
+ // 部署两级审批流程:task1 -> task2,task2 驳回时退回到 task1;审批人统一由测试上下文计算为用户 1
+ deployment = repositoryService.createDeployment()
+ .addClasspathResource("bpmn/two-step-approve.bpmn20.xml")
+ .deploy();
+
+ // 依赖的业务 Service 使用 Mock(委托给真实 Flowable 引擎),Flowable 相关 Service、BPM 任务/流程实例 Service 使用 Spring 中的真实 Bean
+ mockFlowableBackedServices(repositoryService);
+ }
+
+ @AfterEach
+ public void tearDown() {
+ // 清理测试数据:加签子任务没有 execution,需要先删除,否则 Flowable 级联删除流程实例时会空指针
+ List signTaskIds = flowableTaskService.createTaskQuery().deploymentId(deployment.getId()).list()
+ .stream().filter(task -> task.getExecutionId() == null).map(Task::getId).toList();
+ flowableTaskService.deleteTasks(signTaskIds);
+ extraDeploymentIds.forEach(id -> repositoryService.deleteDeployment(id, true));
+ repositoryService.deleteDeployment(deployment.getId(), true);
+ }
+
+ @Test
+ public void testTriggerTask_success() {
+ // 准备参数:流程停留在 receiveTask 等待节点
+ deploy("bpmn/trigger-task.bpmn20.xml");
+ String processInstanceId = startProcessInstance("triggerTask");
+ assertNotNull(runtimeService.createExecutionQuery().processInstanceId(processInstanceId)
+ .activityId("wait").singleResult());
+
+ // 调用
+ bpmTaskService.triggerTask(processInstanceId, "wait");
+ // 断言:触发等待节点后,流程结束
+ assertNull(processInstanceService.getProcessInstance(processInstanceId));
+ assertNotNull(processInstanceService.getHistoricProcessInstance(processInstanceId).getEndTime());
+ }
+
+ @Test
+ public void testTriggerTask_executionNotExists() {
+ // 准备参数
+ String processInstanceId = startProcessInstance();
+
+ // 调用:触发不存在的等待节点
+ bpmTaskService.triggerTask(processInstanceId, "notExists");
+ // 断言:流程实例不受影响
+ assertEquals("task1", getRunningTask(processInstanceId).getTaskDefinitionKey());
+ }
+
+ @Test
+ public void testApproveTask_success() {
+ // 准备参数
+ String processInstanceId = startProcessInstance();
+ Task task1 = getRunningTask(processInstanceId);
+ assertEquals("1", task1.getAssignee());
+
+ // 调用:审批 task1
+ bpmTaskService.approveTask(1L, new BpmTaskApproveReqVO().setId(task1.getId()).setReason("同意"));
+ // 断言:task1 审批通过,流程流转到 task2
+ assertTaskStatus(task1.getId(), BpmTaskStatusEnum.APPROVE.getStatus(), "同意");
+ verify(commentService).createComment(eq(task1.getId()), eq(processInstanceId),
+ eq(BpmCommentTypeEnum.APPROVE), eq("同意"));
+ Task task2 = getRunningTask(processInstanceId);
+ assertEquals("task2", task2.getTaskDefinitionKey());
+
+ // 调用:审批 task2
+ bpmTaskService.approveTask(1L, new BpmTaskApproveReqVO().setId(task2.getId()).setReason("同意"));
+ // 断言:流程实例结束
+ assertNull(processInstanceService.getProcessInstance(processInstanceId));
+ assertNotNull(processInstanceService.getHistoricProcessInstance(processInstanceId).getEndTime());
+ List tasks = bpmTaskService.getTaskListByProcessInstanceId(processInstanceId, true);
+ assertEquals(List.of("task1", "task2"), tasks.stream().map(HistoricTaskInstance::getTaskDefinitionKey).toList());
+ }
+
+ @Test
+ public void testApproveTask_withSignAndAttachment() {
+ // 准备参数:task1 开启签名
+ doAnswer(invocation -> {
+ BpmnModel bpmnModel = repositoryService.getBpmnModel(invocation.getArgument(0));
+ BpmnModelUtils.addExtensionElement(bpmnModel.getFlowElement("task1"), BpmnModelConstants.SIGN_ENABLE, "true");
+ return bpmnModel;
+ }).when(modelService).getBpmnModelByDefinitionId(anyString());
+ String processInstanceId = startProcessInstance();
+ Task task1 = getRunningTask(processInstanceId);
+
+ // 调用
+ bpmTaskService.approveTask(1L, new BpmTaskApproveReqVO().setId(task1.getId()).setReason("同意")
+ .setSignPicUrl("https://www.iocoder.cn/sign.png")
+ .setAttachments(List.of("https://www.iocoder.cn/files/a.pdf"))
+ .setVariables(Map.of("day", 3)));
+ // 断言:签名、表单变量保存到任务上,表单变量同步到流程实例
+ HistoricTaskInstance historicTask = bpmTaskService.getHistoricTask(task1.getId());
+ assertEquals("https://www.iocoder.cn/sign.png",
+ historicTask.getTaskLocalVariables().get(BpmnVariableConstants.TASK_SIGN_PIC_URL));
+ assertEquals(3, historicTask.getTaskLocalVariables().get("day"));
+ assertEquals(3, runtimeService.getVariable(processInstanceId, "day"));
+ // 断言:附件
+ List attachments = bpmTaskService.getAttachments(processInstanceId, Set.of(task1.getId()),
+ BpmAttachmentTypeEnum.TASK_ATTACHMENT);
+ assertEquals(1, attachments.size());
+ assertEquals("a.pdf", attachments.get(0).getName());
+ assertEquals("https://www.iocoder.cn/files/a.pdf", attachments.get(0).getUrl());
+ assertEquals("task2", getRunningTask(processInstanceId).getTaskDefinitionKey());
+ }
+
+ @Test
+ public void testApproveTask_assigneeNotSelf() {
+ // 准备参数
+ String processInstanceId = startProcessInstance();
+ Task task1 = getRunningTask(processInstanceId);
+
+ // 调用,并断言异常
+ assertServiceException(() -> bpmTaskService.approveTask(2L,
+ new BpmTaskApproveReqVO().setId(task1.getId()).setReason("同意")), TASK_OPERATE_FAIL_ASSIGN_NOT_SELF);
+ }
+
+ @Test
+ public void testRejectTask_finishProcessInstance() {
+ // 准备参数
+ String processInstanceId = startProcessInstance();
+ Task task1 = getRunningTask(processInstanceId);
+
+ // 调用:task1 未配置驳回策略,默认结束流程
+ bpmTaskService.rejectTask(1L, new BpmTaskRejectReqVO().setId(task1.getId()).setReason("不同意")
+ .setAttachments(List.of("https://www.iocoder.cn/files/b.pdf")));
+ // 断言:任务不通过,附件保存到任务上
+ assertTaskStatus(task1.getId(), BpmTaskStatusEnum.REJECT.getStatus(), "不同意");
+ List attachments = bpmTaskService.getAttachments(processInstanceId, Set.of(task1.getId()),
+ BpmAttachmentTypeEnum.TASK_ATTACHMENT);
+ assertEquals(List.of("b.pdf"), attachments.stream().map(Attachment::getName).toList());
+ // 断言:流程实例不通过并结束
+ assertNull(processInstanceService.getProcessInstance(processInstanceId));
+ assertEquals(BpmProcessInstanceStatusEnum.REJECT.getStatus(), getHistoricProcessVariable(processInstanceId,
+ BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS));
+ assertEquals("审批不通过任务,原因:不同意", getHistoricProcessVariable(processInstanceId,
+ BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_REASON));
+ }
+
+ @Test
+ public void testRejectTask_returnUserTask() {
+ // 准备参数:task1 审批通过后,流转到 task2
+ String processInstanceId = startProcessInstance();
+ approveRunningTask(processInstanceId);
+ Task task2 = getRunningTask(processInstanceId);
+
+ // 调用:task2 配置驳回到 task1
+ bpmTaskService.rejectTask(1L, new BpmTaskRejectReqVO().setId(task2.getId()).setReason("退回修改"));
+ // 断言:task2 被标记为退回,流程回到 task1
+ assertTaskStatus(task2.getId(), BpmTaskStatusEnum.RETURN.getStatus(), "退回修改");
+ Task task1 = getRunningTask(processInstanceId);
+ assertEquals("task1", task1.getTaskDefinitionKey());
+ assertEquals(Boolean.TRUE, runtimeService.getVariable(processInstanceId,
+ String.format(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, "task1")));
+ assertEquals(Set.of("task2"), runtimeService.getVariable(processInstanceId,
+ BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_TASK_IDS));
+
+ // 调用:再次审批 task1
+ bpmTaskService.approveTask(1L, new BpmTaskApproveReqVO().setId(task1.getId()).setReason("已修改"));
+ // 断言:退回标记被清理,流程再次流转到 task2
+ assertFalse(runtimeService.hasVariable(processInstanceId,
+ String.format(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, "task1")));
+ assertEquals("task2", getRunningTask(processInstanceId).getTaskDefinitionKey());
+ }
+
+ @Test
+ public void testRejectTask_signChildTask() {
+ // 准备参数:task1 向前加签给用户 2
+ String processInstanceId = startProcessInstance();
+ Task task1 = getRunningTask(processInstanceId);
+ bpmTaskService.createSignTask(1L, new BpmTaskSignCreateReqVO().setId(task1.getId())
+ .setType(BpmTaskSignTypeEnum.BEFORE.getType()).setUserIds(Set.of(2L)).setReason("加签"));
+ Task childTask = bpmTaskService.getTaskListByParentTaskId(task1.getId()).get(0);
+
+ // 调用:用户 2 审批不通过加签任务
+ bpmTaskService.rejectTask(2L, new BpmTaskRejectReqVO().setId(childTask.getId()).setReason("不同意"));
+ // 断言:加签任务、根任务都标记为不通过,流程实例不通过并结束
+ assertTaskStatus(childTask.getId(), BpmTaskStatusEnum.REJECT.getStatus(), "不同意");
+ assertTaskStatus(task1.getId(), BpmTaskStatusEnum.REJECT.getStatus(),
+ BpmCommentTypeEnum.REJECT.formatComment("加签任务不通过"));
+ verify(commentService).createComment(eq(task1.getId()), eq(processInstanceId),
+ eq(BpmCommentTypeEnum.REJECT), eq("加签任务不通过"));
+ assertNull(processInstanceService.getProcessInstance(processInstanceId));
+ assertEquals(BpmProcessInstanceStatusEnum.REJECT.getStatus(), getHistoricProcessVariable(processInstanceId,
+ BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS));
+ }
+
+ @Test
+ public void testGetUserTaskListByReturn_success() {
+ // 准备参数
+ String processInstanceId = startProcessInstance();
+ assertTrue(bpmTaskService.getUserTaskListByReturn(getRunningTask(processInstanceId).getId()).isEmpty());
+ approveRunningTask(processInstanceId);
+ Task task2 = getRunningTask(processInstanceId);
+
+ // 调用
+ List userTasks = bpmTaskService.getUserTaskListByReturn(task2.getId());
+ // 断言
+ assertEquals(List.of("task1"), userTasks.stream().map(UserTask::getId).toList());
+ }
+
+ @Test
+ public void testReturnTask_success() {
+ // 准备参数
+ String processInstanceId = startProcessInstance();
+ approveRunningTask(processInstanceId);
+ Task task2 = getRunningTask(processInstanceId);
+
+ // 调用
+ bpmTaskService.returnTask(1L, new BpmTaskReturnReqVO().setId(task2.getId())
+ .setTargetTaskDefinitionKey("task1").setReason("退回"));
+ // 断言
+ assertTaskStatus(task2.getId(), BpmTaskStatusEnum.RETURN.getStatus(), "退回");
+ verify(commentService).createComment(eq(task2.getId()), eq(processInstanceId),
+ eq(BpmCommentTypeEnum.RETURN), eq("退回"));
+ assertEquals("task1", getRunningTask(processInstanceId).getTaskDefinitionKey());
+ }
+
+ @Test
+ public void testReturnTask_targetNotExists() {
+ // 准备参数
+ String processInstanceId = startProcessInstance();
+ Task task1 = getRunningTask(processInstanceId);
+
+ // 调用,并断言异常
+ assertServiceException(() -> bpmTaskService.returnTask(1L, new BpmTaskReturnReqVO().setId(task1.getId())
+ .setTargetTaskDefinitionKey("notExists").setReason("退回")), TASK_TARGET_NODE_NOT_EXISTS);
+ }
+
+ @Test
+ public void testReturnTask_sourceTargetError() {
+ // 准备参数
+ String processInstanceId = startProcessInstance();
+ Task task1 = getRunningTask(processInstanceId);
+
+ // 调用,并断言异常:task2 位于 task1 之后,不能退回
+ assertServiceException(() -> bpmTaskService.returnTask(1L, new BpmTaskReturnReqVO().setId(task1.getId())
+ .setTargetTaskDefinitionKey("task2").setReason("退回")), TASK_RETURN_FAIL_SOURCE_TARGET_ERROR);
+ }
+
+ @Test
+ public void testDelegateTask_thenApprove() {
+ // 准备参数
+ String processInstanceId = startProcessInstance();
+ Task task1 = getRunningTask(processInstanceId);
+
+ // 调用:委派给用户 2
+ bpmTaskService.delegateTask(1L, new BpmTaskDelegateReqVO().setId(task1.getId())
+ .setDelegateUserId(2L).setReason("请帮忙审批"));
+ // 断言
+ Task delegatedTask = bpmTaskService.getTask(task1.getId());
+ assertEquals("1", delegatedTask.getOwner());
+ assertEquals("2", delegatedTask.getAssignee());
+ assertEquals(DelegationState.PENDING, delegatedTask.getDelegationState());
+ verify(commentService).createComment(eq(task1.getId()), eq(processInstanceId),
+ eq(BpmCommentTypeEnum.DELEGATE_START), eq("用户1"), eq("用户2"), eq("请帮忙审批"));
+
+ // 调用:被委派人审批,任务归还给原审批人
+ bpmTaskService.approveTask(2L, new BpmTaskApproveReqVO().setId(task1.getId()).setReason("已处理"));
+ // 断言:任务仍在 task1,审批人恢复为用户 1
+ Task resolvedTask = bpmTaskService.getTask(task1.getId());
+ assertEquals("1", resolvedTask.getAssignee());
+ assertEquals(DelegationState.RESOLVED, resolvedTask.getDelegationState());
+ assertEquals(BpmTaskStatusEnum.RUNNING.getStatus(),
+ resolvedTask.getTaskLocalVariables().get(BpmnVariableConstants.TASK_VARIABLE_STATUS));
+ }
+
+ @Test
+ public void testDelegateTask_redelegate() {
+ // 准备参数:用户 1 委派给用户 2
+ String processInstanceId = startProcessInstance();
+ Task task1 = getRunningTask(processInstanceId);
+ bpmTaskService.delegateTask(1L, new BpmTaskDelegateReqVO().setId(task1.getId())
+ .setDelegateUserId(2L).setReason("请帮忙审批"));
+
+ // 调用:用户 2 再委派给用户 3
+ bpmTaskService.delegateTask(2L, new BpmTaskDelegateReqVO().setId(task1.getId())
+ .setDelegateUserId(3L).setReason("转给用户 3"));
+ // 断言:owner 仍为最初的审批人
+ Task delegatedTask = bpmTaskService.getTask(task1.getId());
+ assertEquals("1", delegatedTask.getOwner());
+ assertEquals("3", delegatedTask.getAssignee());
+ verify(commentService).createComment(eq(task1.getId()), eq(processInstanceId),
+ eq(BpmCommentTypeEnum.DELEGATE_START), eq("用户2"), eq("用户3"), eq("转给用户 3"));
+
+ // 调用:用户 3 审批,任务归还给用户 1
+ bpmTaskService.approveTask(3L, new BpmTaskApproveReqVO().setId(task1.getId()).setReason("已处理"));
+ // 断言
+ assertEquals("1", bpmTaskService.getTask(task1.getId()).getAssignee());
+ verify(commentService).createComment(eq(task1.getId()), eq(processInstanceId),
+ eq(BpmCommentTypeEnum.DELEGATE_END), any(), eq("用户1"), eq("已处理"));
+ }
+
+ @Test
+ public void testDelegateTask_userRepeat() {
+ // 准备参数
+ String processInstanceId = startProcessInstance();
+ Task task1 = getRunningTask(processInstanceId);
+
+ // 调用,并断言异常
+ assertServiceException(() -> bpmTaskService.delegateTask(1L, new BpmTaskDelegateReqVO().setId(task1.getId())
+ .setDelegateUserId(1L).setReason("委派")), TASK_DELEGATE_FAIL_USER_REPEAT);
+ }
+
+ @Test
+ public void testTransferTask_success() {
+ // 准备参数
+ String processInstanceId = startProcessInstance();
+ Task task1 = getRunningTask(processInstanceId);
+
+ // 调用
+ bpmTaskService.transferTask(1L, new BpmTaskTransferReqVO().setId(task1.getId())
+ .setAssigneeUserId(2L).setReason("转办"));
+ // 断言
+ Task transferredTask = bpmTaskService.getTask(task1.getId());
+ assertEquals("1", transferredTask.getOwner());
+ assertEquals("2", transferredTask.getAssignee());
+ assertNull(transferredTask.getDelegationState());
+ verify(commentService).createComment(eq(task1.getId()), eq(processInstanceId),
+ eq(BpmCommentTypeEnum.TRANSFER), eq("用户1"), eq("用户2"), eq("转办"));
+ }
+
+ @Test
+ public void testTransferTask_emptyAssignee() {
+ // 准备参数:task1 计算不到审批人,且未配置自动处理
+ when(taskCandidateInvoker.calculateUsersByTask(any())).thenReturn(Set.of());
+ String processInstanceId = startProcessInstance();
+ Task task1 = getRunningTask(processInstanceId);
+ assertNull(task1.getAssignee());
+
+ // 调用:转办给用户 2
+ bpmTaskService.transferTask(1L, new BpmTaskTransferReqVO().setId(task1.getId())
+ .setAssigneeUserId(2L).setReason("转办"));
+ // 断言:用户 2 成为审批人
+ Task transferredTask = bpmTaskService.getTask(task1.getId());
+ assertEquals("2", transferredTask.getAssignee());
+ assertNull(transferredTask.getOwner());
+ // 调用 + 断言:用户 2 审批通过,流程流转到 task2
+ when(taskCandidateInvoker.calculateUsersByTask(any())).thenReturn(Set.of(1L));
+ bpmTaskService.approveTask(2L, new BpmTaskApproveReqVO().setId(task1.getId()).setReason("同意"));
+ assertEquals("task2", getRunningTask(processInstanceId).getTaskDefinitionKey());
+ }
+
+ @Test
+ public void testCreateSignTask_before_thenApproveChild() {
+ // 准备参数
+ String processInstanceId = startProcessInstance();
+ Task task1 = getRunningTask(processInstanceId);
+
+ // 调用:向前加签给用户 2
+ bpmTaskService.createSignTask(1L, new BpmTaskSignCreateReqVO().setId(task1.getId())
+ .setType(BpmTaskSignTypeEnum.BEFORE.getType()).setUserIds(Set.of(2L)).setReason("加签"));
+ // 断言:父任务等待子任务审批,子任务分配给用户 2
+ Task parentTask = bpmTaskService.getTask(task1.getId());
+ assertNull(parentTask.getAssignee());
+ assertEquals("1", parentTask.getOwner());
+ assertEquals(BpmTaskSignTypeEnum.BEFORE.getType(), parentTask.getScopeType());
+ assertEquals(BpmTaskStatusEnum.WAIT.getStatus(),
+ parentTask.getTaskLocalVariables().get(BpmnVariableConstants.TASK_VARIABLE_STATUS));
+ List childTasks = bpmTaskService.getTaskListByParentTaskId(task1.getId());
+ assertEquals(1, childTasks.size());
+ assertEquals("2", childTasks.get(0).getAssignee());
+ verify(commentService).createComment(eq(task1.getId()), eq(processInstanceId),
+ eq(BpmCommentTypeEnum.ADD_SIGN), eq("用户1"), eq("向前加签"), eq("用户2"), eq("加签"));
+
+ // 调用:用户 2 审批子任务
+ bpmTaskService.approveTask(2L, new BpmTaskApproveReqVO().setId(childTasks.get(0).getId()).setReason("同意"));
+ // 断言:父任务恢复给用户 1 审批
+ Task resolvedTask = bpmTaskService.getTask(task1.getId());
+ assertEquals("1", resolvedTask.getAssignee());
+ assertNull(resolvedTask.getScopeType());
+ assertEquals(BpmTaskStatusEnum.RUNNING.getStatus(),
+ resolvedTask.getTaskLocalVariables().get(BpmnVariableConstants.TASK_VARIABLE_STATUS));
+ assertTrue(bpmTaskService.getTaskListByParentTaskId(task1.getId()).isEmpty());
+ }
+
+ @Test
+ public void testCreateSignTask_after_thenApprove() {
+ // 准备参数
+ String processInstanceId = startProcessInstance();
+ Task task1 = getRunningTask(processInstanceId);
+
+ // 调用:向后加签给用户 2
+ bpmTaskService.createSignTask(1L, new BpmTaskSignCreateReqVO().setId(task1.getId())
+ .setType(BpmTaskSignTypeEnum.AFTER.getType()).setUserIds(Set.of(2L)).setReason("加签"));
+ // 断言:子任务暂由用户 2 拥有,等待父任务审批
+ Task childTask = bpmTaskService.getTaskListByParentTaskId(task1.getId()).get(0);
+ assertEquals("2", childTask.getOwner());
+ assertNull(childTask.getAssignee());
+ assertEquals(BpmTaskStatusEnum.WAIT.getStatus(), getTaskStatus(childTask.getId()));
+
+ // 调用:用户 1 审批父任务,激活子任务
+ bpmTaskService.approveTask(1L, new BpmTaskApproveReqVO().setId(task1.getId()).setReason("同意"));
+ // 断言:父任务处于审批通过中,子任务分配给用户 2
+ assertEquals(BpmTaskStatusEnum.APPROVING.getStatus(), getTaskStatus(task1.getId()));
+ assertEquals("2", bpmTaskService.getTask(childTask.getId()).getAssignee());
+ assertEquals(BpmTaskStatusEnum.RUNNING.getStatus(), getTaskStatus(childTask.getId()));
+
+ // 调用:用户 2 审批子任务
+ bpmTaskService.approveTask(2L, new BpmTaskApproveReqVO().setId(childTask.getId()).setReason("同意"));
+ // 断言:父任务自动完成,流程流转到 task2
+ assertTaskStatus(task1.getId(), BpmTaskStatusEnum.APPROVE.getStatus(), "同意");
+ assertEquals("task2", getRunningTask(processInstanceId).getTaskDefinitionKey());
+ }
+
+ @Test
+ public void testCreateSignTask_typeError() {
+ // 准备参数
+ String processInstanceId = startProcessInstance();
+ Task task1 = getRunningTask(processInstanceId);
+ bpmTaskService.createSignTask(1L, new BpmTaskSignCreateReqVO().setId(task1.getId())
+ .setType(BpmTaskSignTypeEnum.AFTER.getType()).setUserIds(Set.of(2L)).setReason("加签"));
+
+ // 调用,并断言异常
+ assertServiceException(() -> bpmTaskService.createSignTask(1L, new BpmTaskSignCreateReqVO().setId(task1.getId())
+ .setType(BpmTaskSignTypeEnum.BEFORE.getType()).setUserIds(Set.of(3L)).setReason("加签")),
+ TASK_SIGN_CREATE_TYPE_ERROR, "向后加签", "向前加签");
+ }
+
+ @Test
+ public void testCreateSignTask_userRepeat() {
+ // 准备参数
+ String processInstanceId = startProcessInstance();
+ Task task1 = getRunningTask(processInstanceId);
+
+ // 调用,并断言异常:用户 1 已经是当前节点的审批人
+ assertServiceException(() -> bpmTaskService.createSignTask(1L, new BpmTaskSignCreateReqVO().setId(task1.getId())
+ .setType(BpmTaskSignTypeEnum.BEFORE.getType()).setUserIds(Set.of(1L)).setReason("加签")),
+ TASK_SIGN_CREATE_USER_REPEAT, "用户1");
+ }
+
+ @Test
+ public void testDeleteSignTask_success() {
+ // 准备参数
+ String processInstanceId = startProcessInstance();
+ Task task1 = getRunningTask(processInstanceId);
+ bpmTaskService.createSignTask(1L, new BpmTaskSignCreateReqVO().setId(task1.getId())
+ .setType(BpmTaskSignTypeEnum.BEFORE.getType()).setUserIds(Set.of(2L)).setReason("加签"));
+ Task childTask = bpmTaskService.getTaskListByParentTaskId(task1.getId()).get(0);
+
+ // 调用
+ bpmTaskService.deleteSignTask(1L, new BpmTaskSignDeleteReqVO().setId(childTask.getId()).setReason("减签"));
+ // 断言:子任务被取消删除,父任务恢复给用户 1 审批
+ assertNull(bpmTaskService.getTask(childTask.getId()));
+ assertEquals(BpmTaskStatusEnum.CANCEL.getStatus(), getTaskStatus(childTask.getId()));
+ Task parentTask = bpmTaskService.getTask(task1.getId());
+ assertEquals("1", parentTask.getAssignee());
+ assertNull(parentTask.getScopeType());
+ verify(commentService).createComment(eq(task1.getId()), eq(processInstanceId),
+ eq(BpmCommentTypeEnum.SUB_SIGN), eq("用户1"), eq("用户2"));
+ }
+
+ @Test
+ public void testDeleteSignTask_partial() {
+ // 准备参数:task1 向前加签给用户 2、用户 3
+ String processInstanceId = startProcessInstance();
+ Task task1 = getRunningTask(processInstanceId);
+ bpmTaskService.createSignTask(1L, new BpmTaskSignCreateReqVO().setId(task1.getId())
+ .setType(BpmTaskSignTypeEnum.BEFORE.getType()).setUserIds(Set.of(2L, 3L)).setReason("加签"));
+ Map childTasks = bpmTaskService.getTaskListByParentTaskId(task1.getId()).stream()
+ .collect(Collectors.toMap(Task::getAssignee, Function.identity()));
+
+ // 调用:减签用户 2
+ bpmTaskService.deleteSignTask(1L, new BpmTaskSignDeleteReqVO().setId(childTasks.get("2").getId())
+ .setReason("减签"));
+ // 断言:用户 3 的加签任务仍在,父任务继续等待
+ assertEquals(BpmTaskStatusEnum.CANCEL.getStatus(), getTaskStatus(childTasks.get("2").getId()));
+ assertEquals(List.of(childTasks.get("3").getId()), bpmTaskService.getTaskListByParentTaskId(task1.getId())
+ .stream().map(Task::getId).toList());
+ Task parentTask = bpmTaskService.getTask(task1.getId());
+ assertNull(parentTask.getAssignee());
+ assertEquals(BpmTaskSignTypeEnum.BEFORE.getType(), parentTask.getScopeType());
+
+ // 调用 + 断言:用户 3 审批通过后,父任务恢复给用户 1 审批
+ bpmTaskService.approveTask(3L, new BpmTaskApproveReqVO().setId(childTasks.get("3").getId()).setReason("同意"));
+ assertEquals("1", bpmTaskService.getTask(task1.getId()).getAssignee());
+ }
+
+ @Test
+ public void testDeleteSignTask_afterSignNotApproved() {
+ // 准备参数:task1 向后加签给用户 2,但 task1 尚未审批
+ String processInstanceId = startProcessInstance();
+ Task task1 = getRunningTask(processInstanceId);
+ bpmTaskService.createSignTask(1L, new BpmTaskSignCreateReqVO().setId(task1.getId())
+ .setType(BpmTaskSignTypeEnum.AFTER.getType()).setUserIds(Set.of(2L)).setReason("加签"));
+ Task childTask = bpmTaskService.getTaskListByParentTaskId(task1.getId()).get(0);
+
+ // 调用
+ bpmTaskService.deleteSignTask(1L, new BpmTaskSignDeleteReqVO().setId(childTask.getId()).setReason("减签"));
+ // 断言:父任务不会因为加签任务被减签而自动完成,仍由用户 1 审批
+ Task parentTask = getRunningTask(processInstanceId);
+ assertEquals(task1.getId(), parentTask.getId());
+ assertEquals("1", parentTask.getAssignee());
+ assertNull(parentTask.getScopeType());
+ assertEquals(BpmTaskStatusEnum.RUNNING.getStatus(), getTaskStatus(task1.getId()));
+ }
+
+ @Test
+ public void testDeleteSignTask_noParent() {
+ // 准备参数
+ String processInstanceId = startProcessInstance();
+ Task task1 = getRunningTask(processInstanceId);
+
+ // 调用,并断言异常
+ assertServiceException(() -> bpmTaskService.deleteSignTask(1L,
+ new BpmTaskSignDeleteReqVO().setId(task1.getId()).setReason("减签")), TASK_SIGN_DELETE_NO_PARENT);
+ }
+
+ @Test
+ public void testWithdrawTask_success() {
+ // 准备参数
+ String processInstanceId = startProcessInstance();
+ Task task1 = getRunningTask(processInstanceId);
+ approveRunningTask(processInstanceId);
+ Task task2 = getRunningTask(processInstanceId);
+
+ // 调用
+ bpmTaskService.withdrawTask(1L, task1.getId());
+ // 断言:task2 被取消,流程回到 task1
+ assertTaskStatus(task2.getId(), BpmTaskStatusEnum.CANCEL.getStatus(), "前一任务撤回,系统自动取消");
+ verify(commentService).createComment(eq(task2.getId()), eq(processInstanceId),
+ eq(BpmCommentTypeEnum.CANCEL), eq("前一节点撤回"));
+ assertEquals("task1", getRunningTask(processInstanceId).getTaskDefinitionKey());
+ }
+
+ @Test
+ public void testWithdrawTask_taskNotExists() {
+ // 准备参数:task1 尚未审批
+ String processInstanceId = startProcessInstance();
+ Task task1 = getRunningTask(processInstanceId);
+
+ // 调用,并断言异常
+ assertServiceException(() -> bpmTaskService.withdrawTask(1L, task1.getId()), TASK_WITHDRAW_FAIL_TASK_NOT_EXISTS);
+ }
+
+ @Test
+ public void testWithdrawTask_nextTaskApproved() {
+ // 准备参数:task1、task2 均已审批,流程已经结束
+ String processInstanceId = startProcessInstance();
+ Task task1 = getRunningTask(processInstanceId);
+ approveRunningTask(processInstanceId);
+ approveRunningTask(processInstanceId);
+
+ // 调用,并断言异常
+ assertServiceException(() -> bpmTaskService.withdrawTask(1L, task1.getId()),
+ TASK_WITHDRAW_FAIL_PROCESS_NOT_RUNNING);
+ }
+
+ @Test
+ public void testWithdrawTask_notAllow() {
+ // 准备参数
+ String processInstanceId = startProcessInstance();
+ Task task1 = getRunningTask(processInstanceId);
+ approveRunningTask(processInstanceId);
+ when(processDefinitionService.getProcessDefinitionInfo(anyString()))
+ .thenReturn(new BpmProcessDefinitionInfoDO().setAllowWithdrawTask(false));
+
+ // 调用,并断言异常
+ assertServiceException(() -> bpmTaskService.withdrawTask(1L, task1.getId()), TASK_WITHDRAW_FAIL_NOT_ALLOW);
+ }
+
+ @Test
+ public void testMoveTaskToEnd_success() {
+ // 准备参数
+ String processInstanceId = startProcessInstance();
+ Task task1 = getRunningTask(processInstanceId);
+
+ // 调用
+ bpmTaskService.moveTaskToEnd(processInstanceId, "结束流程");
+ // 断言:运行中的任务被取消,流程实例结束
+ assertTaskStatus(task1.getId(), BpmTaskStatusEnum.CANCEL.getStatus(), "系统自动取消");
+ assertNull(processInstanceService.getProcessInstance(processInstanceId));
+ }
+
+ @Test
+ public void testMoveTaskToEnd_withSignTask() {
+ // 准备参数:task1 向前加签给用户 2,子任务未审批
+ String processInstanceId = startProcessInstance();
+ Task task1 = getRunningTask(processInstanceId);
+ bpmTaskService.createSignTask(1L, new BpmTaskSignCreateReqVO().setId(task1.getId())
+ .setType(BpmTaskSignTypeEnum.BEFORE.getType()).setUserIds(Set.of(2L)).setReason("加签"));
+ Task childTask = bpmTaskService.getTaskListByParentTaskId(task1.getId()).get(0);
+
+ // 调用
+ bpmTaskService.moveTaskToEnd(processInstanceId, "结束流程");
+ // 断言:父任务、子任务都被取消,流程实例结束
+ assertTaskStatus(task1.getId(), BpmTaskStatusEnum.CANCEL.getStatus(), "系统自动取消");
+ assertTaskStatus(childTask.getId(), BpmTaskStatusEnum.CANCEL.getStatus(), "系统自动取消");
+ assertNull(processInstanceService.getProcessInstance(processInstanceId));
+ assertNull(bpmTaskService.getTask(childTask.getId()));
+ }
+
+ @Test
+ public void testProcessTaskTimeout_approve() {
+ // 准备参数
+ String processInstanceId = startProcessInstance();
+ Task task1 = getRunningTask(processInstanceId);
+
+ // 调用:task1 超时自动通过
+ bpmTaskService.processTaskTimeout(processInstanceId, "task1",
+ BpmUserTaskTimeoutHandlerTypeEnum.APPROVE.getType());
+ // 断言
+ assertTaskStatus(task1.getId(), BpmTaskStatusEnum.APPROVE.getStatus(), BpmReasonEnum.TIMEOUT_APPROVE.getReason());
+ assertEquals("task2", getRunningTask(processInstanceId).getTaskDefinitionKey());
+ }
+
+ @Test
+ public void testProcessTaskTimeout_reject() {
+ // 准备参数
+ String processInstanceId = startProcessInstance();
+ Task task1 = getRunningTask(processInstanceId);
+
+ // 调用:task1 超时自动拒绝
+ bpmTaskService.processTaskTimeout(processInstanceId, "task1",
+ BpmUserTaskTimeoutHandlerTypeEnum.REJECT.getType());
+ // 断言:任务不通过,流程实例不通过并结束
+ assertEquals(BpmTaskStatusEnum.REJECT.getStatus(), getTaskStatus(task1.getId()));
+ assertNull(processInstanceService.getProcessInstance(processInstanceId));
+ assertEquals(BpmProcessInstanceStatusEnum.REJECT.getStatus(), getHistoricProcessVariable(processInstanceId,
+ BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS));
+ }
+
+ // ========== 流程实例相关 ==========
+
+ @Test
+ public void testCreateProcessInstance_withTitle() {
+ // 准备参数:开启标题设置
+ BpmModelMetaInfoVO.TitleSetting titleSetting = new BpmModelMetaInfoVO.TitleSetting().setEnable(true)
+ .setTitle("{" + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_ID + "}的"
+ + "{" + BpmnVariableConstants.PROCESS_DEFINITION_NAME + "},请假{day}天");
+ doAnswer(invocation -> new BpmProcessDefinitionInfoDO().setProcessDefinitionId(invocation.getArgument(0))
+ .setModelType(BpmModelTypeEnum.BPMN.getType()).setTitleSetting(titleSetting))
+ .when(processDefinitionService).getProcessDefinitionInfo(anyString());
+ String processDefinitionId = repositoryService.createProcessDefinitionQuery()
+ .deploymentId(deployment.getId()).singleResult().getId();
+
+ // 调用:模拟 FlowableWebFilter 设置发起人
+ String processInstanceId = FlowableUtils.executeAuthenticatedUserId(1L,
+ () -> processInstanceService.createProcessInstance(1L, new BpmProcessInstanceCreateReqVO()
+ .setProcessDefinitionId(processDefinitionId).setVariables(new HashMap<>(Map.of("day", 3)))));
+ // 断言
+ ProcessInstance processInstance = processInstanceService.getProcessInstance(processInstanceId);
+ assertEquals("用户1的两级审批,请假3天", processInstance.getName());
+ assertEquals(3, runtimeService.getVariable(processInstanceId, "day"));
+ assertEquals(1L, runtimeService.getVariable(processInstanceId,
+ BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_ID));
+ assertEquals(BpmProcessInstanceStatusEnum.RUNNING.getStatus(), runtimeService.getVariable(processInstanceId,
+ BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS));
+ assertEquals("task1", getRunningTask(processInstanceId).getTaskDefinitionKey());
+ }
+
+ @Test
+ public void testCancelProcessInstanceByStartUser_success() {
+ // 准备参数
+ String processInstanceId = startProcessInstance();
+ Task task1 = getRunningTask(processInstanceId);
+
+ // 调用
+ processInstanceService.cancelProcessInstanceByStartUser(1L, new BpmProcessInstanceCancelReqVO()
+ .setId(processInstanceId).setReason("不想请了"));
+ // 断言:流程实例取消并结束,运行中的任务被取消
+ assertNull(processInstanceService.getProcessInstance(processInstanceId));
+ assertEquals(BpmProcessInstanceStatusEnum.CANCEL.getStatus(), getHistoricProcessVariable(processInstanceId,
+ BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS));
+ assertEquals(BpmReasonEnum.CANCEL_PROCESS_INSTANCE_BY_START_USER.format("不想请了"),
+ getHistoricProcessVariable(processInstanceId, BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_REASON));
+ assertTaskStatus(task1.getId(), BpmTaskStatusEnum.CANCEL.getStatus(), BpmReasonEnum.CANCEL_BY_SYSTEM.getReason());
+ }
+
+ @Test
+ public void testCancelProcessInstanceByStartUser_notSelf() {
+ // 准备参数
+ String processInstanceId = startProcessInstance();
+
+ // 调用,并断言异常
+ assertServiceException(() -> processInstanceService.cancelProcessInstanceByStartUser(2L,
+ new BpmProcessInstanceCancelReqVO().setId(processInstanceId).setReason("取消")),
+ PROCESS_INSTANCE_CANCEL_FAIL_NOT_SELF);
+ assertNotNull(processInstanceService.getProcessInstance(processInstanceId));
+ }
+
+ @Test
+ public void testCancelProcessInstanceByAdmin_success() {
+ // 准备参数:task1 已审批,流程处于 task2
+ String processInstanceId = startProcessInstance();
+ approveRunningTask(processInstanceId);
+
+ // 调用
+ processInstanceService.cancelProcessInstanceByAdmin(100L, new BpmProcessInstanceCancelReqVO()
+ .setId(processInstanceId).setReason("流程配置错误"));
+ // 断言
+ assertNull(processInstanceService.getProcessInstance(processInstanceId));
+ assertEquals(BpmProcessInstanceStatusEnum.CANCEL.getStatus(), getHistoricProcessVariable(processInstanceId,
+ BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS));
+ assertEquals(BpmReasonEnum.CANCEL_PROCESS_INSTANCE_BY_ADMIN.format("用户100", "流程配置错误"),
+ getHistoricProcessVariable(processInstanceId, BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_REASON));
+ }
+
+ @Test
+ public void testUpdateAndRemoveProcessInstanceVariables_success() {
+ // 准备参数
+ String processInstanceId = startProcessInstance();
+
+ // 调用 + 断言:更新变量
+ processInstanceService.updateProcessInstanceVariables(processInstanceId, Map.of("day", 5));
+ assertEquals(5, runtimeService.getVariable(processInstanceId, "day"));
+ // 调用 + 断言:删除变量
+ processInstanceService.removeProcessInstanceVariables(processInstanceId, Set.of("day"));
+ assertFalse(runtimeService.hasVariable(processInstanceId, "day"));
+ }
+
+ @Test
+ public void testGetTodoTask_success() {
+ // 准备参数
+ String processInstanceId = startProcessInstance();
+ Task task1 = getRunningTask(processInstanceId);
+
+ // 调用:不传 taskId 时,按流程实例查询首个待办
+ BpmTaskRespVO todoTask = bpmTaskService.getTodoTask(1L, null, processInstanceId);
+ // 断言
+ assertEquals(task1.getId(), todoTask.getId());
+ assertEquals("一级审批", todoTask.getName());
+ assertFalse(todoTask.getSignEnable());
+ assertFalse(todoTask.getReasonRequire());
+ // 断言:其他用户没有待办
+ assertNull(bpmTaskService.getTodoTask(2L, task1.getId(), processInstanceId));
+ }
+
+ @Test
+ public void testGetTaskTodoPageAndDonePage_success() {
+ // 准备参数
+ String processInstanceId = startProcessInstance();
+ approveRunningTask(processInstanceId);
+ BpmTaskPageReqVO pageReqVO = new BpmTaskPageReqVO();
+ pageReqVO.setName("审批");
+ pageReqVO.setProcessDefinitionKey("twoStepApprove");
+
+ // 调用 + 断言:待办为 task2
+ List todoTasks = bpmTaskService.getTaskTodoPage(1L, pageReqVO).getList();
+ assertEquals(List.of("task2"), todoTasks.stream().map(Task::getTaskDefinitionKey).toList());
+ // 调用 + 断言:已办为 task1
+ pageReqVO.setStatus(BpmTaskStatusEnum.APPROVE.getStatus());
+ List doneTasks = bpmTaskService.getTaskDonePage(1L, pageReqVO).getList();
+ assertEquals(List.of("task1"), doneTasks.stream().map(HistoricTaskInstance::getTaskDefinitionKey).toList());
+ // 调用 + 断言:其它用户没有待办
+ assertEquals(0, bpmTaskService.getTaskTodoPage(2L, new BpmTaskPageReqVO()).getTotal());
+ }
+
+ // ========== 状态回调相关(依赖 BpmTaskEventListener、BpmProcessInstanceEventListener) ==========
+
+ @Test
+ public void testProcessCallback_approve() {
+ // 调用:发起流程
+ String processInstanceId = startProcessInstance();
+ // 断言:任务创建回调将 task1 置为审批中,任务分配回调发送待办消息
+ Task task1 = getRunningTask(processInstanceId);
+ assertEquals(BpmTaskStatusEnum.RUNNING.getStatus(), getTaskStatus(task1.getId()));
+ verify(messageService).sendMessageWhenTaskAssigned(argThat(reqDTO ->
+ task1.getId().equals(reqDTO.getTaskId()) && reqDTO.getAssigneeUserId().equals(1L)
+ && reqDTO.getStartUserId().equals(1L) && "用户1".equals(reqDTO.getStartUserNickname())));
+
+ // 调用:task1、task2 依次审批通过
+ approveRunningTask(processInstanceId);
+ approveRunningTask(processInstanceId);
+ // 断言:流程完成回调将状态更新为审批通过,并发送消息、状态事件
+ assertEquals(BpmProcessInstanceStatusEnum.APPROVE.getStatus(), getHistoricProcessVariable(processInstanceId,
+ BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS));
+ verify(messageService, times(2)).sendMessageWhenTaskAssigned(any());
+ verify(messageService).sendMessageWhenProcessInstanceApprove(argThat(reqDTO ->
+ processInstanceId.equals(reqDTO.getProcessInstanceId()) && reqDTO.getStartUserId().equals(1L)));
+ BpmProcessInstanceStatusEvent event = getProcessInstanceStatusEvent(processInstanceId);
+ assertEquals(BpmProcessInstanceStatusEnum.APPROVE.getStatus(), event.getStatus());
+ assertEquals("twoStepApprove", event.getProcessDefinitionKey());
+ assertEquals("business-1", event.getBusinessKey());
+ }
+
+ @Test
+ public void testProcessCallback_reject() {
+ // 准备参数
+ String processInstanceId = startProcessInstance();
+ Task task1 = getRunningTask(processInstanceId);
+
+ // 调用
+ bpmTaskService.rejectTask(1L, new BpmTaskRejectReqVO().setId(task1.getId()).setReason("不同意"));
+ // 断言:流程完成回调发送不通过消息、状态事件,且不会误发通过消息
+ verify(messageService).sendMessageWhenProcessInstanceReject(argThat(reqDTO ->
+ processInstanceId.equals(reqDTO.getProcessInstanceId()) && "审批不通过任务,原因:不同意".equals(reqDTO.getReason())));
+ verify(messageService, never()).sendMessageWhenProcessInstanceApprove(any());
+ BpmProcessInstanceStatusEvent event = getProcessInstanceStatusEvent(processInstanceId);
+ assertEquals(BpmProcessInstanceStatusEnum.REJECT.getStatus(), event.getStatus());
+ assertEquals("审批不通过任务,原因:不同意", event.getReason());
+ }
+
+ @Test
+ public void testProcessCallback_cancel() {
+ // 准备参数
+ String processInstanceId = startProcessInstance();
+
+ // 调用
+ processInstanceService.cancelProcessInstanceByStartUser(1L, new BpmProcessInstanceCancelReqVO()
+ .setId(processInstanceId).setReason("不想请了"));
+ // 断言:取消时只发送状态事件,不发送通过、不通过消息
+ BpmProcessInstanceStatusEvent event = getProcessInstanceStatusEvent(processInstanceId);
+ assertEquals(BpmProcessInstanceStatusEnum.CANCEL.getStatus(), event.getStatus());
+ assertEquals(BpmReasonEnum.CANCEL_PROCESS_INSTANCE_BY_START_USER.format("不想请了"), event.getReason());
+ verify(messageService, never()).sendMessageWhenProcessInstanceApprove(any());
+ verify(messageService, never()).sendMessageWhenProcessInstanceReject(any());
+ }
+
+ // ========== 自动审批相关(依赖 getSelf() 获取 Spring 代理) ==========
+
+ @Test
+ public void testProcessTaskCreated_autoApprove() {
+ // 准备参数:task1 配置为【自动通过】
+ mockUserTaskExtensionElement("task1", BpmnModelConstants.USER_TASK_APPROVE_TYPE,
+ BpmUserTaskApproveTypeEnum.AUTO_APPROVE.getType());
+ mockTaskWithoutCandidate("task1");
+
+ // 调用
+ String processInstanceId = startProcessInstance();
+ // 断言:task1 在事务结束后自动通过,流程流转到 task2
+ HistoricTaskInstance task1 = getHistoricTask(processInstanceId, "task1");
+ assertTaskStatus(task1.getId(), BpmTaskStatusEnum.APPROVE.getStatus(),
+ BpmReasonEnum.APPROVE_TYPE_AUTO_APPROVE.getReason());
+ assertEquals("task2", getRunningTask(processInstanceId).getTaskDefinitionKey());
+ }
+
+ @Test
+ public void testProcessTaskCreated_autoReject() {
+ // 准备参数:task1 配置为【自动拒绝】
+ mockUserTaskExtensionElement("task1", BpmnModelConstants.USER_TASK_APPROVE_TYPE,
+ BpmUserTaskApproveTypeEnum.AUTO_REJECT.getType());
+ mockTaskWithoutCandidate("task1");
+
+ // 调用
+ String processInstanceId = startProcessInstance();
+ // 断言:task1 自动拒绝,流程实例不通过并结束
+ HistoricTaskInstance task1 = getHistoricTask(processInstanceId, "task1");
+ assertTaskStatus(task1.getId(), BpmTaskStatusEnum.REJECT.getStatus(),
+ BpmReasonEnum.APPROVE_TYPE_AUTO_REJECT.getReason());
+ assertNull(processInstanceService.getProcessInstance(processInstanceId));
+ assertEquals(BpmProcessInstanceStatusEnum.REJECT.getStatus(), getHistoricProcessVariable(processInstanceId,
+ BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS));
+ }
+
+ @Test
+ public void testProcessTaskAssigned_autoApproveAll() {
+ // 准备参数:流程配置【仅审批一次,后续重复的审批节点均自动通过】
+ mockAutoApprovalType(BpmAutoApproveTypeEnum.APPROVE_ALL);
+ String processInstanceId = startProcessInstance();
+
+ // 调用:用户 1 审批 task1
+ approveRunningTask(processInstanceId);
+ // 断言:task2 仍由用户 1 审批,自动通过,流程结束
+ HistoricTaskInstance task2 = getHistoricTask(processInstanceId, "task2");
+ assertTaskStatus(task2.getId(), BpmTaskStatusEnum.APPROVE.getStatus(), BpmAutoApproveTypeEnum.APPROVE_ALL.getName());
+ assertNull(processInstanceService.getProcessInstance(processInstanceId));
+ assertEquals(BpmProcessInstanceStatusEnum.APPROVE.getStatus(), getHistoricProcessVariable(processInstanceId,
+ BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS));
+ }
+
+ @Test
+ public void testProcessTaskAssigned_autoApproveSequent() {
+ // 准备参数:流程配置【仅针对连续审批的节点自动通过】
+ mockAutoApprovalType(BpmAutoApproveTypeEnum.APPROVE_SEQUENT);
+ String processInstanceId = startProcessInstance();
+
+ // 调用:用户 1 审批 task1
+ approveRunningTask(processInstanceId);
+ // 断言:task2 的上一节点 task1 也由用户 1 审批,自动通过
+ HistoricTaskInstance task2 = getHistoricTask(processInstanceId, "task2");
+ assertTaskStatus(task2.getId(), BpmTaskStatusEnum.APPROVE.getStatus(),
+ BpmAutoApproveTypeEnum.APPROVE_SEQUENT.getName());
+ assertNull(processInstanceService.getProcessInstance(processInstanceId));
+ }
+
+ // ========== 定时器超时相关(依赖 BpmTaskEventListener#timerFired) ==========
+
+ @Test
+ public void testTimerFired_userTaskTimeoutApprove() {
+ // 准备参数
+ deploy("bpmn/timeout-approve.bpmn20.xml");
+ String processInstanceId = startProcessInstance("timeoutApprove");
+ Task task1 = getRunningTask(processInstanceId);
+
+ // 调用:触发 task1 的超时定时器
+ executeTimerJob(processInstanceId);
+ // 断言:task1 超时自动通过,流程结束
+ assertTaskStatus(task1.getId(), BpmTaskStatusEnum.APPROVE.getStatus(), BpmReasonEnum.TIMEOUT_APPROVE.getReason());
+ assertNull(processInstanceService.getProcessInstance(processInstanceId));
+ assertEquals(BpmProcessInstanceStatusEnum.APPROVE.getStatus(), getHistoricProcessVariable(processInstanceId,
+ BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS));
+ }
+
+ @Test
+ public void testTimerFired_userTaskTimeoutReminder() {
+ // 准备参数:超时处理方式改为【自动提醒】
+ deploy("bpmn/timeout-approve.bpmn20.xml");
+ mockBoundaryEventTimeoutHandlerType("task1-timeout", BpmUserTaskTimeoutHandlerTypeEnum.REMINDER);
+ String processInstanceId = startProcessInstance("timeoutApprove");
+ Task task1 = getRunningTask(processInstanceId);
+
+ // 调用
+ executeTimerJob(processInstanceId);
+ // 断言:发送超时提醒,任务保持审批中
+ verify(messageService).sendMessageWhenTaskTimeout(argThat(reqDTO ->
+ task1.getId().equals(reqDTO.getTaskId()) && reqDTO.getAssigneeUserId().equals(1L)
+ && processInstanceId.equals(reqDTO.getProcessInstanceId())));
+ assertEquals(task1.getId(), getRunningTask(processInstanceId).getId());
+ assertEquals(BpmTaskStatusEnum.RUNNING.getStatus(), getTaskStatus(task1.getId()));
+ }
+
+ @Test
+ public void testTimerFired_userTaskTimeoutReject() {
+ // 准备参数:超时处理方式改为【自动拒绝】
+ deploy("bpmn/timeout-approve.bpmn20.xml");
+ mockBoundaryEventTimeoutHandlerType("task1-timeout", BpmUserTaskTimeoutHandlerTypeEnum.REJECT);
+ String processInstanceId = startProcessInstance("timeoutApprove");
+ Task task1 = getRunningTask(processInstanceId);
+
+ // 调用
+ executeTimerJob(processInstanceId);
+ // 断言:task1 超时自动拒绝,流程实例不通过并结束
+ assertTaskStatus(task1.getId(), BpmTaskStatusEnum.REJECT.getStatus(), BpmReasonEnum.REJECT_TASK.getReason());
+ assertNull(processInstanceService.getProcessInstance(processInstanceId));
+ assertEquals(BpmProcessInstanceStatusEnum.REJECT.getStatus(), getHistoricProcessVariable(processInstanceId,
+ BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS));
+ }
+
+ @Test
+ public void testTimerFired_delayTimer() {
+ // 准备参数
+ deploy("bpmn/delay-timer.bpmn20.xml");
+ String processInstanceId = startProcessInstance("delayTimer");
+ assertNotNull(runtimeService.createExecutionQuery().processInstanceId(processInstanceId)
+ .activityId("wait").singleResult());
+
+ // 调用:延迟器到期
+ executeTimerJob(processInstanceId);
+ // 断言:receiveTask 被触发,流程结束
+ assertNull(processInstanceService.getProcessInstance(processInstanceId));
+ assertEquals(BpmProcessInstanceStatusEnum.APPROVE.getStatus(), getHistoricProcessVariable(processInstanceId,
+ BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS));
+ }
+
+ // ========== 子流程相关 ==========
+
+ @Test
+ public void testTimerFired_childProcessTimeout() {
+ // 准备参数:主流程发起 twoStepApprove 子流程
+ deploy("bpmn/call-activity.bpmn20.xml");
+ String parentId = startProcessInstance("callActivityParent");
+ ProcessInstance child = getChildProcessInstance(parentId);
+ Task childTask = getRunningTask(child.getId());
+
+ // 调用:触发子流程的超时定时器
+ executeTimerJob(parentId);
+ // 断言:子流程被结束、任务被取消,主流程继续流转并结束
+ assertTaskStatus(childTask.getId(), BpmTaskStatusEnum.CANCEL.getStatus(), BpmReasonEnum.CANCEL_BY_SYSTEM.getReason());
+ assertNull(processInstanceService.getProcessInstance(child.getId()));
+ assertNull(processInstanceService.getProcessInstance(parentId));
+ assertEquals(BpmProcessInstanceStatusEnum.APPROVE.getStatus(), getHistoricProcessVariable(parentId,
+ BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS));
+ }
+
+ @Test
+ public void testRejectTask_childProcessRejectParent() {
+ // 准备参数
+ deploy("bpmn/call-activity.bpmn20.xml");
+ String parentId = startProcessInstance("callActivityParent");
+ ProcessInstance child = getChildProcessInstance(parentId);
+ Task childTask = getRunningTask(child.getId());
+
+ // 调用:子流程审批不通过
+ bpmTaskService.rejectTask(1L, new BpmTaskRejectReqVO().setId(childTask.getId()).setReason("不同意"));
+ // 断言:子流程不通过时,主流程同样标记为不通过并结束
+ assertNull(processInstanceService.getProcessInstance(child.getId()));
+ assertNull(processInstanceService.getProcessInstance(parentId));
+ assertEquals(BpmProcessInstanceStatusEnum.REJECT.getStatus(), getHistoricProcessVariable(parentId,
+ BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS));
+ assertEquals(BpmReasonEnum.REJECT_TASK.format(BpmReasonEnum.REJECT_CHILD_PROCESS.getReason()),
+ getHistoricProcessVariable(parentId, BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_REASON));
+ assertEquals(BpmProcessInstanceStatusEnum.REJECT.getStatus(), getProcessInstanceStatusEvent(parentId).getStatus());
+ }
+
+ @Test
+ public void testCancelProcessInstanceByStartUser_cancelChildProcess() {
+ // 准备参数
+ deploy("bpmn/call-activity.bpmn20.xml");
+ String parentId = startProcessInstance("callActivityParent");
+ ProcessInstance child = getChildProcessInstance(parentId);
+ Task childTask = getRunningTask(child.getId());
+
+ // 调用:取消主流程
+ processInstanceService.cancelProcessInstanceByStartUser(1L, new BpmProcessInstanceCancelReqVO()
+ .setId(parentId).setReason("不想请了"));
+ // 断言:主流程、子流程都被取消
+ assertNull(processInstanceService.getProcessInstance(parentId));
+ assertNull(processInstanceService.getProcessInstance(child.getId()));
+ assertEquals(BpmProcessInstanceStatusEnum.CANCEL.getStatus(), getHistoricProcessVariable(parentId,
+ BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS));
+ assertEquals(BpmProcessInstanceStatusEnum.CANCEL.getStatus(), getHistoricProcessVariable(child.getId(),
+ BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS));
+ assertEquals(BpmReasonEnum.CANCEL_CHILD_PROCESS_INSTANCE_BY_MAIN_PROCESS.getReason(),
+ getHistoricProcessVariable(child.getId(), BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_REASON));
+ assertTaskStatus(childTask.getId(), BpmTaskStatusEnum.CANCEL.getStatus(), BpmReasonEnum.CANCEL_BY_SYSTEM.getReason());
+ }
+
+ @Test
+ public void testCancelProcessInstanceByStartUser_childNotAllow() {
+ // 准备参数
+ deploy("bpmn/call-activity.bpmn20.xml");
+ String parentId = startProcessInstance("callActivityParent");
+ ProcessInstance child = getChildProcessInstance(parentId);
+
+ // 调用,并断言异常:子流程不允许单独取消
+ assertServiceException(() -> processInstanceService.cancelProcessInstanceByStartUser(1L,
+ new BpmProcessInstanceCancelReqVO().setId(child.getId()).setReason("取消")),
+ PROCESS_INSTANCE_CANCEL_CHILD_FAIL_NOT_ALLOW);
+ }
+
+ // ========== 私有方法 ==========
+
+ private String startProcessInstance() {
+ return startProcessInstance("twoStepApprove");
+ }
+
+ private String startProcessInstance(String processDefinitionKey) {
+ return processInstanceService.createProcessInstance(1L, new BpmProcessInstanceCreateReqDTO()
+ .setProcessDefinitionKey(processDefinitionKey).setBusinessKey("business-1"));
+ }
+
+ private void deploy(String resource) {
+ extraDeploymentIds.add(repositoryService.createDeployment().addClasspathResource(resource).deploy().getId());
+ }
+
+ private ProcessInstance getChildProcessInstance(String parentId) {
+ ProcessInstance child = runtimeService.createProcessInstanceQuery().superProcessInstanceId(parentId).singleResult();
+ assertNotNull(child);
+ return child;
+ }
+
+ private HistoricTaskInstance getHistoricTask(String processInstanceId, String taskDefinitionKey) {
+ return historyService.createHistoricTaskInstanceQuery().processInstanceId(processInstanceId)
+ .taskDefinitionKey(taskDefinitionKey).singleResult();
+ }
+
+ /**
+ * 将流程实例上唯一的定时器转为可执行任务并立即执行,模拟定时器到期
+ */
+ private void executeTimerJob(String processInstanceId) {
+ Job timerJob = managementService.createTimerJobQuery().processInstanceId(processInstanceId).singleResult();
+ assertNotNull(timerJob);
+ Job job = managementService.moveTimerToExecutableJob(timerJob.getId());
+ managementService.executeJob(job.getId());
+ }
+
+ private BpmProcessInstanceStatusEvent getProcessInstanceStatusEvent(String processInstanceId) {
+ List events = applicationEvents.stream(BpmProcessInstanceStatusEvent.class)
+ .filter(event -> processInstanceId.equals(event.getId())).toList();
+ assertEquals(1, events.size());
+ return events.get(0);
+ }
+
+ private void mockUserTaskExtensionElement(String taskDefinitionKey, String name, Object value) {
+ doAnswer(invocation -> {
+ BpmnModel bpmnModel = repositoryService.getBpmnModel(invocation.getArgument(0));
+ FlowElement element = bpmnModel.getFlowElement(taskDefinitionKey);
+ if (element != null && !element.getExtensionElements().containsKey(name)) {
+ BpmnModelUtils.addExtensionElement(element, name, String.valueOf(value));
+ }
+ return bpmnModel;
+ }).when(modelService).getBpmnModelByDefinitionId(anyString());
+ }
+
+ private void mockBoundaryEventTimeoutHandlerType(String boundaryEventId, BpmUserTaskTimeoutHandlerTypeEnum handlerType) {
+ doAnswer(invocation -> {
+ BpmnModel bpmnModel = repositoryService.getBpmnModel(invocation.getArgument(0));
+ FlowElement element = bpmnModel.getFlowElement(boundaryEventId);
+ if (element != null) {
+ element.getExtensionElements().get(BpmnModelConstants.USER_TASK_TIMEOUT_HANDLER_TYPE).get(0)
+ .setElementText(String.valueOf(handlerType.getType()));
+ }
+ return bpmnModel;
+ }).when(modelService).getBpmnModelByDefinitionId(anyString());
+ }
+
+ /**
+ * 与 {@link cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker#calculateUsersByTask} 一致:非人工审核(自动通过、不通过)的节点不计算审批人
+ */
+ private void mockTaskWithoutCandidate(String taskDefinitionKey) {
+ when(taskCandidateInvoker.calculateUsersByTask(any())).thenAnswer(invocation ->
+ taskDefinitionKey.equals(((DelegateExecution) invocation.getArgument(0)).getCurrentActivityId())
+ ? Set.of() : Set.of(1L));
+ }
+
+ private void mockAutoApprovalType(BpmAutoApproveTypeEnum autoApproveType) {
+ doAnswer(invocation -> new BpmProcessDefinitionInfoDO().setProcessDefinitionId(invocation.getArgument(0))
+ .setModelType(BpmModelTypeEnum.BPMN.getType()).setAutoApprovalType(autoApproveType.getType()))
+ .when(processDefinitionService).getProcessDefinitionInfo(anyString());
+ }
+
+ private Task getRunningTask(String processInstanceId) {
+ List tasks = bpmTaskService.getRunningTaskListByProcessInstanceId(processInstanceId, null, null);
+ assertEquals(1, tasks.size());
+ return tasks.get(0);
+ }
+
+ private void approveRunningTask(String processInstanceId) {
+ Task task = getRunningTask(processInstanceId);
+ bpmTaskService.approveTask(1L, new BpmTaskApproveReqVO().setId(task.getId()).setReason("同意"));
+ }
+
+ private Integer getTaskStatus(String taskId) {
+ return (Integer) bpmTaskService.getHistoricTask(taskId).getTaskLocalVariables()
+ .get(BpmnVariableConstants.TASK_VARIABLE_STATUS);
+ }
+
+ private void assertTaskStatus(String taskId, Integer status, String reason) {
+ HistoricTaskInstance task = bpmTaskService.getHistoricTask(taskId);
+ assertEquals(status, task.getTaskLocalVariables().get(BpmnVariableConstants.TASK_VARIABLE_STATUS));
+ assertEquals(reason, task.getTaskLocalVariables().get(BpmnVariableConstants.TASK_VARIABLE_REASON));
+ }
+
+ private Object getHistoricProcessVariable(String processInstanceId, String name) {
+ return processInstanceService.getHistoricProcessInstance(processInstanceId).getProcessVariables().get(name);
+ }
+
+}
diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BpmUserTaskActivityBehaviorFlowableTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BpmUserTaskActivityBehaviorFlowableTest.java
new file mode 100644
index 0000000..8ce530d
--- /dev/null
+++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BpmUserTaskActivityBehaviorFlowableTest.java
@@ -0,0 +1,85 @@
+package cn.iocoder.yudao.module.bpm.framework.flowable;
+
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior.BpmUserTaskActivityBehavior;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
+import jakarta.annotation.Resource;
+import org.flowable.engine.RepositoryService;
+import org.flowable.engine.RuntimeService;
+import org.flowable.engine.TaskService;
+import org.flowable.engine.runtime.ProcessInstance;
+import org.flowable.task.api.Task;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Map;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+/**
+ * {@link BpmUserTaskActivityBehavior} 的 Flowable 集成测试
+ *
+ * @author HUIHUI
+ */
+public class BpmUserTaskActivityBehaviorFlowableTest extends BaseFlowableUnitTest {
+
+ @Resource
+ private RepositoryService repositoryService;
+ @Resource
+ private RuntimeService runtimeService;
+ @Resource
+ private TaskService taskService;
+
+ private String deploymentId;
+
+ @BeforeEach
+ public void setUp() {
+ // 简单审批流程:BPMN 中的 assignee 表达式为 ${startUserId}
+ deploymentId = repositoryService.createDeployment()
+ .addClasspathResource("bpmn/simple-approve.bpmn20.xml")
+ .deploy().getId();
+ }
+
+ @AfterEach
+ public void tearDown() {
+ repositoryService.deleteDeployment(deploymentId, true);
+ }
+
+ @Test
+ public void testHandleAssignments_success() {
+ // 准备参数:发起人为用户 1,审批人计算结果为用户 2
+ when(taskCandidateInvoker.calculateUsersByTask(any())).thenReturn(Set.of(2L));
+
+ // 调用
+ String processInstanceId = startProcessInstance();
+ // 断言:审批人以 BpmTaskCandidateInvoker 的计算结果为准,而不是 BPMN 中的 assignee 表达式
+ Task task = taskService.createTaskQuery().processInstanceId(processInstanceId).singleResult();
+ assertEquals("审批", task.getName());
+ assertEquals("2", task.getAssignee());
+ }
+
+ @Test
+ public void testHandleAssignments_candidateEmpty() {
+ // 准备参数:计算不到审批人
+ when(taskCandidateInvoker.calculateUsersByTask(any())).thenReturn(Set.of());
+
+ // 调用
+ String processInstanceId = startProcessInstance();
+ // 断言:任务仍然创建,但没有审批人(后续由审批人为空的策略处理)
+ Task task = taskService.createTaskQuery().processInstanceId(processInstanceId).singleResult();
+ assertNotNull(task);
+ assertNull(task.getAssignee());
+ }
+
+ private String startProcessInstance() {
+ // 模拟 FlowableWebFilter 设置发起人,与生产发起流程的上下文一致
+ ProcessInstance processInstance = FlowableUtils.executeAuthenticatedUserId(1L,
+ () -> runtimeService.startProcessInstanceByKey("simpleApprove", "business-1",
+ Map.of("startUserId", 1L)));
+ return processInstance.getId();
+ }
+
+}
diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvokerTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvokerTest.java
index a726377..0b80116 100644
--- a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvokerTest.java
+++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvokerTest.java
@@ -49,12 +49,12 @@ public class BpmTaskCandidateInvokerTest extends BaseMockitoUnitTest {
@Mock
private AdminUserApi adminUserApi;
-
@Mock
private BpmProcessInstanceService processInstanceService;
@Spy
private BpmTaskCandidateStrategy userStrategy;
+
@Mock
private BpmTaskCandidateAssignEmptyStrategy emptyStrategy;
diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/expression/BpmTaskAssignLeaderExpressionTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/expression/BpmTaskAssignLeaderExpressionTest.java
index f7eac7c..3546481 100644
--- a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/expression/BpmTaskAssignLeaderExpressionTest.java
+++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/expression/BpmTaskAssignLeaderExpressionTest.java
@@ -30,7 +30,6 @@ public class BpmTaskAssignLeaderExpressionTest extends BaseMockitoUnitTest {
private AdminUserApi adminUserApi;
@Mock
private DeptApi deptApi;
-
@Mock
private BpmProcessInstanceService processInstanceService;
diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderMultiStrategyTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderMultiStrategyTest.java
index 68b1e86..b2b6f6e 100644
--- a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderMultiStrategyTest.java
+++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderMultiStrategyTest.java
@@ -33,7 +33,6 @@ public class BpmTaskCandidateStartUserDeptLeaderMultiStrategyTest extends BaseMo
@Mock
private BpmProcessInstanceService processInstanceService;
-
@Mock
private AdminUserApi adminUserApi;
@Mock
diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderStrategyTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderStrategyTest.java
index 48db3c7..61386e7 100644
--- a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderStrategyTest.java
+++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderStrategyTest.java
@@ -33,7 +33,6 @@ public class BpmTaskCandidateStartUserDeptLeaderStrategyTest extends BaseMockito
@Mock
private BpmProcessInstanceService processInstanceService;
-
@Mock
private AdminUserApi adminUserApi;
@Mock
diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateExpressionStrategyTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateExpressionStrategyTest.java
index 1da8a2f..f4ab7c4 100644
--- a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateExpressionStrategyTest.java
+++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateExpressionStrategyTest.java
@@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.o
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
import org.flowable.engine.delegate.DelegateExecution;
-import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.MockedStatic;
@@ -17,7 +16,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
-@Disabled // TODO 芋艿:临时注释
public class BpmTaskCandidateExpressionStrategyTest extends BaseMockitoUnitTest {
@InjectMocks
diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateGroupStrategyTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateGroupStrategyTest.java
index 44327ec..6eae76f 100644
--- a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateGroupStrategyTest.java
+++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateGroupStrategyTest.java
@@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.u
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmUserGroupDO;
import cn.iocoder.yudao.module.bpm.service.definition.BpmUserGroupService;
-import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
@@ -17,7 +16,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
-@Disabled // TODO 芋艿:临时注释
public class BpmTaskCandidateGroupStrategyTest extends BaseMockitoUnitTest {
@InjectMocks
diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidatePostStrategyTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidatePostStrategyTest.java
index a444224..083eda6 100644
--- a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidatePostStrategyTest.java
+++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidatePostStrategyTest.java
@@ -4,7 +4,6 @@ import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.system.api.dept.PostApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
-import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
@@ -18,7 +17,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
-@Disabled // TODO 芋艿:临时注释
public class BpmTaskCandidatePostStrategyTest extends BaseMockitoUnitTest {
@InjectMocks
diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateRoleStrategyTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateRoleStrategyTest.java
index 4db4cb1..5f4f6f2 100644
--- a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateRoleStrategyTest.java
+++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateRoleStrategyTest.java
@@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.u
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
import cn.iocoder.yudao.module.system.api.permission.RoleApi;
-import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
@@ -15,7 +14,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
-@Disabled // TODO 芋艿:临时注释
public class BpmTaskCandidateRoleStrategyTest extends BaseMockitoUnitTest {
@InjectMocks
diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateUserStrategyTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateUserStrategyTest.java
index fca4aa1..4f8679e 100644
--- a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateUserStrategyTest.java
+++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateUserStrategyTest.java
@@ -1,7 +1,6 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
-import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
@@ -10,7 +9,6 @@ import java.util.Set;
import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet;
import static org.junit.jupiter.api.Assertions.assertEquals;
-@Disabled // TODO 芋艿:临时注释
public class BpmTaskCandidateUserStrategyTest extends BaseMockitoUnitTest {
@InjectMocks
@@ -27,5 +25,4 @@ public class BpmTaskCandidateUserStrategyTest extends BaseMockitoUnitTest {
assertEquals(asSet(1L, 2L), userIds);
}
-
}
diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListenerTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListenerTest.java
new file mode 100644
index 0000000..edc64d7
--- /dev/null
+++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListenerTest.java
@@ -0,0 +1,67 @@
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.listener;
+
+import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmBoundaryEventTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskTimeoutHandlerTypeEnum;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
+import cn.iocoder.yudao.module.bpm.service.definition.BpmModelService;
+import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService;
+import org.flowable.bpmn.model.BoundaryEvent;
+import org.flowable.bpmn.model.BpmnModel;
+import org.flowable.bpmn.model.Process;
+import org.flowable.common.engine.api.delegate.event.FlowableEngineEntityEvent;
+import org.flowable.job.api.Job;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+
+import static org.mockito.Mockito.*;
+
+/**
+ * {@link BpmTaskEventListener} 的单元测试
+ *
+ * @author 芋道源码
+ */
+public class BpmTaskEventListenerTest extends BaseMockitoUnitTest {
+
+ @InjectMocks
+ private BpmTaskEventListener taskEventListener;
+
+ @Mock
+ private BpmModelService modelService;
+ @Mock
+ private BpmTaskService taskService;
+
+ @Test
+ public void testTimerFired_elementIdFromJobHandlerConfiguration() {
+ // mock 流程模型:task 上挂载【超时提醒】的边界事件
+ BoundaryEvent boundaryEvent = new BoundaryEvent();
+ boundaryEvent.setId("timeout-boundary");
+ boundaryEvent.setAttachedToRefId("task");
+ BpmnModelUtils.addExtensionElement(boundaryEvent, BpmnModelConstants.BOUNDARY_EVENT_TYPE,
+ BpmBoundaryEventTypeEnum.USER_TASK_TIMEOUT.getType());
+ BpmnModelUtils.addExtensionElement(boundaryEvent, BpmnModelConstants.USER_TASK_TIMEOUT_HANDLER_TYPE,
+ BpmUserTaskTimeoutHandlerTypeEnum.REMINDER.getType());
+ Process process = new Process();
+ process.addFlowElement(boundaryEvent);
+ BpmnModel bpmnModel = new BpmnModel();
+ bpmnModel.addProcess(process);
+ when(modelService.getBpmnModelByDefinitionId("definition-1")).thenReturn(bpmnModel);
+ // 准备参数:部分 Flowable Job 的 elementId 为空,需要从 handler configuration 解析 activityId
+ Job job = mock(Job.class);
+ when(job.getJobHandlerConfiguration()).thenReturn("{\"activityId\":\"timeout-boundary\"}");
+ FlowableEngineEntityEvent event = mock(FlowableEngineEntityEvent.class);
+ when(event.getProcessDefinitionId()).thenReturn("definition-1");
+ when(event.getProcessInstanceId()).thenReturn("process-instance-1");
+ when(event.getEntity()).thenReturn(job);
+
+ // 调用
+ taskEventListener.timerFired(event);
+
+ // 断言:使用解析出的 activityId 查找边界事件,并触发用户任务超时处理
+ verify(taskService).processTaskTimeout("process-instance-1", "task",
+ BpmUserTaskTimeoutHandlerTypeEnum.REMINDER.getType());
+ }
+
+}
diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmHttpRequestUtilsTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmHttpRequestUtilsTest.java
new file mode 100644
index 0000000..0a450e8
--- /dev/null
+++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmHttpRequestUtilsTest.java
@@ -0,0 +1,65 @@
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.util;
+
+import cn.hutool.extra.spring.SpringUtil;
+import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
+import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
+import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent;
+import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
+import org.flowable.engine.runtime.ProcessInstance;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.client.RestTemplate;
+
+import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * {@link BpmHttpRequestUtils} 的单元测试
+ *
+ * @author HUIHUI
+ */
+public class BpmHttpRequestUtilsTest extends BaseMockitoUnitTest {
+
+ @Mock
+ private BpmProcessInstanceService processInstanceService;
+ @Mock
+ private ProcessInstance processInstance;
+ @Mock
+ private RestTemplate restTemplate;
+
+ @AfterEach
+ public void tearDown() {
+ TenantContextHolder.clear();
+ }
+
+ @Test
+ public void testExecuteBpmHttpRequest_statusEventTenantFromProcessInstance() {
+ // 准备参数:事务完成后的回调可能已经没有租户上下文,但流程实例仍带有租户编号
+ BpmProcessInstanceStatusEvent event = new BpmProcessInstanceStatusEvent(this);
+ event.setId("process-instance");
+ when(processInstanceService.getProcessInstance("process-instance")).thenReturn(processInstance);
+ when(processInstance.getTenantId()).thenReturn("tenant-1");
+ when(restTemplate.exchange(anyString(), any(), any(), eq(String.class))).thenReturn(ResponseEntity.ok("{}"));
+
+ try (MockedStatic springUtilMockedStatic = mockStatic(SpringUtil.class)) {
+ springUtilMockedStatic.when(() -> SpringUtil.getBean(BpmProcessInstanceService.class))
+ .thenReturn(processInstanceService);
+ springUtilMockedStatic.when(() -> SpringUtil.getBean(RestTemplate.class)).thenReturn(restTemplate);
+
+ // 调用
+ BpmHttpRequestUtils.executeBpmHttpRequest(event, "http://callback");
+ }
+ // 断言:请求头使用流程实例租户,而不是已清空的上下文租户
+ ArgumentCaptor> requestCaptor = ArgumentCaptor.forClass(HttpEntity.class);
+ verify(restTemplate).exchange(eq("http://callback"), any(), requestCaptor.capture(), eq(String.class));
+ assertEquals("tenant-1", requestCaptor.getValue().getHeaders().getFirst(HEADER_TENANT_ID));
+ }
+
+}
diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtilsTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtilsTest.java
new file mode 100644
index 0000000..d664dab
--- /dev/null
+++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtilsTest.java
@@ -0,0 +1,75 @@
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.util;
+
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskRejectHandlerTypeEnum;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
+import org.flowable.bpmn.model.UserTask;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * {@link BpmnModelUtils} 的单元测试
+ *
+ * @author HUIHUI
+ */
+public class BpmnModelUtilsTest {
+
+ @Test
+ public void testCandidateElements_success() {
+ // 准备参数
+ UserTask userTask = new UserTask();
+
+ // 调用
+ BpmnModelUtils.addCandidateElements(BpmTaskCandidateStrategyEnum.USER.getStrategy(), "1,2", userTask);
+ // 断言
+ assertEquals(BpmTaskCandidateStrategyEnum.USER.getStrategy(),
+ BpmnModelUtils.parseCandidateStrategy(userTask));
+ assertEquals("1,2", BpmnModelUtils.parseCandidateParam(userTask));
+ }
+
+ @Test
+ public void testCandidateElements_empty() {
+ // 准备参数
+ UserTask userTask = new UserTask();
+
+ // 调用
+ BpmnModelUtils.addCandidateElements(null, null, userTask);
+ // 断言
+ assertNull(BpmnModelUtils.parseCandidateStrategy(userTask));
+ assertNull(BpmnModelUtils.parseCandidateParam(userTask));
+ }
+
+ @Test
+ public void testRejectElements_success() {
+ // 准备参数
+ UserTask userTask = new UserTask();
+ BpmSimpleModelNodeVO.RejectHandler rejectHandler = new BpmSimpleModelNodeVO.RejectHandler()
+ .setType(BpmUserTaskRejectHandlerTypeEnum.RETURN_USER_TASK.getType())
+ .setReturnNodeId("approve");
+
+ // 调用
+ BpmnModelUtils.addTaskRejectElements(rejectHandler, userTask);
+ // 断言
+ assertEquals(BpmUserTaskRejectHandlerTypeEnum.RETURN_USER_TASK,
+ BpmnModelUtils.parseRejectHandlerType(userTask));
+ assertEquals("approve", BpmnModelUtils.parseReturnTaskId(userTask));
+ }
+
+ @Test
+ public void testExtensionElement_emptyValue_notAdd() {
+ // 准备参数
+ UserTask userTask = new UserTask();
+
+ // 调用
+ BpmnModelUtils.addExtensionElement(userTask, "remark", (String) null);
+ BpmnModelUtils.addExtensionElementJson(userTask, "config", null);
+ BpmnModelUtils.addExtensionElement(userTask, "attributes", Collections.emptyMap());
+ // 断言
+ assertNull(BpmnModelUtils.parseExtensionElement(userTask, "remark"));
+ assertNull(BpmnModelUtils.parseExtensionElement(userTask, "config"));
+ }
+
+}
diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtilsTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtilsTest.java
new file mode 100644
index 0000000..c8d2e9a
--- /dev/null
+++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtilsTest.java
@@ -0,0 +1,367 @@
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.util;
+
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmChildProcessStartUserEmptyTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmChildProcessStartUserTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmBoundaryEventTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmConditionOpCodeEnum;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmDelayTimerTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModeConditionTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmTriggerTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskApproveMethodEnum;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskApproveTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskTimeoutHandlerTypeEnum;
+import cn.iocoder.yudao.module.bpm.framework.flowable.BaseFlowableUnitTest;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
+import org.flowable.bpmn.model.BoundaryEvent;
+import org.flowable.bpmn.model.BpmnModel;
+import org.flowable.bpmn.model.CallActivity;
+import org.flowable.bpmn.model.EndEvent;
+import org.flowable.bpmn.model.ExclusiveGateway;
+import org.flowable.bpmn.model.FlowElement;
+import org.flowable.bpmn.model.Process;
+import org.flowable.bpmn.model.ReceiveTask;
+import org.flowable.bpmn.model.SequenceFlow;
+import org.flowable.bpmn.model.ServiceTask;
+import org.flowable.bpmn.model.TimerEventDefinition;
+import org.flowable.bpmn.model.UserTask;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.BOUNDARY_EVENT_TYPE;
+import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.START_EVENT_NODE_ID;
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * {@link SimpleModelUtils} 的单元测试
+ *
+ * @author HUIHUI
+ */
+public class SimpleModelUtilsTest extends BaseFlowableUnitTest {
+
+ @Test
+ public void testBuildBpmnModel_startUserApproveEnd() {
+ // 准备参数
+ BpmSimpleModelNodeVO approveNode = node("approve", BpmSimpleModelNodeTypeEnum.APPROVE_NODE)
+ .setName("审批").setApproveType(BpmUserTaskApproveTypeEnum.USER.getType())
+ .setApproveMethod(BpmUserTaskApproveMethodEnum.RANDOM.getMethod())
+ .setCandidateStrategy(BpmTaskCandidateStrategyEnum.USER.getStrategy()).setCandidateParam("1,2");
+ BpmSimpleModelNodeVO startUserNode = node("start-user", BpmSimpleModelNodeTypeEnum.START_USER_NODE)
+ .setName("发起人").setChildNode(approveNode);
+ approveNode.setChildNode(node("end", BpmSimpleModelNodeTypeEnum.END_NODE).setName("结束"));
+
+ // 调用
+ BpmnModel bpmnModel = SimpleModelUtils.buildBpmnModel("process", "测试流程", startUserNode);
+ Process process = bpmnModel.getMainProcess();
+ // 断言
+ assertEquals("process", process.getId());
+ assertEquals("测试流程", process.getName());
+ assertNotNull(process.getFlowElement(START_EVENT_NODE_ID));
+ assertInstanceOf(UserTask.class, process.getFlowElement("start-user"));
+ UserTask approveTask = (UserTask) process.getFlowElement("approve");
+ assertEquals(BpmTaskCandidateStrategyEnum.USER.getStrategy(), BpmnModelUtils.parseCandidateStrategy(approveTask));
+ assertEquals("1,2", BpmnModelUtils.parseCandidateParam(approveTask));
+ assertInstanceOf(EndEvent.class, process.getFlowElement("end"));
+ assertTrue(process.getFlowElements().stream().anyMatch(item -> item instanceof SequenceFlow
+ && "approve".equals(((SequenceFlow) item).getSourceRef())
+ && "end".equals(((SequenceFlow) item).getTargetRef())));
+ }
+
+ @Test
+ public void testBuildBpmnModel_approveConfig() {
+ // 准备参数
+ BpmSimpleModelNodeVO approveNode = node("approve", BpmSimpleModelNodeTypeEnum.APPROVE_NODE)
+ .setApproveType(BpmUserTaskApproveTypeEnum.USER.getType())
+ .setApproveMethod(BpmUserTaskApproveMethodEnum.RATIO.getMethod()).setApproveRatio(60)
+ .setCandidateStrategy(BpmTaskCandidateStrategyEnum.USER.getStrategy()).setCandidateParam("1")
+ .setTimeoutHandler(new BpmSimpleModelNodeVO.TimeoutHandler()
+ .setEnable(true).setType(BpmUserTaskTimeoutHandlerTypeEnum.REMINDER.getType())
+ .setTimeDuration("PT1H").setMaxRemindCount(3));
+
+ // 调用
+ List flowElements = new ApproveNodeFlowBuilder().convert(approveNode);
+ // 断言
+ UserTask userTask = (UserTask) flowElements.get(0);
+ assertEquals("${ nrOfCompletedInstances/nrOfInstances >= 0.60}",
+ userTask.getLoopCharacteristics().getCompletionCondition());
+ assertEquals(BpmTaskCandidateStrategyEnum.USER.getStrategy(), BpmnModelUtils.parseCandidateStrategy(userTask));
+ BoundaryEvent boundaryEvent = (BoundaryEvent) flowElements.get(1);
+ TimerEventDefinition timer = (TimerEventDefinition) boundaryEvent.getEventDefinitions().get(0);
+ assertEquals("PT1H", timer.getTimeDuration());
+ assertEquals("R3/PT1H", timer.getTimeCycle());
+ }
+
+ @Test
+ public void testDelayTimerNodeConvert_durationAndDate() {
+ // 准备参数
+ BpmSimpleModelNodeVO durationNode = node("delay-duration", BpmSimpleModelNodeTypeEnum.DELAY_TIMER_NODE)
+ .setDelaySetting(new BpmSimpleModelNodeVO.DelaySetting()
+ .setDelayType(BpmDelayTimerTypeEnum.FIXED_TIME_DURATION.getType()).setDelayTime("PT2H"));
+ BpmSimpleModelNodeVO dateNode = node("delay-date", BpmSimpleModelNodeTypeEnum.DELAY_TIMER_NODE)
+ .setDelaySetting(new BpmSimpleModelNodeVO.DelaySetting()
+ .setDelayType(BpmDelayTimerTypeEnum.FIXED_DATE_TIME.getType())
+ .setDelayTime("2026-10-06T12:00:00"));
+
+ // 调用
+ BoundaryEvent durationEvent = (BoundaryEvent) new SimpleModelUtils.DelayTimerNodeConvert()
+ .convertList(durationNode).get(1);
+ BoundaryEvent dateEvent = (BoundaryEvent) new SimpleModelUtils.DelayTimerNodeConvert()
+ .convertList(dateNode).get(1);
+ // 断言
+ TimerEventDefinition durationTimer = (TimerEventDefinition) durationEvent.getEventDefinitions().get(0);
+ assertEquals("PT2H", durationTimer.getTimeDuration());
+ assertNull(durationTimer.getTimeDate());
+ TimerEventDefinition dateTimer = (TimerEventDefinition) dateEvent.getEventDefinitions().get(0);
+ assertEquals("2026-10-06T12:00:00", dateTimer.getTimeDate());
+ assertNull(dateTimer.getTimeDuration());
+ }
+
+ @Test
+ public void testBuildBpmnModel_childProcessTimeout() {
+ // 准备参数
+ BpmSimpleModelNodeVO.ChildProcessSetting setting = new BpmSimpleModelNodeVO.ChildProcessSetting()
+ .setCalledProcessDefinitionKey("child-process").setCalledProcessDefinitionName("子流程")
+ .setAsync(false).setSkipStartUserNode(true)
+ .setStartUserSetting(new BpmSimpleModelNodeVO.ChildProcessSetting.StartUserSetting()
+ .setType(BpmChildProcessStartUserTypeEnum.MAIN_PROCESS_START_USER.getType())
+ .setEmptyType(BpmChildProcessStartUserEmptyTypeEnum.MAIN_PROCESS_START_USER.getType()))
+ .setTimeoutSetting(new BpmSimpleModelNodeVO.ChildProcessSetting.TimeoutSetting()
+ .setEnable(true).setType(BpmDelayTimerTypeEnum.FIXED_DATE_TIME.getType())
+ .setTimeExpression("2026-10-06T12:00:00"));
+ BpmSimpleModelNodeVO childNode = node("child", BpmSimpleModelNodeTypeEnum.CHILD_PROCESS)
+ .setChildProcessSetting(setting).setChildNode(node("end", BpmSimpleModelNodeTypeEnum.END_NODE));
+
+ // 调用
+ BpmnModel bpmnModel = SimpleModelUtils.buildBpmnModel("process", "测试流程", childNode);
+ Process process = bpmnModel.getMainProcess();
+ // 断言
+ assertInstanceOf(CallActivity.class, process.getFlowElement("child"));
+ BoundaryEvent boundaryEvent = process.getFlowElements().stream().filter(item -> item instanceof BoundaryEvent)
+ .map(item -> (BoundaryEvent) item).findFirst().orElseThrow();
+ TimerEventDefinition timer = (TimerEventDefinition) boundaryEvent.getEventDefinitions().get(0);
+ assertEquals("2026-10-06T12:00:00", timer.getTimeDate());
+ assertNull(timer.getTimeDuration());
+ assertEquals(String.valueOf(BpmBoundaryEventTypeEnum.CHILD_PROCESS_TIMEOUT.getType()),
+ BpmnModelUtils.parseBoundaryEventExtensionElement(boundaryEvent, BOUNDARY_EVENT_TYPE));
+ }
+
+ @Test
+ public void testBuildBpmnModel_childProcessTimeoutDuration() {
+ // 准备参数
+ BpmSimpleModelNodeVO.ChildProcessSetting setting = new BpmSimpleModelNodeVO.ChildProcessSetting()
+ .setCalledProcessDefinitionKey("child-process").setCalledProcessDefinitionName("子流程")
+ .setAsync(false).setSkipStartUserNode(true)
+ .setStartUserSetting(new BpmSimpleModelNodeVO.ChildProcessSetting.StartUserSetting()
+ .setType(BpmChildProcessStartUserTypeEnum.MAIN_PROCESS_START_USER.getType())
+ .setEmptyType(BpmChildProcessStartUserEmptyTypeEnum.MAIN_PROCESS_START_USER.getType()))
+ .setTimeoutSetting(new BpmSimpleModelNodeVO.ChildProcessSetting.TimeoutSetting()
+ .setEnable(true).setType(BpmDelayTimerTypeEnum.FIXED_TIME_DURATION.getType())
+ .setTimeExpression("PT30M"));
+ BpmSimpleModelNodeVO childNode = node("child", BpmSimpleModelNodeTypeEnum.CHILD_PROCESS)
+ .setChildProcessSetting(setting).setChildNode(node("end", BpmSimpleModelNodeTypeEnum.END_NODE));
+
+ // 调用
+ BpmnModel bpmnModel = SimpleModelUtils.buildBpmnModel("process", "测试流程", childNode);
+ Process process = bpmnModel.getMainProcess();
+ // 断言:固定时长使用 timeDuration,且不会误设置 timeDate、timeCycle
+ BoundaryEvent boundaryEvent = process.getFlowElements().stream().filter(item -> item instanceof BoundaryEvent)
+ .map(item -> (BoundaryEvent) item).findFirst().orElseThrow();
+ assertEquals("child", boundaryEvent.getAttachedToRef().getId());
+ assertFalse(boundaryEvent.isCancelActivity());
+ TimerEventDefinition timer = (TimerEventDefinition) boundaryEvent.getEventDefinitions().get(0);
+ assertEquals("PT30M", timer.getTimeDuration());
+ assertNull(timer.getTimeDate());
+ assertNull(timer.getTimeCycle());
+ // 断言:边界事件类型为子流程超时,而不是延迟器超时
+ assertEquals(String.valueOf(BpmBoundaryEventTypeEnum.CHILD_PROCESS_TIMEOUT.getType()),
+ BpmnModelUtils.parseBoundaryEventExtensionElement(boundaryEvent, BOUNDARY_EVENT_TYPE));
+ }
+
+ @Test
+ public void testBuildBpmnModel_conditionBranch() {
+ // 准备参数
+ BpmSimpleModelNodeVO branch = node("branch", BpmSimpleModelNodeTypeEnum.CONDITION_BRANCH_NODE);
+ BpmSimpleModelNodeVO condition = node("condition", BpmSimpleModelNodeTypeEnum.CONDITION_NODE)
+ .setName("金额大于 10").setConditionSetting(new BpmSimpleModelNodeVO.ConditionSetting()
+ .setConditionType(BpmSimpleModeConditionTypeEnum.EXPRESSION.getType())
+ .setConditionExpression("${amount > 10}").setDefaultFlow(false))
+ .setChildNode(node("approve", BpmSimpleModelNodeTypeEnum.APPROVE_NODE)
+ .setApproveType(BpmUserTaskApproveTypeEnum.AUTO_APPROVE.getType()));
+ BpmSimpleModelNodeVO defaultCondition = node("default-condition", BpmSimpleModelNodeTypeEnum.CONDITION_NODE)
+ .setName("默认").setConditionSetting(new BpmSimpleModelNodeVO.ConditionSetting()
+ .setConditionType(BpmSimpleModeConditionTypeEnum.EXPRESSION.getType())
+ .setDefaultFlow(true))
+ .setChildNode(node("reject", BpmSimpleModelNodeTypeEnum.APPROVE_NODE)
+ .setApproveType(BpmUserTaskApproveTypeEnum.AUTO_REJECT.getType()));
+ branch.setConditionNodes(Arrays.asList(condition, defaultCondition))
+ .setChildNode(node("end", BpmSimpleModelNodeTypeEnum.END_NODE));
+
+ // 调用
+ Process process = SimpleModelUtils.buildBpmnModel("process", "测试流程", branch).getMainProcess();
+ // 断言
+ ExclusiveGateway gateway = (ExclusiveGateway) process.getFlowElement("branch");
+ assertEquals("default-condition", gateway.getDefaultFlow());
+ SequenceFlow conditionFlow = (SequenceFlow) process.getFlowElement("condition");
+ assertEquals("${amount > 10}", conditionFlow.getConditionExpression());
+ assertEquals("approve", conditionFlow.getTargetRef());
+ }
+
+ @Test
+ public void testTriggerNodeConvert_httpCallback() {
+ // 准备参数
+ BpmSimpleModelNodeVO.TriggerSetting.HttpRequestTriggerSetting httpSetting =
+ new BpmSimpleModelNodeVO.TriggerSetting.HttpRequestTriggerSetting().setUrl("https://example.com/callback");
+ BpmSimpleModelNodeVO node = node("trigger", BpmSimpleModelNodeTypeEnum.TRIGGER_NODE)
+ .setTriggerSetting(new BpmSimpleModelNodeVO.TriggerSetting()
+ .setType(BpmTriggerTypeEnum.HTTP_CALLBACK.getType())
+ .setHttpRequestSetting(httpSetting));
+
+ // 调用
+ List extends FlowElement> flowElements = new SimpleModelUtils.TriggerNodeConvert().convertList(node);
+ // 断言
+ assertEquals(2, flowElements.size());
+ assertInstanceOf(ReceiveTask.class, flowElements.get(0));
+ assertInstanceOf(ServiceTask.class, flowElements.get(1));
+ assertNotNull(httpSetting.getCallbackTaskDefineKey());
+ assertEquals(httpSetting.getCallbackTaskDefineKey(), flowElements.get(0).getId());
+ }
+
+ @Test
+ public void testBuildConditionExpression_rule() {
+ // 准备参数
+ BpmSimpleModelNodeVO.ConditionRule numberRule = new BpmSimpleModelNodeVO.ConditionRule()
+ .setOpCode(BpmConditionOpCodeEnum.GT.getCode()).setLeftSide("amount").setRightSide("10");
+ BpmSimpleModelNodeVO.ConditionRule stringRule = new BpmSimpleModelNodeVO.ConditionRule()
+ .setOpCode(BpmConditionOpCodeEnum.EQ.getCode()).setLeftSide("status").setRightSide("PASS");
+ BpmSimpleModelNodeVO.Condition condition = new BpmSimpleModelNodeVO.Condition()
+ .setAnd(true).setRules(Arrays.asList(numberRule, stringRule));
+ BpmSimpleModelNodeVO.ConditionGroups groups = new BpmSimpleModelNodeVO.ConditionGroups()
+ .setAnd(false).setConditions(Collections.singletonList(condition));
+ BpmSimpleModelNodeVO.ConditionSetting setting = new BpmSimpleModelNodeVO.ConditionSetting()
+ .setConditionType(BpmSimpleModeConditionTypeEnum.RULE.getType()).setConditionGroups(groups);
+
+ // 调用
+ String expression = SimpleModelUtils.buildConditionExpression(setting);
+ // 断言
+ assertEquals("${( var:getOrDefault(amount, null) > 10 && var:getOrDefault(status, null) == \"PASS\" )}",
+ expression);
+ }
+
+ @Test
+ public void testBuildConditionExpression_expressionAndEmptyRule() {
+ // 准备参数
+ BpmSimpleModelNodeVO.ConditionSetting expressionSetting = new BpmSimpleModelNodeVO.ConditionSetting()
+ .setConditionType(BpmSimpleModeConditionTypeEnum.EXPRESSION.getType())
+ .setConditionExpression("${amount > 1}");
+ BpmSimpleModelNodeVO.ConditionSetting emptyRuleSetting = new BpmSimpleModelNodeVO.ConditionSetting()
+ .setConditionType(BpmSimpleModeConditionTypeEnum.RULE.getType())
+ .setConditionGroups(new BpmSimpleModelNodeVO.ConditionGroups().setAnd(true)
+ .setConditions(Collections.emptyList()));
+
+ // 调用,并断言
+ assertEquals("${amount > 1}", SimpleModelUtils.buildConditionExpression(expressionSetting));
+ assertNull(SimpleModelUtils.buildConditionExpression(emptyRuleSetting));
+ }
+
+ @Test
+ public void testSimulateProcess_conditionBranches() {
+ // 准备参数
+ BpmSimpleModelNodeVO branch = node("branch", BpmSimpleModelNodeTypeEnum.CONDITION_BRANCH_NODE);
+ BpmSimpleModelNodeVO matched = node("matched", BpmSimpleModelNodeTypeEnum.APPROVE_NODE)
+ .setApproveType(BpmUserTaskApproveTypeEnum.AUTO_APPROVE.getType())
+ .setConditionSetting(new BpmSimpleModelNodeVO.ConditionSetting()
+ .setConditionType(BpmSimpleModeConditionTypeEnum.EXPRESSION.getType())
+ .setConditionExpression("${amount > 10}").setDefaultFlow(false));
+ BpmSimpleModelNodeVO defaultNode = node("default", BpmSimpleModelNodeTypeEnum.APPROVE_NODE)
+ .setApproveType(BpmUserTaskApproveTypeEnum.AUTO_REJECT.getType())
+ .setConditionSetting(new BpmSimpleModelNodeVO.ConditionSetting()
+ .setConditionType(BpmSimpleModeConditionTypeEnum.EXPRESSION.getType())
+ .setDefaultFlow(true));
+ matched.setChildNode(node("matched-end", BpmSimpleModelNodeTypeEnum.END_NODE));
+ defaultNode.setChildNode(node("default-end", BpmSimpleModelNodeTypeEnum.END_NODE));
+ branch.setConditionNodes(Arrays.asList(
+ node("condition-1", BpmSimpleModelNodeTypeEnum.CONDITION_NODE).setConditionSetting(matched.getConditionSetting())
+ .setChildNode(matched),
+ node("condition-2", BpmSimpleModelNodeTypeEnum.CONDITION_NODE).setConditionSetting(defaultNode.getConditionSetting())
+ .setChildNode(defaultNode)));
+
+ // 调用
+ List result = SimpleModelUtils.simulateProcess(branch,
+ Collections.singletonMap("amount", 20));
+ // 断言
+ assertEquals(Arrays.asList("matched", "matched-end"), result.stream()
+ .map(BpmSimpleModelNodeVO::getId).toList());
+ }
+
+ @Test
+ public void testSimulateProcess_inclusiveAndParallelBranches() {
+ // 准备参数
+ BpmSimpleModelNodeVO inclusive = node("inclusive", BpmSimpleModelNodeTypeEnum.INCLUSIVE_BRANCH_NODE);
+ BpmSimpleModelNodeVO first = node("first", BpmSimpleModelNodeTypeEnum.APPROVE_NODE)
+ .setApproveType(BpmUserTaskApproveTypeEnum.AUTO_APPROVE.getType());
+ BpmSimpleModelNodeVO second = node("second", BpmSimpleModelNodeTypeEnum.COPY_NODE);
+ first.setChildNode(node("first-end", BpmSimpleModelNodeTypeEnum.END_NODE));
+ second.setChildNode(node("second-end", BpmSimpleModelNodeTypeEnum.END_NODE));
+ inclusive.setConditionNodes(Arrays.asList(
+ node("condition-1", BpmSimpleModelNodeTypeEnum.CONDITION_NODE)
+ .setConditionSetting(new BpmSimpleModelNodeVO.ConditionSetting()
+ .setConditionType(BpmSimpleModeConditionTypeEnum.EXPRESSION.getType())
+ .setConditionExpression("${left}").setDefaultFlow(false)).setChildNode(first),
+ node("condition-2", BpmSimpleModelNodeTypeEnum.CONDITION_NODE)
+ .setConditionSetting(new BpmSimpleModelNodeVO.ConditionSetting()
+ .setConditionType(BpmSimpleModeConditionTypeEnum.EXPRESSION.getType())
+ .setConditionExpression("${right}").setDefaultFlow(false)).setChildNode(second)));
+ Map variables = new HashMap<>();
+ variables.put("left", true);
+ variables.put("right", true);
+
+ // 调用
+ List result = SimpleModelUtils.simulateProcess(inclusive, variables);
+ // 断言
+ assertEquals(Arrays.asList("first", "first-end", "second", "second-end"), result.stream()
+ .map(BpmSimpleModelNodeVO::getId).toList());
+ }
+
+ @Test
+ public void testNodeHelpers() {
+ // 准备参数
+ BpmSimpleModelNodeVO sequential = node("approve", BpmSimpleModelNodeTypeEnum.APPROVE_NODE)
+ .setApproveMethod(BpmUserTaskApproveMethodEnum.SEQUENTIAL.getMethod());
+ BpmSimpleModelNodeVO skipNode = node("skip", BpmSimpleModelNodeTypeEnum.APPROVE_NODE)
+ .setSkipExpression("${skip}");
+
+ // 调用,并断言
+ assertTrue(SimpleModelUtils.isValidNode(sequential));
+ assertFalse(SimpleModelUtils.isValidNode(new BpmSimpleModelNodeVO()));
+ assertTrue(SimpleModelUtils.isSequentialApproveNode(sequential));
+ assertFalse(SimpleModelUtils.isSequentialApproveNode(node("other", BpmSimpleModelNodeTypeEnum.COPY_NODE)
+ .setApproveMethod(BpmUserTaskApproveMethodEnum.SEQUENTIAL.getMethod())));
+ assertTrue(SimpleModelUtils.isSkipNode(skipNode, Collections.singletonMap("skip", true)));
+ assertFalse(SimpleModelUtils.isSkipNode(node("without-skip", BpmSimpleModelNodeTypeEnum.APPROVE_NODE),
+ Collections.emptyMap()));
+ }
+
+ private static BpmSimpleModelNodeVO node(String id, BpmSimpleModelNodeTypeEnum type) {
+ return new BpmSimpleModelNodeVO().setId(id).setType(type.getType());
+ }
+
+ /**
+ * 通过受保护转换器验证审批节点配置,不暴露生产实现细节。
+ */
+ private static class ApproveNodeFlowBuilder {
+ List convert(BpmSimpleModelNodeVO node) {
+ BpmnModel model = SimpleModelUtils.buildBpmnModel("test", "test",
+ node.setChildNode(node("end", BpmSimpleModelNodeTypeEnum.END_NODE)));
+ Process process = model.getMainProcess();
+ return Arrays.asList(process.getFlowElement("approve"), process.getFlowElements().stream()
+ .filter(item -> item instanceof BoundaryEvent).map(item -> (BoundaryEvent) item)
+ .findFirst().orElseThrow());
+ }
+ }
+}
diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/category/BpmCategoryServiceImplTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/category/BpmCategoryServiceImplTest.java
index 19199e5..ce3d6af 100644
--- a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/category/BpmCategoryServiceImplTest.java
+++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/category/BpmCategoryServiceImplTest.java
@@ -138,4 +138,4 @@ public class BpmCategoryServiceImplTest extends BaseDbUnitTest {
assertPojoEquals(dbCategory, pageResult.getList().get(0));
}
-}
\ No newline at end of file
+}
diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/comment/BpmCommentServiceImplTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/comment/BpmCommentServiceImplTest.java
new file mode 100644
index 0000000..7a0db81
--- /dev/null
+++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/comment/BpmCommentServiceImplTest.java
@@ -0,0 +1,73 @@
+package cn.iocoder.yudao.module.bpm.service.comment;
+
+import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
+import cn.iocoder.yudao.module.bpm.controller.admin.comment.vo.BpmCommentCreateReqVO;
+import cn.iocoder.yudao.module.bpm.enums.task.BpmCommentTypeEnum;
+import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService;
+import org.flowable.engine.TaskService;
+import org.flowable.task.api.Task;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * {@link BpmCommentServiceImpl} 的单元测试
+ *
+ * @author HUIHUI
+ */
+public class BpmCommentServiceImplTest extends BaseMockitoUnitTest {
+
+ @InjectMocks
+ private BpmCommentServiceImpl commentService;
+
+ @Mock
+ private TaskService taskService;
+ @Mock
+ private BpmTaskService bpmTaskService;
+ @Mock
+ private Task task;
+
+ @Test
+ public void testGetCommentListByProcessInstanceId_success() {
+ // 准备参数
+ String processInstanceId = "process-instance-1";
+
+ // 调用
+ commentService.getCommentListByProcessInstanceId(processInstanceId);
+ // 断言
+ verify(taskService).getProcessInstanceComments(processInstanceId);
+ }
+
+ @Test
+ public void testCreateComment_success() {
+ // 准备参数
+ BpmCommentCreateReqVO reqVO = new BpmCommentCreateReqVO()
+ .setTaskId("task-1").setMessage("请补充附件");
+ when(bpmTaskService.validateTaskExists(reqVO.getTaskId())).thenReturn(task);
+ when(task.getId()).thenReturn(reqVO.getTaskId());
+ when(task.getProcessInstanceId()).thenReturn("process-instance-1");
+
+ // 调用
+ commentService.createComment(reqVO);
+ // 断言
+ verify(taskService).addComment(eq("task-1"), eq("process-instance-1"),
+ eq(BpmCommentTypeEnum.COMMENT.getType()), eq("请补充附件"));
+ }
+
+ @Test
+ public void testCreateComment_withType_success() {
+ // 准备参数
+ String taskId = "task-1";
+ String processInstanceId = "process-instance-1";
+
+ // 调用
+ commentService.createComment(taskId, processInstanceId, BpmCommentTypeEnum.APPROVE, "同意");
+ // 断言
+ verify(taskService).addComment(taskId, processInstanceId, BpmCommentTypeEnum.APPROVE.getType(), "审批通过,原因是:同意");
+ }
+
+}
diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/definition/BpmFormServiceTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/definition/BpmFormServiceTest.java
index 93281a4..5912c96 100644
--- a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/definition/BpmFormServiceTest.java
+++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/definition/BpmFormServiceTest.java
@@ -35,7 +35,6 @@ public class BpmFormServiceTest extends BaseDbUnitTest {
@Resource
private BpmFormServiceImpl formService;
-
@Resource
private BpmFormMapper formMapper;
diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImplTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImplTest.java
index 217d1b3..23007d0 100644
--- a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImplTest.java
+++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImplTest.java
@@ -7,6 +7,7 @@ import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelSaveReqVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelUpdateReqVO;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeTypeEnum;
import org.flowable.engine.RepositoryService;
@@ -22,8 +23,11 @@ import org.mockito.Mock;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
+import java.util.List;
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
+import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.MODEL_KEY_EXISTS;
+import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.MODEL_KEY_VALID;
import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.MODEL_NOT_EXISTS;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.AdditionalMatchers.aryEq;
@@ -47,6 +51,8 @@ public class BpmModelServiceImplTest extends BaseMockitoUnitTest {
private RepositoryService repositoryService;
@Mock
private ModelQuery modelQuery;
+ @Mock
+ private BpmProcessDefinitionService processDefinitionService;
@BeforeEach
public void setUp() {
@@ -153,6 +159,163 @@ public class BpmModelServiceImplTest extends BaseMockitoUnitTest {
assertEquals(Collections.singletonList(40L), metaInfo.getManagerUserIds());
}
+ @Test
+ public void testCreateModel_simple() {
+ // 准备参数
+ BpmSimpleModelNodeVO simpleModel = simpleModel();
+ BpmModelSaveReqVO reqVO = new BpmModelSaveReqVO();
+ reqVO.setKey("simple_process");
+ reqVO.setName("简单流程");
+ reqVO.setCategory("OA");
+ reqVO.setType(BpmModelTypeEnum.SIMPLE.getType());
+ reqVO.setSimpleModel(simpleModel);
+ Model model = mock(Model.class);
+ when(model.getId()).thenReturn(MODEL_ID);
+ when(model.getKey()).thenReturn(reqVO.getKey());
+ when(model.getName()).thenReturn(reqVO.getName());
+ mockModelKeyNotExists(reqVO.getKey());
+ when(repositoryService.newModel()).thenReturn(model);
+
+ // 调用
+ String result = modelService.createModel(reqVO);
+
+ // 断言
+ assertEquals(MODEL_ID, result);
+ verify(repositoryService).saveModel(same(model));
+ verify(repositoryService).addModelEditorSource(eq(MODEL_ID), any(byte[].class));
+ verify(repositoryService).addModelEditorSourceExtra(eq(MODEL_ID), aryEq(JsonUtils.toJsonByte(simpleModel)));
+ }
+
+ @Test
+ public void testCreateModel_invalidKey() {
+ // 准备参数
+ BpmModelSaveReqVO reqVO = new BpmModelSaveReqVO().setKey("bad key");
+
+ // 调用,并断言异常
+ assertServiceException(() -> modelService.createModel(reqVO), MODEL_KEY_VALID);
+ verifyNoInteractions(repositoryService);
+ }
+
+ @Test
+ public void testCreateModel_duplicateKey() {
+ // 准备参数
+ BpmModelSaveReqVO reqVO = new BpmModelSaveReqVO().setKey("duplicate_process");
+ mockModelKeyExists(reqVO.getKey());
+
+ // 调用,并断言异常
+ assertServiceException(() -> modelService.createModel(reqVO), MODEL_KEY_EXISTS, reqVO.getKey());
+ verify(repositoryService, never()).newModel();
+ }
+
+ @Test
+ public void testGetModelList_nameFilter() {
+ // 准备参数
+ Model model = mock(Model.class);
+ when(repositoryService.createModelQuery()).thenReturn(modelQuery);
+ when(modelQuery.modelNameLike(eq("%请假%"))).thenReturn(modelQuery);
+ when(modelQuery.modelTenantId(eq(TENANT_ID.toString()))).thenReturn(modelQuery);
+ when(modelQuery.list()).thenReturn(Collections.singletonList(model));
+
+ // 调用
+ List result = modelService.getModelList("请假");
+
+ // 断言
+ assertEquals(Collections.singletonList(model), result);
+ verify(modelQuery).modelNameLike(eq("%请假%"));
+ }
+
+ @Test
+ public void testGetSimpleModel_success() {
+ // 准备参数
+ Model model = mock(Model.class);
+ BpmSimpleModelNodeVO simpleModel = simpleModel();
+ when(model.getId()).thenReturn(MODEL_ID);
+ mockGetModel(model);
+ when(repositoryService.getModelEditorSourceExtra(eq(MODEL_ID))).thenReturn(JsonUtils.toJsonByte(simpleModel));
+
+ // 调用
+ BpmSimpleModelNodeVO result = modelService.getSimpleModel(MODEL_ID);
+
+ // 断言
+ assertEquals(simpleModel.getId(), result.getId());
+ assertEquals(simpleModel.getType(), result.getType());
+ }
+
+ @Test
+ public void testUpdateSimpleModel_success() {
+ // 准备参数
+ Model model = mock(Model.class);
+ when(model.getId()).thenReturn(MODEL_ID);
+ when(model.getKey()).thenReturn("simple_process");
+ when(model.getName()).thenReturn("简单流程");
+ when(model.getCreateTime()).thenReturn(new Date(1_000L));
+ BpmModelMetaInfoVO metaInfo = new BpmModelMetaInfoVO();
+ metaInfo.setType(BpmModelTypeEnum.SIMPLE.getType());
+ metaInfo.setManagerUserIds(Collections.singletonList(10L));
+ when(model.getMetaInfo()).thenReturn(JsonUtils.toJsonString(metaInfo));
+ BpmSimpleModelNodeVO simpleModel = simpleModel();
+ BpmSimpleModelUpdateReqVO reqVO = new BpmSimpleModelUpdateReqVO().setId(MODEL_ID).setSimpleModel(simpleModel);
+ mockGetModel(model);
+
+ // 调用
+ modelService.updateSimpleModel(10L, reqVO);
+
+ // 断言
+ verify(repositoryService).addModelEditorSource(eq(MODEL_ID), any(byte[].class));
+ verify(repositoryService).addModelEditorSourceExtra(eq(MODEL_ID), aryEq(JsonUtils.toJsonByte(simpleModel)));
+ }
+
+ @Test
+ public void testUpdateModelSortBatch_success() {
+ // 准备参数
+ List ids = Arrays.asList("model-1", "model-2", "model-3");
+ Model model1 = mockSortableModel("model-1");
+ Model model2 = mockSortableModel("model-2");
+ Model model3 = mockSortableModel("model-3");
+ ModelQuery listQuery = mock(ModelQuery.class);
+ ModelQuery managerQuery = mock(ModelQuery.class);
+ when(repositoryService.createModelQuery()).thenReturn(listQuery, managerQuery, managerQuery, managerQuery);
+ when(listQuery.modelTenantId(eq(TENANT_ID.toString()))).thenReturn(listQuery);
+ when(listQuery.list()).thenReturn(Arrays.asList(model1, model2, model3));
+ when(managerQuery.modelId(anyString())).thenReturn(managerQuery);
+ when(managerQuery.modelTenantId(eq(TENANT_ID.toString()))).thenReturn(managerQuery);
+ when(managerQuery.singleResult()).thenReturn(model1, model2, model3);
+
+ // 调用
+ modelService.updateModelSortBatch(1L, ids);
+
+ // 断言:所有模型(包括第一个)都更新排序,且按 ids 顺序递增(列表按 sort 升序展示)
+ verify(repositoryService, times(3)).saveModel(any(Model.class));
+ ArgumentCaptor modelIdCaptor = ArgumentCaptor.forClass(String.class);
+ ArgumentCaptor sortCaptor = ArgumentCaptor.forClass(Long.class);
+ verify(processDefinitionService, times(3))
+ .updateProcessDefinitionSortByModelId(modelIdCaptor.capture(), sortCaptor.capture());
+ assertEquals(Arrays.asList("model-3", "model-2", "model-1"), modelIdCaptor.getAllValues());
+ List sorts = sortCaptor.getAllValues();
+ assertTrue(sorts.get(0) > sorts.get(1) && sorts.get(1) > sorts.get(2));
+ }
+
+ private void mockModelKeyNotExists(String key) {
+ when(repositoryService.createModelQuery()).thenReturn(modelQuery);
+ when(modelQuery.modelTenantId(eq(TENANT_ID.toString()))).thenReturn(modelQuery);
+ when(modelQuery.modelKey(eq(key))).thenReturn(modelQuery);
+ when(modelQuery.singleResult()).thenReturn(null);
+ }
+
+ private void mockModelKeyExists(String key) {
+ when(repositoryService.createModelQuery()).thenReturn(modelQuery);
+ when(modelQuery.modelTenantId(eq(TENANT_ID.toString()))).thenReturn(modelQuery);
+ when(modelQuery.modelKey(eq(key))).thenReturn(modelQuery);
+ when(modelQuery.singleResult()).thenReturn(mock(Model.class));
+ }
+
+ private BpmSimpleModelNodeVO simpleModel() {
+ return new BpmSimpleModelNodeVO().setId("start-user")
+ .setType(BpmSimpleModelNodeTypeEnum.START_USER_NODE.getType())
+ .setChildNode(new BpmSimpleModelNodeVO().setId("end")
+ .setType(BpmSimpleModelNodeTypeEnum.END_NODE.getType()));
+ }
+
private Model mockModel(Integer type) {
Model model = mock(Model.class);
when(model.getKey()).thenReturn("test_process");
@@ -165,6 +328,16 @@ public class BpmModelServiceImplTest extends BaseMockitoUnitTest {
return model;
}
+ private Model mockSortableModel(String id) {
+ Model model = mock(Model.class);
+ when(model.getId()).thenReturn(id);
+ when(model.getCreateTime()).thenReturn(new Date(1_000L));
+ BpmModelMetaInfoVO metaInfo = new BpmModelMetaInfoVO();
+ metaInfo.setManagerUserIds(Collections.singletonList(1L));
+ when(model.getMetaInfo()).thenReturn(JsonUtils.toJsonString(metaInfo));
+ return model;
+ }
+
private void mockGetModel(Model model) {
when(repositoryService.createModelQuery()).thenReturn(modelQuery);
when(modelQuery.modelId(eq(MODEL_ID))).thenReturn(modelQuery);
diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImplTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImplTest.java
new file mode 100644
index 0000000..09fe75f
--- /dev/null
+++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImplTest.java
@@ -0,0 +1,162 @@
+package cn.iocoder.yudao.module.bpm.service.definition;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionPageReqVO;
+import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
+import cn.iocoder.yudao.module.bpm.dal.mysql.definition.BpmProcessDefinitionInfoMapper;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import org.flowable.common.engine.impl.db.SuspensionState;
+import org.flowable.engine.RepositoryService;
+import org.flowable.engine.repository.Deployment;
+import org.flowable.engine.repository.DeploymentQuery;
+import org.flowable.engine.repository.ProcessDefinition;
+import org.flowable.engine.repository.ProcessDefinitionQuery;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.*;
+
+/**
+ * {@link BpmProcessDefinitionServiceImpl} 的单元测试
+ *
+ * @author HUIHUI
+ */
+public class BpmProcessDefinitionServiceImplTest extends BaseMockitoUnitTest {
+
+ @InjectMocks
+ private BpmProcessDefinitionServiceImpl processDefinitionService;
+
+ @Mock
+ private RepositoryService repositoryService;
+ @Mock
+ private BpmProcessDefinitionInfoMapper processDefinitionMapper;
+ @Mock
+ private AdminUserApi adminUserApi;
+ @Mock
+ private ProcessDefinitionQuery processDefinitionQuery;
+ @Mock
+ private DeploymentQuery deploymentQuery;
+
+ @Test
+ public void testCanUserStartProcessDefinition_null() {
+ // 调用,并断言
+ assertFalse(processDefinitionService.canUserStartProcessDefinition(null, 1L));
+ }
+
+ @Test
+ public void testCanUserStartProcessDefinition_startUserIds() {
+ // 准备参数
+ BpmProcessDefinitionInfoDO definition = new BpmProcessDefinitionInfoDO().setStartUserIds(List.of(1L, 2L));
+
+ // 调用,并断言
+ assertTrue(processDefinitionService.canUserStartProcessDefinition(definition, 2L));
+ assertFalse(processDefinitionService.canUserStartProcessDefinition(definition, 3L));
+ }
+
+ @Test
+ public void testCanUserStartProcessDefinition_startDeptIdsMatch() {
+ // 准备参数
+ BpmProcessDefinitionInfoDO definition = new BpmProcessDefinitionInfoDO().setStartDeptIds(List.of(10L));
+ when(adminUserApi.getUser(1L)).thenReturn(new AdminUserRespDTO().setDeptId(10L));
+
+ // 调用,并断言
+ assertTrue(processDefinitionService.canUserStartProcessDefinition(definition, 1L));
+ }
+
+ @Test
+ public void testCanUserStartProcessDefinition_startDeptIdsNotMatch() {
+ // 准备参数
+ BpmProcessDefinitionInfoDO definition = new BpmProcessDefinitionInfoDO().setStartDeptIds(List.of(10L));
+ when(adminUserApi.getUser(1L)).thenReturn(new AdminUserRespDTO().setDeptId(20L));
+
+ // 调用,并断言
+ assertFalse(processDefinitionService.canUserStartProcessDefinition(definition, 1L));
+ }
+
+ @Test
+ public void testCanUserStartProcessDefinition_noLimit() {
+ // 调用,并断言
+ assertTrue(processDefinitionService.canUserStartProcessDefinition(new BpmProcessDefinitionInfoDO(), 1L));
+ }
+
+ @Test
+ public void testGetProcessDefinitionByDeploymentId_empty() {
+ // 调用,并断言
+ assertNull(processDefinitionService.getProcessDefinitionByDeploymentId(null));
+ assertNull(processDefinitionService.getProcessDefinitionByDeploymentId(""));
+ }
+
+ @Test
+ public void testGetProcessDefinitionListByDeploymentIds_empty() {
+ // 调用,并断言
+ assertTrue(processDefinitionService.getProcessDefinitionListByDeploymentIds(Collections.emptySet()).isEmpty());
+ }
+
+ @Test
+ public void testGetActiveProcessDefinition_success() {
+ // 准备参数
+ ProcessDefinition definition = mock(ProcessDefinition.class);
+ when(repositoryService.createProcessDefinitionQuery()).thenReturn(processDefinitionQuery);
+ when(processDefinitionQuery.processDefinitionTenantId(anyString())).thenReturn(processDefinitionQuery);
+ when(processDefinitionQuery.processDefinitionKey("leave")).thenReturn(processDefinitionQuery);
+ when(processDefinitionQuery.active()).thenReturn(processDefinitionQuery);
+ when(processDefinitionQuery.singleResult()).thenReturn(definition);
+
+ // 调用,并断言
+ assertSame(definition, processDefinitionService.getActiveProcessDefinition("leave"));
+ }
+
+ @Test
+ public void testGetDeploymentList_filterNotExists() {
+ // 准备参数
+ Deployment deployment = mock(Deployment.class);
+ when(repositoryService.createDeploymentQuery()).thenReturn(deploymentQuery);
+ when(deploymentQuery.deploymentId(anyString())).thenReturn(deploymentQuery);
+ when(deploymentQuery.singleResult()).thenReturn(deployment, null);
+
+ // 调用,并断言
+ assertEquals(Collections.singletonList(deployment),
+ processDefinitionService.getDeploymentList(Set.of("deployment-1", "deployment-2")));
+ }
+
+ @Test
+ public void testUpdateProcessDefinitionState_activeAndSuspended() {
+ // 准备参数
+ ProcessDefinition definition = mock(ProcessDefinition.class);
+ when(repositoryService.getProcessDefinition("definition-1")).thenReturn(definition);
+
+ // 调用:挂起状态恢复为激活
+ when(definition.isSuspended()).thenReturn(true);
+ processDefinitionService.updateProcessDefinitionState("definition-1", SuspensionState.ACTIVE.getStateCode());
+ verify(repositoryService).activateProcessDefinitionById("definition-1", false, null);
+
+ // 调用:激活状态挂起
+ when(definition.isSuspended()).thenReturn(false);
+ processDefinitionService.updateProcessDefinitionState("definition-1", SuspensionState.SUSPENDED.getStateCode());
+ verify(repositoryService).suspendProcessDefinitionById("definition-1", false, null);
+ }
+
+ @Test
+ public void testGetProcessDefinitionPage_empty() {
+ // 准备参数
+ BpmProcessDefinitionPageReqVO pageVO = new BpmProcessDefinitionPageReqVO();
+ when(repositoryService.createProcessDefinitionQuery()).thenReturn(processDefinitionQuery);
+ when(processDefinitionQuery.processDefinitionTenantId(anyString())).thenReturn(processDefinitionQuery);
+ when(processDefinitionQuery.count()).thenReturn(0L);
+
+ // 调用,并断言
+ PageResult result = processDefinitionService.getProcessDefinitionPage(pageVO);
+ assertEquals(0L, result.getTotal());
+ assertTrue(result.getList().isEmpty());
+ }
+
+}
diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessExpressionServiceImplTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessExpressionServiceImplTest.java
new file mode 100644
index 0000000..e132965
--- /dev/null
+++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessExpressionServiceImplTest.java
@@ -0,0 +1,118 @@
+package cn.iocoder.yudao.module.bpm.service.definition;
+
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+import cn.iocoder.yudao.framework.test.core.util.AssertUtils;
+import cn.iocoder.yudao.framework.test.core.util.RandomUtils;
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.expression.BpmProcessExpressionPageReqVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.expression.BpmProcessExpressionSaveReqVO;
+import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessExpressionDO;
+import cn.iocoder.yudao.module.bpm.dal.mysql.definition.BpmProcessExpressionMapper;
+import jakarta.annotation.Resource;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.springframework.context.annotation.Import;
+
+import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime;
+import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildTime;
+import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
+import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_EXPRESSION_NOT_EXISTS;
+
+/**
+ * {@link BpmProcessExpressionServiceImpl} 的单元测试类
+ *
+ * @author HUIHUI
+ */
+@Import(BpmProcessExpressionServiceImpl.class)
+public class BpmProcessExpressionServiceImplTest extends BaseDbUnitTest {
+
+ @Resource
+ private BpmProcessExpressionServiceImpl processExpressionService;
+ @Resource
+ private BpmProcessExpressionMapper processExpressionMapper;
+
+ @Test
+ public void testCreateProcessExpression_success() {
+ // 准备参数
+ BpmProcessExpressionSaveReqVO reqVO = RandomUtils.randomPojo(BpmProcessExpressionSaveReqVO.class)
+ .setId(null);
+
+ // 调用
+ Long expressionId = processExpressionService.createProcessExpression(reqVO);
+ // 断言
+ Assertions.assertNotNull(expressionId);
+ AssertUtils.assertPojoEquals(reqVO, processExpressionMapper.selectById(expressionId), "id");
+ }
+
+ @Test
+ public void testUpdateProcessExpression_success() {
+ // 准备参数
+ BpmProcessExpressionDO dbExpression = RandomUtils.randomPojo(BpmProcessExpressionDO.class);
+ processExpressionMapper.insert(dbExpression);
+ BpmProcessExpressionSaveReqVO reqVO = RandomUtils.randomPojo(BpmProcessExpressionSaveReqVO.class)
+ .setId(dbExpression.getId());
+
+ // 调用
+ processExpressionService.updateProcessExpression(reqVO);
+ // 断言
+ AssertUtils.assertPojoEquals(reqVO, processExpressionMapper.selectById(dbExpression.getId()));
+ }
+
+ @Test
+ public void testUpdateProcessExpression_notExists() {
+ // 准备参数
+ BpmProcessExpressionSaveReqVO reqVO = RandomUtils.randomPojo(BpmProcessExpressionSaveReqVO.class);
+
+ // 调用,并断言异常
+ AssertUtils.assertServiceException(() -> processExpressionService.updateProcessExpression(reqVO),
+ PROCESS_EXPRESSION_NOT_EXISTS);
+ }
+
+ @Test
+ public void testDeleteProcessExpression_success() {
+ // 准备参数
+ BpmProcessExpressionDO dbExpression = RandomUtils.randomPojo(BpmProcessExpressionDO.class);
+ processExpressionMapper.insert(dbExpression);
+
+ // 调用
+ processExpressionService.deleteProcessExpression(dbExpression.getId());
+ // 断言
+ Assertions.assertNull(processExpressionMapper.selectById(dbExpression.getId()));
+ }
+
+ @Test
+ public void testDeleteProcessExpression_notExists() {
+ // 准备参数
+ Long id = RandomUtils.randomLongId();
+
+ // 调用,并断言异常
+ AssertUtils.assertServiceException(() -> processExpressionService.deleteProcessExpression(id),
+ PROCESS_EXPRESSION_NOT_EXISTS);
+ }
+
+ @Test
+ public void testGetProcessExpressionPage() {
+ // 准备参数
+ BpmProcessExpressionDO dbExpression = RandomUtils.randomPojo(BpmProcessExpressionDO.class, o -> {
+ o.setName("请假审批");
+ o.setStatus(CommonStatusEnum.ENABLE.getStatus());
+ o.setCreateTime(buildTime(2023, 2, 2));
+ });
+ processExpressionMapper.insert(dbExpression);
+ processExpressionMapper.insert(cloneIgnoreId(dbExpression, o -> o.setName("报销审批")));
+ processExpressionMapper.insert(cloneIgnoreId(dbExpression, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus())));
+ processExpressionMapper.insert(cloneIgnoreId(dbExpression, o -> o.setCreateTime(buildTime(2024, 2, 2))));
+ BpmProcessExpressionPageReqVO reqVO = new BpmProcessExpressionPageReqVO();
+ reqVO.setName("请假");
+ reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
+ reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+
+ // 调用
+ PageResult pageResult = processExpressionService.getProcessExpressionPage(reqVO);
+ // 断言
+ Assertions.assertEquals(1, pageResult.getTotal());
+ AssertUtils.assertPojoEquals(dbExpression, pageResult.getList().get(0));
+ }
+
+}
diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessListenerServiceImplTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessListenerServiceImplTest.java
new file mode 100644
index 0000000..19caae5
--- /dev/null
+++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessListenerServiceImplTest.java
@@ -0,0 +1,114 @@
+package cn.iocoder.yudao.module.bpm.service.definition;
+
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+import cn.iocoder.yudao.framework.test.core.util.AssertUtils;
+import cn.iocoder.yudao.framework.test.core.util.RandomUtils;
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.listener.BpmProcessListenerSaveReqVO;
+import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessListenerDO;
+import cn.iocoder.yudao.module.bpm.dal.mysql.definition.BpmProcessListenerMapper;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmProcessListenerTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmProcessListenerValueTypeEnum;
+import jakarta.annotation.Resource;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.springframework.context.annotation.Import;
+
+import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_LISTENER_CLASS_IMPLEMENTS_ERROR;
+import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_LISTENER_CLASS_NOT_FOUND;
+import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_LISTENER_EXPRESSION_INVALID;
+import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_LISTENER_NOT_EXISTS;
+
+/**
+ * {@link BpmProcessListenerServiceImpl} 的单元测试类
+ *
+ * @author HUIHUI
+ */
+@Import(BpmProcessListenerServiceImpl.class)
+public class BpmProcessListenerServiceImplTest extends BaseDbUnitTest {
+
+ @Resource
+ private BpmProcessListenerServiceImpl processListenerService;
+ @Resource
+ private BpmProcessListenerMapper processListenerMapper;
+
+ @Test
+ public void testCreateProcessListener_class_success() {
+ // 准备参数
+ BpmProcessListenerSaveReqVO reqVO = randomRequest()
+ .setType(BpmProcessListenerTypeEnum.EXECUTION.getType())
+ .setValueType(BpmProcessListenerValueTypeEnum.CLASS.getType())
+ .setValue("cn.iocoder.yudao.module.bpm.framework.flowable.core.listener.demo.exection.DemoDelegateClassExecutionListener");
+
+ // 调用
+ Long listenerId = processListenerService.createProcessListener(reqVO);
+ // 断言
+ Assertions.assertNotNull(listenerId);
+ AssertUtils.assertPojoEquals(reqVO, processListenerMapper.selectById(listenerId));
+ }
+
+ @Test
+ public void testCreateProcessListener_expression_success() {
+ // 准备参数
+ BpmProcessListenerSaveReqVO reqVO = randomRequest()
+ .setValueType(BpmProcessListenerValueTypeEnum.EXPRESSION.getType()).setValue("${listener.handle}");
+
+ // 调用
+ Long listenerId = processListenerService.createProcessListener(reqVO);
+ // 断言
+ Assertions.assertNotNull(listenerId);
+ }
+
+ @Test
+ public void testCreateProcessListener_classNotFound() {
+ // 准备参数
+ BpmProcessListenerSaveReqVO reqVO = randomRequest().setValueType(BpmProcessListenerValueTypeEnum.CLASS.getType())
+ .setValue("cn.iocoder.yudao.module.bpm.NotExistsListener");
+
+ // 调用,并断言异常
+ AssertUtils.assertServiceException(() -> processListenerService.createProcessListener(reqVO),
+ PROCESS_LISTENER_CLASS_NOT_FOUND, reqVO.getValue());
+ }
+
+ @Test
+ public void testCreateProcessListener_classNotImplements() {
+ // 准备参数
+ BpmProcessListenerSaveReqVO reqVO = randomRequest().setValueType(BpmProcessListenerValueTypeEnum.CLASS.getType())
+ .setValue(String.class.getName());
+
+ // 调用,并断言异常
+ AssertUtils.assertServiceException(() -> processListenerService.createProcessListener(reqVO),
+ PROCESS_LISTENER_CLASS_IMPLEMENTS_ERROR, reqVO.getValue(),
+ org.flowable.engine.delegate.TaskListener.class.getName());
+ }
+
+ @Test
+ public void testCreateProcessListener_expressionInvalid() {
+ // 准备参数
+ BpmProcessListenerSaveReqVO reqVO = randomRequest().setValueType(BpmProcessListenerValueTypeEnum.EXPRESSION.getType())
+ .setValue("listener.handle");
+
+ // 调用,并断言异常
+ AssertUtils.assertServiceException(() -> processListenerService.createProcessListener(reqVO),
+ PROCESS_LISTENER_EXPRESSION_INVALID, reqVO.getValue());
+ }
+
+ @Test
+ public void testDeleteProcessListener_notExists() {
+ // 准备参数
+ Long id = RandomUtils.randomLongId();
+
+ // 调用,并断言异常
+ AssertUtils.assertServiceException(() -> processListenerService.deleteProcessListener(id),
+ PROCESS_LISTENER_NOT_EXISTS);
+ }
+
+ private static BpmProcessListenerSaveReqVO randomRequest() {
+ return RandomUtils.randomPojo(BpmProcessListenerSaveReqVO.class, o -> {
+ o.setStatus(CommonStatusEnum.ENABLE.getStatus());
+ o.setType(BpmProcessListenerTypeEnum.TASK.getType());
+ o.setEvent("create");
+ });
+ }
+
+}
diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/definition/BpmUserGroupServiceTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/definition/BpmUserGroupServiceTest.java
index cf119f2..9067dd0 100644
--- a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/definition/BpmUserGroupServiceTest.java
+++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/definition/BpmUserGroupServiceTest.java
@@ -31,7 +31,6 @@ public class BpmUserGroupServiceTest extends BaseDbUnitTest {
@Resource
private BpmUserGroupServiceImpl userGroupService;
-
@Resource
private BpmUserGroupMapper userGroupMapper;
diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/message/BpmMessageServiceImplTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/message/BpmMessageServiceImplTest.java
new file mode 100644
index 0000000..7995fda
--- /dev/null
+++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/message/BpmMessageServiceImplTest.java
@@ -0,0 +1,81 @@
+package cn.iocoder.yudao.module.bpm.service.message;
+
+import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
+import cn.iocoder.yudao.framework.web.config.WebProperties;
+import cn.iocoder.yudao.module.bpm.enums.message.BpmMessageEnum;
+import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceApproveReqDTO;
+import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceRejectReqDTO;
+import cn.iocoder.yudao.module.system.api.sms.SmsSendApi;
+import cn.iocoder.yudao.module.system.api.sms.dto.send.SmsSendSingleToUserReqDTO;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.verify;
+
+/**
+ * {@link BpmMessageServiceImpl} 的单元测试
+ *
+ * @author HUIHUI
+ */
+public class BpmMessageServiceImplTest extends BaseMockitoUnitTest {
+
+ @InjectMocks
+ private BpmMessageServiceImpl messageService;
+
+ @Mock
+ private SmsSendApi smsSendApi;
+ @Mock
+ private WebProperties webProperties;
+ @Mock
+ private WebProperties.Ui adminUi;
+
+ @BeforeEach
+ public void setUp() {
+ org.mockito.Mockito.when(webProperties.getAdminUi()).thenReturn(adminUi);
+ org.mockito.Mockito.when(adminUi.getUrl()).thenReturn("http://localhost:48080");
+ }
+
+ @Test
+ public void testSendMessageWhenProcessInstanceApprove_success() {
+ // 准备参数
+ BpmMessageSendWhenProcessInstanceApproveReqDTO reqDTO = new BpmMessageSendWhenProcessInstanceApproveReqDTO()
+ .setProcessInstanceId("process-instance-1").setProcessInstanceName("请假申请").setStartUserId(1L);
+
+ // 调用
+ messageService.sendMessageWhenProcessInstanceApprove(reqDTO);
+ // 断言
+ SmsSendSingleToUserReqDTO smsReqDTO = captureSmsRequest();
+ assertEquals(1L, smsReqDTO.getUserId());
+ assertEquals(BpmMessageEnum.PROCESS_INSTANCE_APPROVE.getSmsTemplateCode(), smsReqDTO.getTemplateCode());
+ assertEquals("请假申请", smsReqDTO.getTemplateParams().get("processInstanceName"));
+ assertEquals("http://localhost:48080/bpm/process-instance/detail?id=process-instance-1",
+ smsReqDTO.getTemplateParams().get("detailUrl"));
+ }
+
+ @Test
+ public void testSendMessageWhenProcessInstanceReject_success() {
+ // 准备参数
+ BpmMessageSendWhenProcessInstanceRejectReqDTO reqDTO = new BpmMessageSendWhenProcessInstanceRejectReqDTO()
+ .setProcessInstanceId("process-instance-1").setProcessInstanceName("请假申请")
+ .setStartUserId(1L).setReason("材料不完整");
+
+ // 调用
+ messageService.sendMessageWhenProcessInstanceReject(reqDTO);
+ // 断言
+ SmsSendSingleToUserReqDTO smsReqDTO = captureSmsRequest();
+ assertEquals(1L, smsReqDTO.getUserId());
+ assertEquals(BpmMessageEnum.PROCESS_INSTANCE_REJECT.getSmsTemplateCode(), smsReqDTO.getTemplateCode());
+ assertEquals("材料不完整", smsReqDTO.getTemplateParams().get("reason"));
+ }
+
+ private SmsSendSingleToUserReqDTO captureSmsRequest() {
+ ArgumentCaptor captor = ArgumentCaptor.forClass(SmsSendSingleToUserReqDTO.class);
+ verify(smsSendApi).sendSingleSmsToAdmin(captor.capture());
+ return captor.getValue();
+ }
+
+}
diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/oa/BpmOALeaveServiceImplTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/oa/BpmOALeaveServiceImplTest.java
new file mode 100644
index 0000000..ad38843
--- /dev/null
+++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/oa/BpmOALeaveServiceImplTest.java
@@ -0,0 +1,100 @@
+package cn.iocoder.yudao.module.bpm.service.oa;
+
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
+import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
+import cn.iocoder.yudao.module.bpm.controller.admin.oa.vo.BpmOALeaveCreateReqVO;
+import cn.iocoder.yudao.module.bpm.dal.dataobject.oa.BpmOALeaveDO;
+import cn.iocoder.yudao.module.bpm.dal.mysql.oa.BpmOALeaveMapper;
+import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
+import jakarta.annotation.Resource;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildTime;
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
+import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.OA_LEAVE_NOT_EXISTS;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * {@link BpmOALeaveServiceImpl} 的单元测试类
+ *
+ * @author HUIHUI
+ */
+@Import(BpmOALeaveServiceImpl.class)
+public class BpmOALeaveServiceImplTest extends BaseDbUnitTest {
+
+ @Resource
+ private BpmOALeaveServiceImpl leaveService;
+ @Resource
+ private BpmOALeaveMapper leaveMapper;
+
+ @MockitoBean
+ private BpmProcessInstanceApi processInstanceApi;
+
+ @Test
+ public void testCreateLeave_success() {
+ // 准备参数
+ BpmOALeaveCreateReqVO reqVO = new BpmOALeaveCreateReqVO().setType(1).setReason("休假")
+ .setStartTime(buildTime(2026, 10, 1)).setEndTime(buildTime(2026, 10, 3))
+ .setStartUserSelectAssignees(Map.of("approve", List.of(2L)));
+ when(processInstanceApi.createProcessInstance(eq(1L), any())).thenReturn("process-instance-1");
+
+ // 调用
+ Long leaveId = leaveService.createLeave(1L, reqVO);
+ // 断言
+ BpmOALeaveDO leave = leaveMapper.selectById(leaveId);
+ assertEquals(1L, leave.getUserId());
+ assertEquals(2L, leave.getDay());
+ assertEquals(BpmTaskStatusEnum.RUNNING.getStatus(), leave.getStatus());
+ assertEquals("process-instance-1", leave.getProcessInstanceId());
+ ArgumentCaptor captor = ArgumentCaptor.forClass(BpmProcessInstanceCreateReqDTO.class);
+ verify(processInstanceApi).createProcessInstance(eq(1L), captor.capture());
+ assertEquals("oa_leave", captor.getValue().getProcessDefinitionKey());
+ assertEquals(String.valueOf(leaveId), captor.getValue().getBusinessKey());
+ assertEquals(2L, captor.getValue().getVariables().get("day"));
+ assertEquals(reqVO.getStartUserSelectAssignees(), captor.getValue().getStartUserSelectAssignees());
+ }
+
+ @Test
+ public void testCreateLeave_processCreateFails_rollback() {
+ // 准备参数
+ BpmOALeaveCreateReqVO reqVO = new BpmOALeaveCreateReqVO().setType(1).setReason("休假")
+ .setStartTime(buildTime(2026, 10, 1)).setEndTime(buildTime(2026, 10, 3));
+ when(processInstanceApi.createProcessInstance(eq(1L), any())).thenThrow(new IllegalStateException("流程启动失败"));
+
+ // 调用,并断言异常
+ assertThrows(IllegalStateException.class, () -> leaveService.createLeave(1L, reqVO));
+
+ // 断言
+ assertEquals(0, leaveMapper.selectCount());
+ }
+
+ @Test
+ public void testUpdateLeaveStatus_success() {
+ // 准备参数
+ BpmOALeaveDO leave = randomPojo(BpmOALeaveDO.class);
+ leaveMapper.insert(leave);
+
+ // 调用
+ leaveService.updateLeaveStatus(leave.getId(), BpmTaskStatusEnum.APPROVE.getStatus());
+ // 断言
+ assertEquals(BpmTaskStatusEnum.APPROVE.getStatus(), leaveMapper.selectById(leave.getId()).getStatus());
+ }
+
+ @Test
+ public void testUpdateLeaveStatus_notExists() {
+ // 调用,并断言异常
+ assertServiceException(() -> leaveService.updateLeaveStatus(1L, BpmTaskStatusEnum.APPROVE.getStatus()),
+ OA_LEAVE_NOT_EXISTS);
+ }
+
+}
diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/oa/listener/BpmOALeaveStatusListenerTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/oa/listener/BpmOALeaveStatusListenerTest.java
new file mode 100644
index 0000000..7ae5985
--- /dev/null
+++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/oa/listener/BpmOALeaveStatusListenerTest.java
@@ -0,0 +1,49 @@
+package cn.iocoder.yudao.module.bpm.service.oa.listener;
+
+import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
+import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent;
+import cn.iocoder.yudao.module.bpm.service.oa.BpmOALeaveService;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+
+import static org.mockito.Mockito.verify;
+
+/**
+ * {@link BpmOALeaveStatusListener} 的单元测试
+ *
+ * @author HUIHUI
+ */
+public class BpmOALeaveStatusListenerTest extends BaseMockitoUnitTest {
+
+ @InjectMocks
+ private BpmOALeaveStatusListener leaveStatusListener;
+
+ @Mock
+ private BpmOALeaveService leaveService;
+
+ @Test
+ public void testOnApplicationEvent_sameProcessDefinitionKey_updateStatus() {
+ // 准备参数
+ BpmProcessInstanceStatusEvent event = new BpmProcessInstanceStatusEvent(this);
+ event.setProcessDefinitionKey("oa_leave").setBusinessKey("1").setStatus(2);
+
+ // 调用
+ leaveStatusListener.onApplicationEvent(event);
+ // 断言
+ verify(leaveService).updateLeaveStatus(1L, 2);
+ }
+
+ @Test
+ public void testOnApplicationEvent_differentProcessDefinitionKey_ignore() {
+ // 准备参数
+ BpmProcessInstanceStatusEvent event = new BpmProcessInstanceStatusEvent(this);
+ event.setProcessDefinitionKey("oa_reimbursement").setBusinessKey("1").setStatus(2);
+
+ // 调用
+ leaveStatusListener.onApplicationEvent(event);
+ // 断言
+ org.mockito.Mockito.verifyNoInteractions(leaveService);
+ }
+
+}
diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceCopyServiceImplTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceCopyServiceImplTest.java
new file mode 100644
index 0000000..aa920c0
--- /dev/null
+++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceCopyServiceImplTest.java
@@ -0,0 +1,111 @@
+package cn.iocoder.yudao.module.bpm.service.task;
+
+import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
+import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmProcessInstanceCopyDO;
+import cn.iocoder.yudao.module.bpm.dal.mysql.task.BpmProcessInstanceCopyMapper;
+import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
+import org.flowable.engine.repository.ProcessDefinition;
+import org.flowable.engine.runtime.ProcessInstance;
+import org.flowable.task.api.Task;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
+import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_DEFINITION_NOT_EXISTS;
+import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_INSTANCE_NOT_EXISTS;
+import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.TASK_NOT_EXISTS;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * {@link BpmProcessInstanceCopyServiceImpl} 的单元测试
+ *
+ * @author HUIHUI
+ */
+public class BpmProcessInstanceCopyServiceImplTest extends BaseMockitoUnitTest {
+
+ @InjectMocks
+ private BpmProcessInstanceCopyServiceImpl copyService;
+
+ @Mock
+ private BpmProcessInstanceCopyMapper copyMapper;
+ @Mock
+ private BpmTaskService taskService;
+ @Mock
+ private BpmProcessInstanceService processInstanceService;
+ @Mock
+ private BpmProcessDefinitionService processDefinitionService;
+ @Mock
+ private Task task;
+ @Mock
+ private ProcessInstance processInstance;
+ @Mock
+ private ProcessDefinition processDefinition;
+
+ @Test
+ public void testCreateProcessInstanceCopy_task_success() {
+ // 准备参数
+ when(taskService.getTask("task-1")).thenReturn(task);
+ when(task.getProcessInstanceId()).thenReturn("process-instance-1");
+ when(task.getTaskDefinitionKey()).thenReturn("approve");
+ when(task.getName()).thenReturn("审批");
+ when(task.getId()).thenReturn("task-1");
+ prepareProcessInstance();
+
+ // 调用
+ copyService.createProcessInstanceCopy(List.of(2L, 3L), "请知悉", "task-1");
+ // 断言
+ ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class);
+ verify(copyMapper).insertBatch(captor.capture());
+ assertEquals(List.of(2L, 3L), captor.getValue().stream().map(BpmProcessInstanceCopyDO::getUserId).toList());
+ assertEquals("process-instance-1", captor.getValue().get(0).getProcessInstanceId());
+ assertEquals(1L, captor.getValue().get(0).getStartUserId());
+ }
+
+ @Test
+ public void testCreateProcessInstanceCopy_taskNotExists() {
+ // 准备参数
+ when(taskService.getTask("task-1")).thenReturn(null);
+
+ // 调用,并断言异常
+ assertServiceException(() -> copyService.createProcessInstanceCopy(List.of(2L), "请知悉", "task-1"),
+ TASK_NOT_EXISTS);
+ }
+
+ @Test
+ public void testCreateProcessInstanceCopy_processInstanceNotExists() {
+ // 准备参数
+ when(processInstanceService.getProcessInstance("process-instance-1")).thenReturn(null);
+
+ // 调用,并断言异常
+ assertServiceException(() -> copyService.createProcessInstanceCopy(List.of(2L), "请知悉",
+ "process-instance-1", "approve", "审批", "task-1"), PROCESS_INSTANCE_NOT_EXISTS);
+ }
+
+ @Test
+ public void testCreateProcessInstanceCopy_processDefinitionNotExists() {
+ // 准备参数
+ when(processInstanceService.getProcessInstance("process-instance-1")).thenReturn(processInstance);
+ when(processInstance.getProcessDefinitionId()).thenReturn("definition-1");
+ when(processDefinitionService.getProcessDefinition("definition-1")).thenReturn(null);
+
+ // 调用,并断言异常
+ assertServiceException(() -> copyService.createProcessInstanceCopy(List.of(2L), "请知悉",
+ "process-instance-1", "approve", "审批", "task-1"), PROCESS_DEFINITION_NOT_EXISTS);
+ }
+
+ private void prepareProcessInstance() {
+ when(processInstanceService.getProcessInstance("process-instance-1")).thenReturn(processInstance);
+ when(processInstance.getStartUserId()).thenReturn("1");
+ when(processInstance.getName()).thenReturn("请假申请");
+ when(processInstance.getProcessDefinitionId()).thenReturn("definition-1");
+ when(processDefinitionService.getProcessDefinition("definition-1")).thenReturn(processDefinition);
+ when(processDefinition.getCategory()).thenReturn("oa");
+ }
+
+}
diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImplTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImplTest.java
new file mode 100644
index 0000000..75a45ce
--- /dev/null
+++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImplTest.java
@@ -0,0 +1,542 @@
+package cn.iocoder.yudao.module.bpm.service.task;
+
+import cn.iocoder.yudao.framework.common.exception.ServiceException;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
+import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent;
+import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailRespVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCancelReqVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCreateReqVO;
+import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
+import cn.iocoder.yudao.module.bpm.dal.redis.BpmProcessIdRedisDAO;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum;
+import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.event.BpmProcessInstanceEventPublisher;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmHttpRequestUtils;
+import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
+import cn.iocoder.yudao.module.bpm.service.message.BpmMessageService;
+import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceApproveReqDTO;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import org.flowable.common.engine.impl.identity.Authentication;
+import org.flowable.engine.HistoryService;
+import org.flowable.engine.RuntimeService;
+import org.flowable.engine.repository.ProcessDefinition;
+import org.flowable.engine.runtime.Execution;
+import org.flowable.engine.runtime.ExecutionQuery;
+import org.flowable.engine.runtime.ProcessInstance;
+import org.flowable.engine.runtime.ProcessInstanceBuilder;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.springframework.transaction.support.TransactionSynchronization;
+import org.springframework.transaction.support.TransactionSynchronizationManager;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
+import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * {@link BpmProcessInstanceServiceImpl} 的单元测试
+ *
+ * @author HUIHUI
+ */
+public class BpmProcessInstanceServiceImplTest extends BaseMockitoUnitTest {
+
+ @InjectMocks
+ private BpmProcessInstanceServiceImpl processInstanceService;
+
+ @Mock
+ private RuntimeService runtimeService;
+ @Mock
+ private HistoryService historyService;
+ @Mock
+ private BpmProcessDefinitionService processDefinitionService;
+ @Mock
+ private BpmTaskService taskService;
+ @Mock
+ private BpmMessageService messageService;
+ @Mock
+ private AdminUserApi adminUserApi;
+ @Mock
+ private BpmProcessInstanceEventPublisher processInstanceEventPublisher;
+ @Mock
+ private BpmProcessIdRedisDAO processIdRedisDAO;
+ @Mock
+ private ProcessInstance processInstance;
+ @Mock
+ private ProcessDefinition processDefinition;
+
+ // ========== createProcessInstance 相关 ==========
+
+ @Test
+ public void testCreateProcessInstance_success() {
+ // 准备参数:动态表单必填字段已填写,开启流程编号规则、标题设置,并配置发起人自选审批人
+ BpmProcessInstanceServiceImpl spyService = spy(processInstanceService);
+ BpmModelMetaInfoVO.ProcessIdRule processIdRule = new BpmModelMetaInfoVO.ProcessIdRule().setEnable(true)
+ .setPrefix("QJ");
+ mockCreateProcessInstance(buildProcessDefinitionInfo()
+ .setFormType(BpmModelFormTypeEnum.NORMAL.getType())
+ .setFormFields(List.of(buildFormField("day", "请假天数", List.of("required"))))
+ .setProcessIdRule(processIdRule)
+ .setTitleSetting(new BpmModelMetaInfoVO.TitleSetting().setEnable(true)
+ .setTitle("{" + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_ID + "}的"
+ + "{" + BpmnVariableConstants.PROCESS_DEFINITION_NAME + "},请假{day}天")));
+ mockApprovalDetail(spyService, buildActivityNode("task1", "一级审批", BpmTaskCandidateStrategyEnum.USER),
+ buildActivityNode("task2", "二级审批", BpmTaskCandidateStrategyEnum.START_USER_SELECT));
+ when(adminUserApi.getUserMap(List.of(2L))).thenReturn(Map.of(2L, new AdminUserRespDTO().setId(2L)));
+ when(adminUserApi.getUser(1L)).thenReturn(new AdminUserRespDTO().setId(1L).setNickname("张三"));
+ when(processIdRedisDAO.generate(processIdRule)).thenReturn("QJ-001");
+ ProcessInstanceBuilder processInstanceBuilder = mockProcessInstanceBuilder();
+ Map> startUserSelectAssignees = Map.of("task2", List.of(2L));
+ BpmProcessInstanceCreateReqVO reqVO = new BpmProcessInstanceCreateReqVO().setProcessDefinitionId("definition-1")
+ .setVariables(new HashMap<>(Map.of("day", 3)))
+ .setStartUserSelectAssignees(startUserSelectAssignees);
+
+ // 调用
+ String processInstanceId = spyService.createProcessInstance(1L, reqVO);
+ // 断言
+ assertEquals("process-instance-1", processInstanceId);
+ verify(processInstanceBuilder).processDefinitionId("definition-1");
+ verify(processInstanceBuilder).businessKey(null);
+ verify(processInstanceBuilder).predefineProcessInstanceId("QJ-001");
+ verify(processInstanceBuilder).name("张三的请假,请假3天");
+ Map variables = captureVariables(processInstanceBuilder);
+ assertEquals(3, variables.get("day"));
+ assertEquals(1L, variables.get(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_ID));
+ assertEquals(BpmProcessInstanceStatusEnum.RUNNING.getStatus(),
+ variables.get(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS));
+ assertEquals(true, variables.get(BpmnVariableConstants.PROCESS_INSTANCE_SKIP_EXPRESSION_ENABLED));
+ assertEquals(startUserSelectAssignees,
+ variables.get(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES));
+ }
+
+ @Test
+ public void testCreateProcessInstance_dto() {
+ // 准备参数:未开启流程编号规则、标题设置
+ BpmProcessInstanceServiceImpl spyService = spy(processInstanceService);
+ when(processDefinitionService.getActiveProcessDefinition("leave")).thenReturn(processDefinition);
+ when(processDefinition.getId()).thenReturn("definition-1");
+ when(processDefinition.getName()).thenReturn("请假");
+ BpmProcessDefinitionInfoDO processDefinitionInfo = buildProcessDefinitionInfo();
+ when(processDefinitionService.getProcessDefinitionInfo("definition-1")).thenReturn(processDefinitionInfo);
+ when(processDefinitionService.canUserStartProcessDefinition(processDefinitionInfo, 1L)).thenReturn(true);
+ mockApprovalDetail(spyService);
+ ProcessInstanceBuilder processInstanceBuilder = mockProcessInstanceBuilder();
+ // 发起时,需要设置 Flowable 的认证用户,作为流程发起人
+ List authenticatedUserIds = new ArrayList<>();
+ when(processInstanceBuilder.start()).thenAnswer(invocation -> {
+ authenticatedUserIds.add(Authentication.getAuthenticatedUserId());
+ return processInstance;
+ });
+
+ // 调用
+ String processInstanceId = spyService.createProcessInstance(1L, new BpmProcessInstanceCreateReqDTO()
+ .setProcessDefinitionKey("leave").setBusinessKey("business-1"));
+ // 断言
+ assertEquals("process-instance-1", processInstanceId);
+ assertEquals(List.of("1"), authenticatedUserIds);
+ assertNull(Authentication.getAuthenticatedUserId());
+ verify(processInstanceBuilder).businessKey("business-1");
+ verify(processInstanceBuilder).name("请假");
+ verify(processInstanceBuilder, never()).predefineProcessInstanceId(anyString());
+ Map variables = captureVariables(processInstanceBuilder);
+ assertEquals(1L, variables.get(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_ID));
+ assertFalse(variables.containsKey(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES));
+ verifyNoInteractions(processIdRedisDAO);
+ }
+
+ @Test
+ public void testCreateProcessInstance_definitionNotExists() {
+ // 准备参数
+ BpmProcessInstanceCreateReqVO reqVO = new BpmProcessInstanceCreateReqVO().setProcessDefinitionId("definition-1");
+
+ // 调用,并断言异常
+ assertServiceException(() -> processInstanceService.createProcessInstance(1L, reqVO),
+ PROCESS_DEFINITION_NOT_EXISTS);
+ verifyNoInteractions(runtimeService);
+ }
+
+ @Test
+ public void testCreateProcessInstance_dtoDefinitionNotExists() {
+ // 调用
+ RuntimeException exception = assertThrows(RuntimeException.class, () -> processInstanceService
+ .createProcessInstance(1L, new BpmProcessInstanceCreateReqDTO().setProcessDefinitionKey("leave")));
+ // 断言:ServiceException 被 FlowableUtils 包装,由全局异常处理器解包;认证用户需要被清理
+ ServiceException serviceException = assertInstanceOf(ServiceException.class, exception.getCause());
+ assertEquals(PROCESS_DEFINITION_NOT_EXISTS.getCode(), serviceException.getCode());
+ assertNull(Authentication.getAuthenticatedUserId());
+ }
+
+ @Test
+ public void testCreateProcessInstance_definitionSuspended() {
+ // 准备参数
+ when(processDefinitionService.getProcessDefinition("definition-1")).thenReturn(processDefinition);
+ when(processDefinition.getId()).thenReturn("definition-1");
+ when(processDefinition.isSuspended()).thenReturn(true);
+
+ // 调用,并断言异常
+ assertServiceException(() -> processInstanceService.createProcessInstance(1L,
+ new BpmProcessInstanceCreateReqVO().setProcessDefinitionId("definition-1")), PROCESS_DEFINITION_IS_SUSPENDED);
+ }
+
+ @Test
+ public void testCreateProcessInstance_definitionInfoNotExists() {
+ // 准备参数
+ when(processDefinitionService.getProcessDefinition("definition-1")).thenReturn(processDefinition);
+ when(processDefinition.getId()).thenReturn("definition-1");
+
+ // 调用,并断言异常
+ assertServiceException(() -> processInstanceService.createProcessInstance(1L,
+ new BpmProcessInstanceCreateReqVO().setProcessDefinitionId("definition-1")), PROCESS_DEFINITION_NOT_EXISTS);
+ }
+
+ @Test
+ public void testCreateProcessInstance_userCannotStart() {
+ // 准备参数
+ when(processDefinitionService.getProcessDefinition("definition-1")).thenReturn(processDefinition);
+ when(processDefinition.getId()).thenReturn("definition-1");
+ when(processDefinitionService.getProcessDefinitionInfo("definition-1")).thenReturn(buildProcessDefinitionInfo());
+
+ // 调用,并断言异常
+ assertServiceException(() -> processInstanceService.createProcessInstance(1L,
+ new BpmProcessInstanceCreateReqVO().setProcessDefinitionId("definition-1")),
+ PROCESS_INSTANCE_START_USER_CAN_START);
+ verifyNoInteractions(runtimeService);
+ }
+
+ @Test
+ public void testCreateProcessInstance_formFieldRequired() {
+ // 准备参数:必填标记为数组 ["required"],字段未填写
+ when(processDefinitionService.getProcessDefinition("definition-1")).thenReturn(processDefinition);
+ when(processDefinition.getId()).thenReturn("definition-1");
+ when(processDefinitionService.getProcessDefinitionInfo("definition-1")).thenReturn(buildProcessDefinitionInfo()
+ .setFormType(BpmModelFormTypeEnum.NORMAL.getType())
+ .setFormFields(List.of(buildFormField("day", "请假天数", List.of("required")))));
+
+ // 调用,并断言异常
+ assertServiceException(() -> processInstanceService.createProcessInstance(1L,
+ new BpmProcessInstanceCreateReqVO().setProcessDefinitionId("definition-1")),
+ PROCESS_INSTANCE_START_FORM_FIELD_REQUIRED, "请假天数");
+ verifyNoInteractions(runtimeService);
+ }
+
+ @Test
+ public void testCreateProcessInstance_formFieldRequiredBlank() {
+ // 准备参数:字段标识为 key,必填标记为对象 {"required": true},字段值为空白字符串
+ when(processDefinitionService.getProcessDefinition("definition-1")).thenReturn(processDefinition);
+ when(processDefinition.getId()).thenReturn("definition-1");
+ String formField = JsonUtils.toJsonString(Map.of("key", "reason", "validate", Map.of("required", true)));
+ when(processDefinitionService.getProcessDefinitionInfo("definition-1")).thenReturn(buildProcessDefinitionInfo()
+ .setFormType(BpmModelFormTypeEnum.NORMAL.getType()).setFormFields(List.of(formField)));
+
+ // 调用,并断言异常:没有 title 时,使用字段标识提示
+ assertServiceException(() -> processInstanceService.createProcessInstance(1L,
+ new BpmProcessInstanceCreateReqVO().setProcessDefinitionId("definition-1")
+ .setVariables(new HashMap<>(Map.of("reason", " ")))),
+ PROCESS_INSTANCE_START_FORM_FIELD_REQUIRED, "reason");
+ }
+
+ @Test
+ public void testCreateProcessInstance_startUserSelectAssigneesNotConfig() {
+ // 准备参数:预测到发起人自选节点,但未选择审批人
+ BpmProcessInstanceServiceImpl spyService = spy(processInstanceService);
+ mockCreateProcessInstance(buildProcessDefinitionInfo());
+ mockApprovalDetail(spyService,
+ buildActivityNode("task2", "二级审批", BpmTaskCandidateStrategyEnum.START_USER_SELECT));
+
+ // 调用,并断言异常
+ assertServiceException(() -> spyService.createProcessInstance(1L,
+ new BpmProcessInstanceCreateReqVO().setProcessDefinitionId("definition-1")),
+ PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_CONFIG, "二级审批");
+ verifyNoInteractions(runtimeService);
+ }
+
+ @Test
+ public void testCreateProcessInstance_startUserSelectAssigneesNotExists() {
+ // 准备参数:选择的审批人不存在
+ BpmProcessInstanceServiceImpl spyService = spy(processInstanceService);
+ mockCreateProcessInstance(buildProcessDefinitionInfo());
+ mockApprovalDetail(spyService,
+ buildActivityNode("task2", "二级审批", BpmTaskCandidateStrategyEnum.START_USER_SELECT));
+ when(adminUserApi.getUserMap(List.of(2L))).thenReturn(Map.of());
+
+ // 调用,并断言异常
+ assertServiceException(() -> spyService.createProcessInstance(1L,
+ new BpmProcessInstanceCreateReqVO().setProcessDefinitionId("definition-1")
+ .setStartUserSelectAssignees(Map.of("task2", List.of(2L)))),
+ PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_EXISTS, "二级审批", 2L);
+ }
+
+ // ========== cancelProcessInstance 相关 ==========
+
+ @Test
+ public void testCancelProcessInstanceByAdmin_notExists() {
+ // 准备参数
+ BpmProcessInstanceServiceImpl spyService = spy(processInstanceService);
+ doReturn(null).when(spyService).getProcessInstance("process-instance-1");
+
+ // 调用,并断言异常
+ assertServiceException(() -> spyService.cancelProcessInstanceByAdmin(1L, buildCancelReq()),
+ PROCESS_INSTANCE_CANCEL_FAIL_NOT_EXISTS);
+ }
+
+ @Test
+ public void testCancelProcessInstanceByStartUser_notExists() {
+ // 准备参数
+ BpmProcessInstanceServiceImpl spyService = spy(processInstanceService);
+ doReturn(null).when(spyService).getProcessInstance("process-instance-1");
+ BpmProcessInstanceCancelReqVO reqVO = buildCancelReq();
+
+ // 调用,并断言异常
+ assertServiceException(() -> spyService.cancelProcessInstanceByStartUser(1L, reqVO),
+ PROCESS_INSTANCE_CANCEL_FAIL_NOT_EXISTS);
+ }
+
+ @Test
+ public void testCancelProcessInstanceByStartUser_notAllow() {
+ // 准备参数
+ BpmProcessInstanceServiceImpl spyService = spy(processInstanceService);
+ doReturn(processInstance).when(spyService).getProcessInstance("process-instance-1");
+ when(processInstance.getStartUserId()).thenReturn("1");
+ when(processInstance.getProcessDefinitionId()).thenReturn("definition-1");
+ when(processDefinitionService.getProcessDefinitionInfo("definition-1"))
+ .thenReturn(new BpmProcessDefinitionInfoDO().setAllowCancelRunningProcess(false));
+ BpmProcessInstanceCancelReqVO reqVO = buildCancelReq();
+
+ // 调用,并断言异常
+ assertServiceException(() -> spyService.cancelProcessInstanceByStartUser(1L, reqVO),
+ PROCESS_INSTANCE_CANCEL_FAIL_NOT_ALLOW);
+ }
+
+ // ========== Event 事件相关方法 ==========
+
+ @Test
+ public void testProcessProcessInstanceCompleted_approve() {
+ // 准备参数:流程结束时仍为审批中,说明审批通过;配置了流程后置通知
+ mockCompletedProcessInstance(BpmProcessInstanceStatusEnum.RUNNING.getStatus(), null);
+ BpmModelMetaInfoVO.HttpRequestSetting setting = new BpmModelMetaInfoVO.HttpRequestSetting()
+ .setUrl("http://127.0.0.1/after");
+ when(processDefinitionService.getProcessDefinitionInfo("definition-1"))
+ .thenReturn(new BpmProcessDefinitionInfoDO().setProcessAfterTriggerSetting(setting));
+
+ // 调用
+ try (MockedStatic httpRequestUtilsMockedStatic = mockStatic(BpmHttpRequestUtils.class)) {
+ processInstanceService.processProcessInstanceCompleted(processInstance);
+ // 断言:后置通知
+ httpRequestUtilsMockedStatic.verify(() -> BpmHttpRequestUtils.executeBpmHttpRequest(processInstance,
+ "http://127.0.0.1/after", null, null, true, null));
+ }
+ // 断言:状态更新为审批通过
+ verify(runtimeService).setVariable("process-instance-1", BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS,
+ BpmProcessInstanceStatusEnum.APPROVE.getStatus());
+ // 断言:审批通过消息
+ ArgumentCaptor messageCaptor =
+ ArgumentCaptor.forClass(BpmMessageSendWhenProcessInstanceApproveReqDTO.class);
+ verify(messageService).sendMessageWhenProcessInstanceApprove(messageCaptor.capture());
+ assertEquals("process-instance-1", messageCaptor.getValue().getProcessInstanceId());
+ assertEquals("请假流程", messageCaptor.getValue().getProcessInstanceName());
+ assertEquals(1L, messageCaptor.getValue().getStartUserId());
+ // 断言:状态事件
+ BpmProcessInstanceStatusEvent event = captureStatusEvent();
+ assertEquals("process-instance-1", event.getId());
+ assertEquals(BpmProcessInstanceStatusEnum.APPROVE.getStatus(), event.getStatus());
+ assertEquals("leave", event.getProcessDefinitionKey());
+ assertEquals("business-1", event.getBusinessKey());
+ }
+
+ @Test
+ public void testProcessProcessInstanceCompleted_childRejectRolledBack() {
+ // 准备参数
+ BpmProcessInstanceServiceImpl spyService = spy(processInstanceService);
+ ProcessInstance parentProcessInstance = mockChildRejectProcessInstance(spyService);
+
+ // 调用:事务回滚
+ executeWithSynchronization(() -> spyService.processProcessInstanceCompleted(processInstance),
+ TransactionSynchronization.STATUS_ROLLED_BACK);
+ // 断言:回滚时,不结束父流程
+ verify(spyService).updateProcessInstanceReject(parentProcessInstance,
+ BpmReasonEnum.REJECT_CHILD_PROCESS.getReason());
+ verifyNoInteractions(taskService);
+ }
+
+ @Test
+ public void testProcessProcessInstanceCreated_definitionNotExists() {
+ // 准备参数
+ when(processInstance.getProcessDefinitionId()).thenReturn("definition-1");
+
+ // 调用
+ executeWithSynchronization(() -> processInstanceService.processProcessInstanceCreated(processInstance),
+ TransactionSynchronization.STATUS_COMMITTED);
+ // 断言
+ verifyNoInteractions(runtimeService, adminUserApi);
+ }
+
+ @Test
+ public void testProcessProcessInstanceCreated_titleAndBeforeTrigger() {
+ // 准备参数:开启标题设置,并配置流程前置通知
+ BpmModelMetaInfoVO.HttpRequestSetting setting = new BpmModelMetaInfoVO.HttpRequestSetting()
+ .setUrl("http://127.0.0.1/before");
+ mockCreatedProcessInstance(buildProcessDefinitionInfo()
+ .setTitleSetting(new BpmModelMetaInfoVO.TitleSetting().setEnable(true)
+ .setTitle("{" + BpmnVariableConstants.PROCESS_DEFINITION_NAME + "}-{day}天"))
+ .setProcessBeforeTriggerSetting(setting));
+ when(processInstance.getName()).thenReturn("请假");
+
+ // 调用
+ try (MockedStatic httpRequestUtilsMockedStatic = mockStatic(BpmHttpRequestUtils.class)) {
+ executeWithSynchronization(() -> processInstanceService.processProcessInstanceCreated(processInstance),
+ TransactionSynchronization.STATUS_COMMITTED);
+ // 断言
+ verify(runtimeService).setProcessInstanceName("process-instance-1", "请假-3天");
+ httpRequestUtilsMockedStatic.verify(() -> BpmHttpRequestUtils.executeBpmHttpRequest(processInstance,
+ "http://127.0.0.1/before", null, null, true, null));
+ }
+ }
+
+ @Test
+ public void testProcessProcessInstanceCreated_nameNotChanged() {
+ // 准备参数:未开启标题设置,名称与流程定义一致
+ mockCreatedProcessInstance(buildProcessDefinitionInfo());
+ when(processInstance.getName()).thenReturn("请假");
+
+ // 调用
+ try (MockedStatic httpRequestUtilsMockedStatic = mockStatic(BpmHttpRequestUtils.class)) {
+ executeWithSynchronization(() -> processInstanceService.processProcessInstanceCreated(processInstance),
+ TransactionSynchronization.STATUS_COMMITTED);
+ // 断言:名称未变化时,不更新;未配置前置通知时,不发起请求
+ verify(runtimeService, never()).setProcessInstanceName(anyString(), anyString());
+ httpRequestUtilsMockedStatic.verifyNoInteractions();
+ }
+ }
+
+ // ========== 私有方法 ==========
+
+ private static BpmProcessInstanceCancelReqVO buildCancelReq() {
+ return new BpmProcessInstanceCancelReqVO().setId("process-instance-1").setReason("撤销申请");
+ }
+
+ private static BpmProcessDefinitionInfoDO buildProcessDefinitionInfo() {
+ return new BpmProcessDefinitionInfoDO().setProcessDefinitionId("definition-1");
+ }
+
+ private static String buildFormField(String field, String title, Object validate) {
+ return JsonUtils.toJsonString(Map.of("field", field, "title", title, "validate", validate));
+ }
+
+ private static BpmApprovalDetailRespVO.ActivityNode buildActivityNode(String id, String name,
+ BpmTaskCandidateStrategyEnum strategy) {
+ return new BpmApprovalDetailRespVO.ActivityNode().setId(id).setName(name)
+ .setCandidateStrategy(strategy.getStrategy());
+ }
+
+ private void mockCreateProcessInstance(BpmProcessDefinitionInfoDO processDefinitionInfo) {
+ when(processDefinitionService.getProcessDefinition("definition-1")).thenReturn(processDefinition);
+ when(processDefinition.getId()).thenReturn("definition-1");
+ lenient().when(processDefinition.getName()).thenReturn(" 请假 ");
+ when(processDefinitionService.getProcessDefinitionInfo("definition-1")).thenReturn(processDefinitionInfo);
+ when(processDefinitionService.canUserStartProcessDefinition(processDefinitionInfo, 1L)).thenReturn(true);
+ }
+
+ /**
+ * 模拟发起流程时的审批节点预测结果
+ */
+ private void mockApprovalDetail(BpmProcessInstanceServiceImpl spyService,
+ BpmApprovalDetailRespVO.ActivityNode... activityNodes) {
+ doReturn(new BpmApprovalDetailRespVO().setActivityNodes(new ArrayList<>(List.of(activityNodes))))
+ .when(spyService).getApprovalDetail(eq(1L), any());
+ }
+
+ private ProcessInstanceBuilder mockProcessInstanceBuilder() {
+ ProcessInstanceBuilder processInstanceBuilder = mock(ProcessInstanceBuilder.class, RETURNS_SELF);
+ when(runtimeService.createProcessInstanceBuilder()).thenReturn(processInstanceBuilder);
+ lenient().when(processInstanceBuilder.start()).thenReturn(processInstance);
+ when(processInstance.getId()).thenReturn("process-instance-1");
+ return processInstanceBuilder;
+ }
+
+ @SuppressWarnings("unchecked")
+ private static Map captureVariables(ProcessInstanceBuilder processInstanceBuilder) {
+ ArgumentCaptor