diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmParallelMultiInstanceBehavior.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmParallelMultiInstanceBehavior.java index b35f18e..fef02f4 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmParallelMultiInstanceBehavior.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmParallelMultiInstanceBehavior.java @@ -17,6 +17,7 @@ import org.flowable.engine.impl.bpmn.behavior.ParallelMultiInstanceBehavior; import org.flowable.common.engine.api.delegate.Expression; import java.util.List; +import java.util.Objects; import java.util.Set; /** @@ -37,6 +38,10 @@ public class BpmParallelMultiInstanceBehavior extends ParallelMultiInstanceBehav super(activity, innerActivityBehavior); // 关联 Pull Request:https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1483 // 在解析/构造阶段基于 activityId 初始化与 activity 绑定且不变的字段,避免在运行期修改 Behavior 实例状态 + // 注意:仅 UserTask 使用自定义变量;CallActivity 等节点,需要保留 Flowable 解析的 collection 配置(例如说,子流程多实例的表单字段) + if (!(activity instanceof UserTask)) { + return; + } super.collectionExpression = null; // collectionExpression 和 collectionVariable 是互斥的 super.collectionVariable = FlowableUtils.formatExecutionCollectionVariable(activity.getId()); // 从 execution.getVariable() 读取当前所有任务处理的人的 key @@ -77,10 +82,10 @@ public class BpmParallelMultiInstanceBehavior extends ParallelMultiInstanceBehav if (execution.getCurrentFlowElement() instanceof CallActivity) { FlowElement flowElement = execution.getCurrentFlowElement(); Integer sourceType = BpmnModelUtils.parseMultiInstanceSourceType(flowElement); - if (sourceType.equals(BpmChildProcessMultiInstanceSourceTypeEnum.NUMBER_FORM.getType())) { + if (Objects.equals(sourceType, BpmChildProcessMultiInstanceSourceTypeEnum.NUMBER_FORM.getType())) { return execution.getVariable(super.collectionExpression.getExpressionText(), Integer.class); } - if (sourceType.equals(BpmChildProcessMultiInstanceSourceTypeEnum.MULTIPLE_FORM.getType())) { + if (Objects.equals(sourceType, BpmChildProcessMultiInstanceSourceTypeEnum.MULTIPLE_FORM.getType())) { return execution.getVariable(super.collectionExpression.getExpressionText(), List.class).size(); } } @@ -92,17 +97,29 @@ public class BpmParallelMultiInstanceBehavior extends ParallelMultiInstanceBehav @Override public void setCollectionExpression(Expression collectionExpression) { - // 保持自定义变量名,忽略解析器写入的 collection 表达式 + // UserTask 保持自定义变量名,忽略解析器写入的 collection 表达式 + if (activity instanceof UserTask) { + return; + } + super.setCollectionExpression(collectionExpression); } @Override public void setCollectionVariable(String collectionVariable) { - // 保持自定义变量名,忽略解析器写入的 collection 变量名 + // UserTask 保持自定义变量名,忽略解析器写入的 collection 变量名 + if (activity instanceof UserTask) { + return; + } + super.setCollectionVariable(collectionVariable); } @Override public void setCollectionElementVariable(String collectionElementVariable) { - // 保持自定义变量名,忽略解析器写入的单元素变量名 + // UserTask 保持自定义变量名,忽略解析器写入的单元素变量名 + if (activity instanceof UserTask) { + return; + } + super.setCollectionElementVariable(collectionElementVariable); } } diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java index 65e6b29..fba536a 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java @@ -16,6 +16,7 @@ import org.flowable.engine.impl.persistence.entity.ExecutionEntity; import java.util.LinkedHashSet; import java.util.List; +import java.util.Objects; import java.util.Set; /** @@ -34,6 +35,10 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB super(activity, innerActivityBehavior); // 关联 Pull Request:https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1483 // 在解析/构造阶段基于 activityId 初始化与 activity 绑定且不变的字段,避免在运行期修改 Behavior 实例状态 + // 注意:仅 UserTask 使用自定义变量;CallActivity 等节点,需要保留 Flowable 解析的 collection 配置(例如说,子流程多实例的表单字段) + if (!(activity instanceof UserTask)) { + return; + } super.collectionExpression = null; // collectionExpression 和 collectionVariable 是互斥的 super.collectionVariable = FlowableUtils.formatExecutionCollectionVariable(activity.getId()); // 从 execution.getVariable() 读取当前所有任务处理的人的 key @@ -70,10 +75,10 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB if (execution.getCurrentFlowElement() instanceof CallActivity) { FlowElement flowElement = execution.getCurrentFlowElement(); Integer sourceType = BpmnModelUtils.parseMultiInstanceSourceType(flowElement); - if (sourceType.equals(BpmChildProcessMultiInstanceSourceTypeEnum.NUMBER_FORM.getType())) { + if (Objects.equals(sourceType, BpmChildProcessMultiInstanceSourceTypeEnum.NUMBER_FORM.getType())) { return execution.getVariable(super.collectionExpression.getExpressionText(), Integer.class); } - if (sourceType.equals(BpmChildProcessMultiInstanceSourceTypeEnum.MULTIPLE_FORM.getType())) { + if (Objects.equals(sourceType, BpmChildProcessMultiInstanceSourceTypeEnum.MULTIPLE_FORM.getType())) { return execution.getVariable(super.collectionExpression.getExpressionText(), List.class).size(); } } @@ -96,17 +101,29 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB @Override public void setCollectionExpression(Expression collectionExpression) { - // 保持自定义变量名,忽略解析器写入的 collection 表达式 + // UserTask 保持自定义变量名,忽略解析器写入的 collection 表达式 + if (activity instanceof UserTask) { + return; + } + super.setCollectionExpression(collectionExpression); } @Override public void setCollectionVariable(String collectionVariable) { - // 保持自定义变量名,忽略解析器写入的 collection 变量名 + // UserTask 保持自定义变量名,忽略解析器写入的 collection 变量名 + if (activity instanceof UserTask) { + return; + } + super.setCollectionVariable(collectionVariable); } @Override public void setCollectionElementVariable(String collectionElementVariable) { - // 保持自定义变量名,忽略解析器写入的单元素变量名 + // UserTask 保持自定义变量名,忽略解析器写入的单元素变量名 + if (activity instanceof UserTask) { + return; + } + super.setCollectionElementVariable(collectionElementVariable); } } diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java index 6e2e48c..f5111e2 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java @@ -116,7 +116,7 @@ public class BpmTaskEventListener extends AbstractFlowableEngineEventListener { log.error("[timerFired][解析 entity({}) elementId 为空,跳过处理]", entity); return; } - FlowElement element = BpmnModelUtils.getFlowElementById(bpmnModel, entity.getElementId()); + FlowElement element = BpmnModelUtils.getFlowElementById(bpmnModel, elementId); if (!(element instanceof BoundaryEvent)) { return; } diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmHttpRequestUtils.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmHttpRequestUtils.java index 358ee9f..024bb1b 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmHttpRequestUtils.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmHttpRequestUtils.java @@ -89,7 +89,7 @@ public class BpmHttpRequestUtils { BpmProcessInstanceService processInstanceService = SpringUtils.getBean(BpmProcessInstanceService.class); ProcessInstance processInstance = processInstanceService.getProcessInstance(event.getId()); if (processInstance != null) { - headers.add(HEADER_TENANT_ID, String.valueOf(TenantContextHolder.getTenantId())); + headers.add(HEADER_TENANT_ID, processInstance.getTenantId()); } } // 1.2 设置请求体 diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java index 9cb44a7..3d88db8 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java @@ -876,11 +876,11 @@ public class SimpleModelUtils { if (childProcessSetting.getTimeoutSetting() != null && Boolean.TRUE.equals(childProcessSetting.getTimeoutSetting().getEnable())) { BoundaryEvent boundaryEvent = null; if (childProcessSetting.getTimeoutSetting().getType().equals(BpmDelayTimerTypeEnum.FIXED_DATE_TIME.getType())) { - boundaryEvent = buildTimeoutBoundaryEvent(callActivity, BpmBoundaryEventTypeEnum.DELAY_TIMER_TIMEOUT.getType(), - childProcessSetting.getTimeoutSetting().getTimeExpression(), null, null); - } else if (childProcessSetting.getTimeoutSetting().getType().equals(BpmDelayTimerTypeEnum.FIXED_TIME_DURATION.getType())) { boundaryEvent = buildTimeoutBoundaryEvent(callActivity, BpmBoundaryEventTypeEnum.CHILD_PROCESS_TIMEOUT.getType(), null, null, childProcessSetting.getTimeoutSetting().getTimeExpression()); + } else if (childProcessSetting.getTimeoutSetting().getType().equals(BpmDelayTimerTypeEnum.FIXED_TIME_DURATION.getType())) { + boundaryEvent = buildTimeoutBoundaryEvent(callActivity, BpmBoundaryEventTypeEnum.CHILD_PROCESS_TIMEOUT.getType(), + childProcessSetting.getTimeoutSetting().getTimeExpression(), null, null); } flowElements.add(boundaryEvent); } @@ -931,7 +931,7 @@ public class SimpleModelUtils { if (ObjUtil.isNotNull(timeDuration)) { eventDefinition.setTimeDuration(timeDuration); } - if (ObjUtil.isNotNull(timeDuration)) { + if (ObjUtil.isNotNull(timeCycle)) { eventDefinition.setTimeCycle(timeCycle); } if (ObjUtil.isNotNull(timeDate)) { diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java index 7151851..0b0e666 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java @@ -210,7 +210,7 @@ public class BpmModelServiceImpl implements BpmModelService { // 保存排序 long sort = System.currentTimeMillis(); // 使用时间戳 - i 作为排序 - for (int i = ids.size() - 1; i > 0; i--) { + for (int i = ids.size() - 1; i >= 0; i--) { Model model = modelMap.get(ids.get(i)); // 更新模型 BpmModelMetaInfoVO metaInfo = BpmModelConvert.INSTANCE.parseMetaInfo(model).setSort(sort); diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java index 836b518..a83620d 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java @@ -57,7 +57,12 @@ public class BpmProcessDefinitionServiceImpl implements BpmProcessDefinitionServ @Override public ProcessDefinition getProcessDefinition(String id) { - return repositoryService.getProcessDefinition(id); + // 不使用 repositoryService.getProcessDefinition(id) 的原因:流程定义不存在时,它会抛出 FlowableObjectNotFoundException, + // 而调用方都是通过返回 null 判断流程定义不存在 + if (StrUtil.isEmpty(id)) { + return null; + } + return repositoryService.createProcessDefinitionQuery().processDefinitionId(id).singleResult(); } @Override diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java index b11a372..9fed30f 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java @@ -198,6 +198,9 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService ProcessDefinition processDefinition = processDefinitionService.getProcessDefinition( historicProcessInstance != null ? historicProcessInstance.getProcessDefinitionId() : reqVO.getProcessDefinitionId()); + if (processDefinition == null) { + throw exception(ErrorCodeConstants.PROCESS_DEFINITION_NOT_EXISTS); + } BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService .getProcessDefinitionInfo(processDefinition.getId()); BpmnModel bpmnModel = processDefinitionService.getProcessDefinitionBpmnModel(processDefinition.getId()); @@ -777,6 +780,10 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService * @see 相关 issue */ private void validateDynamicFormRequiredFields(ProcessDefinition definition, Map variables) { + // 流程定义不存在时,交给 createProcessInstance0 统一校验,避免空指针 + if (definition == null) { + return; + } BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService .getProcessDefinitionInfo(definition.getId()); if (processDefinitionInfo == null diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java index 6d359d0..87abd54 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java @@ -1066,7 +1066,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { String taskId = reqVO.getId(); // 1.1 校验任务 Task task = validateTask(userId, reqVO.getId()); - if (task.getAssignee().equals(reqVO.getDelegateUserId().toString())) { // 校验当前审批人和被委派人不是同一人 + if (StrUtil.equals(task.getAssignee(), reqVO.getDelegateUserId().toString())) { // 校验当前审批人和被委派人不是同一人 throw exception(TASK_DELEGATE_FAIL_USER_REPEAT); } // 1.2 校验目标用户存在 @@ -1097,7 +1097,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { String taskId = reqVO.getId(); // 1.1 校验任务 Task task = validateTask(userId, reqVO.getId()); - if (task.getAssignee().equals(reqVO.getAssigneeUserId().toString())) { // 校验当前审批人和被转派人不是同一人 + if (StrUtil.equals(task.getAssignee(), reqVO.getAssigneeUserId().toString())) { // 校验当前审批人和被转派人不是同一人 throw exception(TASK_TRANSFER_FAIL_USER_REPEAT); } // 1.2 校验目标用户存在 diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmUserTaskListener.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmUserTaskListener.java index c1f6d9c..b5983c0 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmUserTaskListener.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmUserTaskListener.java @@ -14,6 +14,9 @@ import org.flowable.task.service.delegate.DelegateTask; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; +import java.util.ArrayList; +import java.util.List; + import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.parseListenerConfig; // TODO @芋艿:可能会想换个包地址 @@ -43,17 +46,19 @@ public class BpmUserTaskListener implements TaskListener { // 2. 发起请求 // TODO @芋艿:哪些默认参数,后续再调研下;感觉可以搞个 task 字段,把整个 delegateTask 放进去; - listenerHandler.getBody().add(new BpmSimpleModelNodeVO.HttpRequestParam().setKey("processInstanceId") + List body = listenerHandler.getBody() == null + ? new ArrayList<>() : new ArrayList<>(listenerHandler.getBody()); + body.add(new BpmSimpleModelNodeVO.HttpRequestParam().setKey("processInstanceId") .setType(BpmHttpRequestParamTypeEnum.FIXED_VALUE.getType()).setValue(delegateTask.getProcessInstanceId())); - listenerHandler.getBody().add(new BpmSimpleModelNodeVO.HttpRequestParam().setKey("assignee") + body.add(new BpmSimpleModelNodeVO.HttpRequestParam().setKey("assignee") .setType(BpmHttpRequestParamTypeEnum.FIXED_VALUE.getType()).setValue(delegateTask.getAssignee())); - listenerHandler.getBody().add(new BpmSimpleModelNodeVO.HttpRequestParam().setKey("taskDefinitionKey") + body.add(new BpmSimpleModelNodeVO.HttpRequestParam().setKey("taskDefinitionKey") .setType(BpmHttpRequestParamTypeEnum.FIXED_VALUE.getType()).setValue(delegateTask.getTaskDefinitionKey())); - listenerHandler.getBody().add(new BpmSimpleModelNodeVO.HttpRequestParam().setKey("taskId") + body.add(new BpmSimpleModelNodeVO.HttpRequestParam().setKey("taskId") .setType(BpmHttpRequestParamTypeEnum.FIXED_VALUE.getType()).setValue(delegateTask.getId())); BpmHttpRequestUtils.executeBpmHttpRequest(processInstance, - listenerHandler.getPath(), listenerHandler.getHeader(), listenerHandler.getBody(), false, null); + listenerHandler.getPath(), listenerHandler.getHeader(), body, false, null); // 3. 是否需要后续操作?TODO 芋艿:待定! } -} \ No newline at end of file +} diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/http/BpmHttpCallbackTrigger.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/http/BpmHttpCallbackTrigger.java index d2cc03e..36ed508 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/http/BpmHttpCallbackTrigger.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/http/BpmHttpCallbackTrigger.java @@ -12,6 +12,9 @@ import org.flowable.engine.runtime.ProcessInstance; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; +import java.util.ArrayList; +import java.util.List; + /** * BPM HTTP 回调触发器 * @@ -41,11 +44,13 @@ public class BpmHttpCallbackTrigger extends BpmAbstractHttpRequestTrigger { // 2. 发起请求 ProcessInstance processInstance = processInstanceService.getProcessInstance(processInstanceId); - setting.getBody().add(new BpmSimpleModelNodeVO.HttpRequestParam() + List 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 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> variablesCaptor = ArgumentCaptor.forClass(Map.class); + verify(processInstanceBuilder).variables(variablesCaptor.capture()); + return variablesCaptor.getValue(); + } + + private void mockCompletedProcessInstance(Integer status, String reason) { + Map variables = new HashMap<>(); + variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS, status); + variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_REASON, reason); + when(processInstance.getProcessVariables()).thenReturn(variables); + lenient().when(processInstance.getId()).thenReturn("process-instance-1"); + lenient().when(processInstance.getName()).thenReturn("请假流程"); + lenient().when(processInstance.getStartUserId()).thenReturn("1"); + lenient().when(processInstance.getProcessDefinitionId()).thenReturn("definition-1"); + lenient().when(processInstance.getProcessDefinitionKey()).thenReturn("leave"); + lenient().when(processInstance.getBusinessKey()).thenReturn("business-1"); + } + + private ProcessInstance mockChildRejectProcessInstance(BpmProcessInstanceServiceImpl spyService) { + mockCompletedProcessInstance(BpmProcessInstanceStatusEnum.REJECT.getStatus(), "不同意"); + when(processInstance.getSuperExecutionId()).thenReturn("super-execution-1"); + ExecutionQuery executionQuery = mock(ExecutionQuery.class, RETURNS_SELF); + when(runtimeService.createExecutionQuery()).thenReturn(executionQuery); + Execution execution = mock(Execution.class); + when(execution.getProcessInstanceId()).thenReturn("parent-process-instance-1"); + when(executionQuery.singleResult()).thenReturn(execution); + ProcessInstance parentProcessInstance = mock(ProcessInstance.class); + lenient().when(parentProcessInstance.getId()).thenReturn("parent-process-instance-1"); + when(parentProcessInstance.getProcessInstanceId()).thenReturn("parent-process-instance-1"); + doReturn(parentProcessInstance).when(spyService).getProcessInstance("parent-process-instance-1"); + return parentProcessInstance; + } + + private void mockCreatedProcessInstance(BpmProcessDefinitionInfoDO processDefinitionInfo) { + when(processInstance.getProcessDefinitionId()).thenReturn("definition-1"); + lenient().when(processInstance.getProcessInstanceId()).thenReturn("process-instance-1"); + when(processInstance.getStartUserId()).thenReturn("1"); + lenient().when(processInstance.getProcessVariables()).thenReturn(Map.of("day", 3)); + when(processDefinitionService.getProcessDefinitionInfo("definition-1")).thenReturn(processDefinitionInfo); + when(processDefinitionService.getProcessDefinition("definition-1")).thenReturn(processDefinition); + when(processDefinition.getName()).thenReturn("请假"); + lenient().when(adminUserApi.getUser(1L)).thenReturn(new AdminUserRespDTO().setId(1L).setNickname("张三")); + } + + private BpmProcessInstanceStatusEvent captureStatusEvent() { + ArgumentCaptor eventCaptor = + ArgumentCaptor.forClass(BpmProcessInstanceStatusEvent.class); + verify(processInstanceEventPublisher).sendProcessInstanceResultEvent(eventCaptor.capture()); + return eventCaptor.getValue(); + } + + /** + * 在事务同步上下文中执行,并模拟事务提交(或回滚)回调 + */ + private static void executeWithSynchronization(Runnable runnable, int transactionStatus) { + TransactionSynchronizationManager.initSynchronization(); + try { + runnable.run(); + List synchronizations = TransactionSynchronizationManager.getSynchronizations(); + if (transactionStatus == TransactionSynchronization.STATUS_COMMITTED) { + synchronizations.forEach(TransactionSynchronization::afterCommit); + } + synchronizations.forEach(synchronization -> synchronization.afterCompletion(transactionStatus)); + } finally { + TransactionSynchronizationManager.clearSynchronization(); + } + } + +} diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImplTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImplTest.java new file mode 100644 index 0000000..cab1b9d --- /dev/null +++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImplTest.java @@ -0,0 +1,953 @@ +package cn.iocoder.yudao.module.bpm.service.task; + +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.extra.spring.SpringUtil; +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.task.vo.task.*; +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; +import cn.iocoder.yudao.module.bpm.enums.definition.*; +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.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.BpmTaskCandidateStrategyEnum; +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.BpmHttpRequestUtils; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; +import cn.iocoder.yudao.module.bpm.service.comment.BpmCommentService; +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.message.dto.BpmMessageSendWhenTaskCreatedReqDTO; +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 org.flowable.bpmn.model.BpmnModel; +import org.flowable.bpmn.model.FlowElement; +import org.flowable.bpmn.model.Process; +import org.flowable.bpmn.model.UserTask; +import org.flowable.engine.HistoryService; +import org.flowable.engine.RuntimeService; +import org.flowable.engine.TaskService; +import org.flowable.engine.impl.persistence.entity.AttachmentEntityImpl; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.engine.task.Attachment; +import org.flowable.task.api.Task; +import org.flowable.task.api.TaskQuery; +import org.flowable.task.api.history.HistoricTaskInstance; +import org.flowable.task.api.history.HistoricTaskInstanceQuery; +import org.flowable.task.service.impl.persistence.entity.TaskEntityImpl; +import org.junit.jupiter.api.AfterEach; +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 org.mockito.MockedStatic; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +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} 的单元测试 + * + * @author HUIHUI + */ +public class BpmTaskServiceImplTest extends BaseMockitoUnitTest { + + @InjectMocks + private BpmTaskServiceImpl taskService; + + @Mock + private TaskService flowableTaskService; + @Mock + private Task task; + @Mock + private HistoryService historyService; + @Mock + private RuntimeService runtimeService; + @Mock + private BpmProcessInstanceService processInstanceService; + @Mock + private BpmProcessDefinitionService processDefinitionService; + @Mock + private BpmProcessInstanceCopyService processInstanceCopyService; + @Mock + private BpmCommentService commentService; + @Mock + private BpmModelService modelService; + @Mock + private BpmMessageService messageService; + @Mock + private AdminUserApi adminUserApi; + @Mock + private DeptApi deptApi; + @Mock + private ProcessInstance processInstance; + + /** + * 自身代理对象,用于验证 getSelf() 发起的自动审批、自动拒绝、转办 + */ + @Mock + private BpmTaskServiceImpl selfService; + + private MockedStatic springUtilMockedStatic; + + @BeforeEach + public void setUp() { + springUtilMockedStatic = mockStatic(SpringUtil.class); + springUtilMockedStatic.when(() -> SpringUtil.getBean(any(Class.class))).thenReturn(selfService); + } + + @AfterEach + public void tearDown() { + springUtilMockedStatic.close(); + } + + @Test + public void testValidateTask_success() { + // 准备参数:通过 spy 绕过 Flowable 查询,只验证负责人校验 + BpmTaskServiceImpl spyService = spy(taskService); + doReturn(task).when(spyService).validateTaskExists("task-1"); + when(task.getAssignee()).thenReturn("1"); + + // 调用 + Task result = spyService.validateTask(1L, "task-1"); + // 断言 + assertSame(task, result); + } + + @Test + public void testValidateTask_assigneeNotSelf() { + // 准备参数 + BpmTaskServiceImpl spyService = spy(taskService); + doReturn(task).when(spyService).validateTaskExists("task-1"); + when(task.getAssignee()).thenReturn("2"); + + // 调用,并断言异常 + assertServiceException(() -> spyService.validateTask(1L, "task-1"), TASK_OPERATE_FAIL_ASSIGN_NOT_SELF); + } + + @Test + public void testValidateTask_noAssignee_success() { + // 准备参数 + BpmTaskServiceImpl spyService = spy(taskService); + doReturn(task).when(spyService).validateTaskExists("task-1"); + when(task.getAssignee()).thenReturn(null); + + // 调用 + Task result = spyService.validateTask(null, "task-1"); + // 断言 + assertSame(task, result); + } + + @Test + public void testValidateTaskExists_notExists() { + // 准备参数 + BpmTaskServiceImpl spyService = spy(taskService); + doReturn(null).when(spyService).getTask("task-1"); + + // 调用,并断言异常 + assertServiceException(() -> spyService.validateTaskExists("task-1"), TASK_NOT_EXISTS); + } + + // ========== Query 查询相关方法 ========== + + @Test + public void testGetAllChildrenTaskListByParentTaskId_success() { + // 准备参数:parent -> child1 -> child2;other 属于其它父任务 + TaskEntityImpl parent = buildTaskEntity("parent", null); + TaskEntityImpl child1 = buildTaskEntity("child1", "parent"); + TaskEntityImpl child2 = buildTaskEntity("child2", "child1"); + TaskEntityImpl other = buildTaskEntity("other", "unknown"); + + // 调用 + List result = taskService.getAllChildrenTaskListByParentTaskId("parent", + List.of(parent, child1, child2, other)); + // 断言 + assertEquals(List.of("child1", "child2"), result.stream().map(TaskEntityImpl::getId).toList()); + assertTrue(taskService.getAllChildrenTaskListByParentTaskId("parent", List.of(parent)).isEmpty()); + assertTrue(taskService.getAllChildrenTaskListByParentTaskId("parent", List.of()).isEmpty()); + } + + @Test + public void testGetAttachments_filter() { + // 准备参数 + Attachment attachment1 = buildAttachment("task-1", BpmAttachmentTypeEnum.TASK_ATTACHMENT.getType()); + Attachment attachment2 = buildAttachment("task-2", BpmAttachmentTypeEnum.TASK_ATTACHMENT.getType()); + Attachment attachment3 = buildAttachment("task-1", "other"); + when(flowableTaskService.getProcessInstanceAttachments("process-instance-1")) + .thenReturn(List.of(attachment1, attachment2, attachment3)); + + // 调用 + List result = taskService.getAttachments("process-instance-1", Set.of("task-1"), + BpmAttachmentTypeEnum.TASK_ATTACHMENT); + // 断言 + assertEquals(List.of(attachment1), result); + assertEquals(3, taskService.getAttachments("process-instance-1", null, null).size()); + } + + // ========== Update 写入相关方法 ========== + + @Test + public void testApproveTask_processInstanceNotExists() { + // 准备参数 + BpmTaskServiceImpl spyService = spy(taskService); + doReturn(task).when(spyService).validateTask(1L, "task-1"); + when(task.getProcessInstanceId()).thenReturn("process-instance-1"); + + // 调用,并断言异常 + assertServiceException(() -> spyService.approveTask(1L, new BpmTaskApproveReqVO().setId("task-1")), + PROCESS_INSTANCE_NOT_EXISTS); + } + + @Test + public void testApproveTask_signatureNotExists() { + // 准备参数 + BpmTaskServiceImpl spyService = spy(taskService); + mockApproveTask(spyService, BpmnModelConstants.SIGN_ENABLE); + + // 调用,并断言异常 + assertServiceException(() -> spyService.approveTask(1L, new BpmTaskApproveReqVO().setId("task-1") + .setReason("同意")), TASK_SIGNATURE_NOT_EXISTS); + } + + @Test + public void testApproveTask_reasonRequire() { + // 准备参数 + BpmTaskServiceImpl spyService = spy(taskService); + mockApproveTask(spyService, BpmnModelConstants.REASON_REQUIRE); + + // 调用,并断言异常 + assertServiceException(() -> spyService.approveTask(1L, new BpmTaskApproveReqVO().setId("task-1")), + TASK_REASON_REQUIRE); + } + + @Test + public void testApproveTask_success() { + // 准备参数:task1 开启签名,下一个节点 task2 为审批人自选;task1 曾被退回过 + BpmTaskServiceImpl spyService = spy(taskService); + BpmnModel bpmnModel = mockApproveTaskSuccess(spyService, BpmTaskCandidateStrategyEnum.APPROVE_USER_SELECT, + Map.of("reason", "事假")); + BpmnModelUtils.addExtensionElement(bpmnModel.getFlowElement("task1"), BpmnModelConstants.SIGN_ENABLE, "true"); + when(runtimeService.getVariable("process-instance-1", + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_TASK_IDS)) + .thenReturn(new HashSet<>(Set.of("task1", "task2"))); + String returnFlagKey = String.format(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, "task1"); + when(runtimeService.hasVariable("process-instance-1", returnFlagKey)).thenReturn(true); + BpmTaskApproveReqVO reqVO = new BpmTaskApproveReqVO().setId("task-1").setReason("同意") + .setSignPicUrl("https://www.iocoder.cn/sign.png") + .setAttachments(List.of("https://www.iocoder.cn/files/a.pdf")) + .setVariables(Map.of("day", 3)) + .setNextAssignees(Map.of("task2", List.of(2L))); + + // 调用 + spyService.approveTask(1L, reqVO); + // 断言:任务状态、签名、评论、附件 + verify(flowableTaskService).setVariableLocal("task-1", BpmnVariableConstants.TASK_VARIABLE_STATUS, + BpmTaskStatusEnum.APPROVE.getStatus()); + verify(flowableTaskService).setVariableLocal("task-1", BpmnVariableConstants.TASK_VARIABLE_REASON, "同意"); + verify(flowableTaskService).setVariableLocal("task-1", BpmnVariableConstants.TASK_SIGN_PIC_URL, + "https://www.iocoder.cn/sign.png"); + verify(commentService).createComment("task-1", "process-instance-1", BpmCommentTypeEnum.APPROVE, "同意"); + verify(flowableTaskService).createAttachment(BpmAttachmentTypeEnum.TASK_ATTACHMENT.getType(), "task-1", + "process-instance-1", "a.pdf", null, "https://www.iocoder.cn/files/a.pdf"); + // 断言:流程变量合并历史变量与前端变量,并记录下一个节点的审批人 + Map processVariables = captureProcessVariables(); + assertEquals("事假", processVariables.get("reason")); + assertEquals(3, processVariables.get("day")); + assertEquals(Map.of("task2", List.of(2L)), processVariables.get( + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_APPROVE_USER_SELECT_ASSIGNEES)); + // 断言:移除预测节点、清理退回标记,并完成任务 + verify(runtimeService).setVariable("process-instance-1", + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_TASK_IDS, Set.of("task2")); + verify(runtimeService).removeVariable("process-instance-1", returnFlagKey); + verify(flowableTaskService).complete("task-1", Map.of("day", 3), true); + } + + @Test + public void testApproveTask_approveUserSelectNotConfig() { + // 准备参数:下一个节点 task2 为审批人自选,但未选择审批人 + BpmTaskServiceImpl spyService = spy(taskService); + mockApproveTaskSuccess(spyService, BpmTaskCandidateStrategyEnum.APPROVE_USER_SELECT, Map.of()); + + // 调用,并断言异常 + assertServiceException(() -> spyService.approveTask(1L, new BpmTaskApproveReqVO().setId("task-1") + .setReason("同意")), PROCESS_INSTANCE_APPROVE_USER_SELECT_ASSIGNEES_NOT_CONFIG, "二级审批"); + verify(flowableTaskService, never()).complete(anyString(), anyMap(), anyBoolean()); + } + + @Test + public void testApproveTask_startUserSelectNotConfig() { + // 准备参数:下一个节点 task2 为发起人自选,但发起时、审批时都未选择审批人 + BpmTaskServiceImpl spyService = spy(taskService); + mockApproveTaskSuccess(spyService, BpmTaskCandidateStrategyEnum.START_USER_SELECT, Map.of()); + + // 调用,并断言异常 + assertServiceException(() -> spyService.approveTask(1L, new BpmTaskApproveReqVO().setId("task-1") + .setReason("同意")), PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_CONFIG, "二级审批"); + } + + @Test + public void testApproveTask_startUserSelectAlreadyConfig() { + // 准备参数:下一个节点 task2 为发起人自选,发起时已经选择审批人 + BpmTaskServiceImpl spyService = spy(taskService); + Map> startUserSelectAssignees = Map.of("task2", List.of(2L)); + mockApproveTaskSuccess(spyService, BpmTaskCandidateStrategyEnum.START_USER_SELECT, Map.of( + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES, startUserSelectAssignees)); + + // 调用:审批时传入的审批人,不允许覆盖发起时选择的审批人 + spyService.approveTask(1L, new BpmTaskApproveReqVO().setId("task-1").setReason("同意") + .setNextAssignees(Map.of("task2", List.of(3L)))); + // 断言 + assertEquals(startUserSelectAssignees, captureProcessVariables().get( + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES)); + verify(flowableTaskService).complete("task-1", Map.of(), true); + verify(runtimeService, never()).removeVariable(anyString(), anyString()); + } + + @Test + public void testMoveTaskToEnd_noRunningTask() { + // 准备参数 + BpmTaskServiceImpl spyService = spy(taskService); + doReturn(List.of()).when(spyService).getRunningTaskListByProcessInstanceId("process-instance-1", null, null); + + // 调用 + spyService.moveTaskToEnd("process-instance-1", "结束流程"); + // 断言 + verifyNoInteractions(runtimeService, modelService); + } + + @Test + public void testRejectTask_processInstanceNotExists() { + // 准备参数 + BpmTaskServiceImpl spyService = spy(taskService); + doReturn(task).when(spyService).validateTask(1L, "task-1"); + when(task.getProcessInstanceId()).thenReturn("process-instance-1"); + + // 调用,并断言异常 + assertServiceException(() -> spyService.rejectTask(1L, new BpmTaskRejectReqVO().setId("task-1") + .setReason("不同意")), PROCESS_INSTANCE_NOT_EXISTS); + } + + @Test + public void testReturnTask_pending() { + // 准备参数 + BpmTaskServiceImpl spyService = spy(taskService); + doReturn(task).when(spyService).validateTask(1L, "task-1"); + when(task.isSuspended()).thenReturn(true); + + // 调用,并断言异常 + assertServiceException(() -> spyService.returnTask(1L, new BpmTaskReturnReqVO().setId("task-1") + .setTargetTaskDefinitionKey("task1").setReason("退回")), TASK_IS_PENDING); + } + + @Test + public void testDelegateTask_userNotExists() { + // 准备参数 + BpmTaskServiceImpl spyService = spy(taskService); + doReturn(task).when(spyService).validateTask(1L, "task-1"); + when(task.getAssignee()).thenReturn("1"); + + // 调用,并断言异常 + assertServiceException(() -> spyService.delegateTask(1L, new BpmTaskDelegateReqVO().setId("task-1") + .setDelegateUserId(2L).setReason("委派")), TASK_DELEGATE_FAIL_USER_NOT_EXISTS); + } + + @Test + public void testDelegateTask_emptyAssignee_userNotExists() { + // 准备参数:自动审批或审批人为空时,任务可能没有 assignee + BpmTaskServiceImpl spyService = spy(taskService); + doReturn(task).when(spyService).validateTask(1L, "task-1"); + when(task.getAssignee()).thenReturn(null); + + // 调用,并断言:不应因 assignee 为空触发空指针 + assertServiceException(() -> spyService.delegateTask(1L, new BpmTaskDelegateReqVO().setId("task-1") + .setDelegateUserId(2L).setReason("委派")), TASK_DELEGATE_FAIL_USER_NOT_EXISTS); + } + + @Test + public void testTransferTask_userRepeat() { + // 准备参数 + BpmTaskServiceImpl spyService = spy(taskService); + doReturn(task).when(spyService).validateTask(1L, "task-1"); + when(task.getAssignee()).thenReturn("1"); + + // 调用,并断言异常 + assertServiceException(() -> spyService.transferTask(1L, new BpmTaskTransferReqVO().setId("task-1") + .setAssigneeUserId(1L).setReason("转办")), TASK_TRANSFER_FAIL_USER_REPEAT); + } + + @Test + public void testTransferTask_userNotExists() { + // 准备参数 + BpmTaskServiceImpl spyService = spy(taskService); + doReturn(task).when(spyService).validateTask(1L, "task-1"); + when(task.getAssignee()).thenReturn("1"); + + // 调用,并断言异常 + assertServiceException(() -> spyService.transferTask(1L, new BpmTaskTransferReqVO().setId("task-1") + .setAssigneeUserId(2L).setReason("转办")), TASK_TRANSFER_FAIL_USER_NOT_EXISTS); + } + + @Test + public void testTransferTask_alreadyHasOwner() { + // 准备参数 + BpmTaskServiceImpl spyService = spy(taskService); + doReturn(task).when(spyService).validateTask(1L, "task-1"); + when(task.getAssignee()).thenReturn("1"); + when(task.getOwner()).thenReturn("3"); + when(task.getProcessInstanceId()).thenReturn("process-instance-1"); + when(adminUserApi.getUser(2L)).thenReturn(new AdminUserRespDTO().setId(2L).setNickname("李四")); + when(adminUserApi.getUser(1L)).thenReturn(new AdminUserRespDTO().setId(1L).setNickname("张三")); + + // 调用 + spyService.transferTask(1L, new BpmTaskTransferReqVO().setId("task-1").setAssigneeUserId(2L).setReason("转办")); + // 断言:已经存在 owner 时,不覆盖 owner + verify(flowableTaskService, never()).setOwner(anyString(), anyString()); + verify(flowableTaskService).setAssignee("task-1", "2"); + verify(commentService).createComment(eq("task-1"), eq("process-instance-1"), + eq(BpmCommentTypeEnum.TRANSFER), eq("张三"), eq("李四"), eq("转办")); + } + + @Test + public void testCreateSignTask_userNotExists() { + // 准备参数 + BpmTaskServiceImpl spyService = spy(taskService); + TaskEntityImpl taskEntity = buildTaskEntity("task-1", null); + taskEntity.setProcessInstanceId("process-instance-1"); + taskEntity.setTaskDefinitionKey("task1"); + doReturn(taskEntity).when(spyService).validateTask(1L, "task-1"); + TaskQuery taskQuery = mock(TaskQuery.class, RETURNS_SELF); + when(flowableTaskService.createTaskQuery()).thenReturn(taskQuery); + when(taskQuery.list()).thenReturn(List.of()); + + // 调用,并断言异常 + assertServiceException(() -> spyService.createSignTask(1L, new BpmTaskSignCreateReqVO().setId("task-1") + .setType(BpmTaskSignTypeEnum.BEFORE.getType()).setUserIds(Set.of(2L)).setReason("加签")), + TASK_SIGN_CREATE_USER_NOT_EXIST); + } + + @Test + public void testDeleteSignTask_parentNotExists() { + // 准备参数 + BpmTaskServiceImpl spyService = spy(taskService); + doReturn(task).when(spyService).validateTaskExists("task-1"); + when(task.getParentTaskId()).thenReturn("parent-1"); + doReturn(null).when(spyService).getTask("parent-1"); + + // 调用,并断言异常 + assertServiceException(() -> spyService.deleteSignTask(1L, new BpmTaskSignDeleteReqVO().setId("task-1") + .setReason("减签")), TASK_SIGN_DELETE_NO_PARENT); + } + + @Test + public void testDeleteSignTask_parentNotSign() { + // 准备参数 + BpmTaskServiceImpl spyService = spy(taskService); + doReturn(task).when(spyService).validateTaskExists("task-1"); + when(task.getParentTaskId()).thenReturn("parent-1"); + doReturn(buildTaskEntity("parent-1", null)).when(spyService).getTask("parent-1"); + + // 调用,并断言异常 + assertServiceException(() -> spyService.deleteSignTask(1L, new BpmTaskSignDeleteReqVO().setId("task-1") + .setReason("减签")), TASK_SIGN_DELETE_NO_PARENT); + } + + @Test + public void testCopyTask_success() { + // 准备参数 + BpmTaskCopyReqVO reqVO = new BpmTaskCopyReqVO().setId("task-1").setCopyUserIds(Set.of(2L)).setReason("抄送"); + + // 调用 + taskService.copyTask(1L, reqVO); + // 断言 + verify(processInstanceCopyService).createProcessInstanceCopy(Set.of(2L), "抄送", "task-1"); + } + + @Test + public void testWithdrawTask_processNotRunning() { + // 准备参数 + HistoricTaskInstance historicTask = mockWithdrawHistoricTask("task1"); + when(historicTask.getProcessInstanceId()).thenReturn("process-instance-1"); + + // 调用,并断言异常 + assertServiceException(() -> taskService.withdrawTask(1L, "task-1"), TASK_WITHDRAW_FAIL_PROCESS_NOT_RUNNING); + } + + @Test + public void testWithdrawTask_noNextUserTask() { + // 准备参数:task2 已经是最后一个审批节点 + HistoricTaskInstance historicTask = mockWithdrawHistoricTask("task2"); + when(historicTask.getProcessInstanceId()).thenReturn("process-instance-1"); + when(historicTask.getProcessDefinitionId()).thenReturn("definition-1"); + when(processInstanceService.getProcessInstance("process-instance-1")).thenReturn(processInstance); + when(processInstance.getProcessDefinitionId()).thenReturn("definition-1"); + when(processDefinitionService.getProcessDefinitionInfo("definition-1")) + .thenReturn(new BpmProcessDefinitionInfoDO().setAllowWithdrawTask(true)); + when(modelService.getBpmnModelByDefinitionId("definition-1")).thenReturn(buildTwoStepBpmnModel()); + + // 调用,并断言异常 + assertServiceException(() -> taskService.withdrawTask(1L, "task-1"), TASK_WITHDRAW_FAIL_NEXT_TASK_NOT_ALLOW); + } + + // ========== Event 事件相关方法 ========== + + @Test + public void testProcessTaskCreated_statusExists() { + // 准备参数 + when(task.getTaskLocalVariables()).thenReturn(Map.of(BpmnVariableConstants.TASK_VARIABLE_STATUS, + BpmTaskStatusEnum.RUNNING.getStatus())); + + // 调用 + taskService.processTaskCreated(task); + // 断言 + verify(flowableTaskService, never()).setVariableLocal(any(), any(), any()); + verifyNoInteractions(processInstanceService); + } + + @Test + public void testProcessTaskCreated_processInstanceNotExists() { + // 准备参数 + mockTask("1"); + + // 调用 + taskService.processTaskCreated(task); + // 断言:状态更新为审批中,但不再处理后续逻辑 + verify(flowableTaskService).setVariableLocal("task-1", BpmnVariableConstants.TASK_VARIABLE_STATUS, + BpmTaskStatusEnum.RUNNING.getStatus()); + verifyNoInteractions(processDefinitionService, modelService); + } + + @Test + public void testProcessTaskCreated_processDefinitionNotExists() { + // 准备参数 + mockTask("1"); + when(processInstanceService.getProcessInstance("process-instance-1")).thenReturn(processInstance); + when(processInstance.getProcessDefinitionId()).thenReturn("definition-1"); + + // 调用 + taskService.processTaskCreated(task); + // 断言 + verify(processDefinitionService).getProcessDefinitionInfo("definition-1"); + verifyNoInteractions(modelService); + } + + @Test + public void testProcessTaskCreated_userApproveHasAssignee() { + // 准备参数:人工审批且已有审批人,不自动处理 + mockProcessTaskCreated("1", Map.of(BpmnModelConstants.USER_TASK_APPROVE_TYPE, + BpmUserTaskApproveTypeEnum.USER.getType(), BpmnModelConstants.USER_TASK_ASSIGN_EMPTY_HANDLER_TYPE, + BpmUserTaskAssignEmptyHandlerTypeEnum.APPROVE.getType())); + + // 调用 + executeWithSynchronization(() -> taskService.processTaskCreated(task), + TransactionSynchronization.STATUS_COMMITTED); + // 断言 + verifyNoInteractions(selfService); + } + + @Test + public void testProcessTaskCreated_rolledBack() { + // 准备参数 + mockProcessTaskCreated("1", Map.of(BpmnModelConstants.USER_TASK_APPROVE_TYPE, + BpmUserTaskApproveTypeEnum.AUTO_APPROVE.getType())); + + // 调用 + executeWithSynchronization(() -> taskService.processTaskCreated(task), + TransactionSynchronization.STATUS_ROLLED_BACK); + // 断言 + verifyNoInteractions(selfService); + } + + @Test + public void testProcessTaskCreated_taskBeforeTrigger() { + // 准备参数:配置任务前置通知 + BpmModelMetaInfoVO.HttpRequestSetting setting = new BpmModelMetaInfoVO.HttpRequestSetting() + .setUrl("http://127.0.0.1/task-before"); + mockProcessTaskCreated("1", Map.of(BpmnModelConstants.USER_TASK_APPROVE_TYPE, + BpmUserTaskApproveTypeEnum.USER.getType()), + new BpmProcessDefinitionInfoDO().setTaskBeforeTriggerSetting(setting)); + + // 调用 + try (MockedStatic httpRequestUtilsMockedStatic = mockStatic(BpmHttpRequestUtils.class)) { + executeWithSynchronization(() -> taskService.processTaskCreated(task), + TransactionSynchronization.STATUS_COMMITTED); + // 断言 + httpRequestUtilsMockedStatic.verify(() -> BpmHttpRequestUtils.executeBpmHttpRequest(processInstance, + "http://127.0.0.1/task-before", null, null, true, null)); + } + verifyNoInteractions(selfService); + } + + @Test + public void testProcessTaskCreated_unknownStatusTaskNotExists() { + // 准备参数:事务状态未知,且任务已经不存在(例如说,已被自动审批) + BpmTaskServiceImpl spyService = spy(taskService); + mockProcessTaskCreated("1", Map.of(BpmnModelConstants.USER_TASK_APPROVE_TYPE, + BpmUserTaskApproveTypeEnum.AUTO_APPROVE.getType())); + doReturn(null).when(spyService).getTask("task-1"); + + // 调用 + executeWithSynchronization(() -> spyService.processTaskCreated(task), + TransactionSynchronization.STATUS_UNKNOWN); + // 断言 + verifyNoInteractions(selfService); + } + + @Test + public void testProcessTaskCreated_unknownStatusTaskExists() { + // 准备参数:事务状态未知,但任务仍然存在 + BpmTaskServiceImpl spyService = spy(taskService); + mockProcessTaskCreated("1", Map.of(BpmnModelConstants.USER_TASK_APPROVE_TYPE, + BpmUserTaskApproveTypeEnum.AUTO_APPROVE.getType())); + doReturn(task).when(spyService).getTask("task-1"); + + // 调用 + executeWithSynchronization(() -> spyService.processTaskCreated(task), + TransactionSynchronization.STATUS_UNKNOWN); + // 断言:仍然执行自动审批 + assertEquals(BpmReasonEnum.APPROVE_TYPE_AUTO_APPROVE.getReason(), captureApproveTaskInternal(null).getReason()); + } + + @Test + public void testProcessTaskCanceled_notExists() { + // 准备参数 + BpmTaskServiceImpl spyService = spy(taskService); + doReturn(null).when(spyService).getTask("task-1"); + + // 调用 + spyService.processTaskCanceled("task-1"); + // 断言 + verify(flowableTaskService, never()).setVariableLocal(any(), any(), any()); + } + + @Test + public void testProcessTaskCanceled_endStatus() { + // 准备参数 + BpmTaskServiceImpl spyService = spy(taskService); + doReturn(task).when(spyService).getTask("task-1"); + when(task.getTaskLocalVariables()).thenReturn(Map.of(BpmnVariableConstants.TASK_VARIABLE_STATUS, + BpmTaskStatusEnum.APPROVE.getStatus())); + + // 调用 + spyService.processTaskCanceled("task-1"); + // 断言 + verify(flowableTaskService, never()).setVariableLocal(any(), any(), any()); + } + + @Test + public void testProcessTaskCanceled_success() { + // 准备参数 + BpmTaskServiceImpl spyService = spy(taskService); + doReturn(task).when(spyService).getTask("task-1"); + when(task.getTaskLocalVariables()).thenReturn(Map.of(BpmnVariableConstants.TASK_VARIABLE_STATUS, + BpmTaskStatusEnum.RUNNING.getStatus())); + + // 调用 + spyService.processTaskCanceled("task-1"); + // 断言 + verify(flowableTaskService).setVariableLocal("task-1", BpmnVariableConstants.TASK_VARIABLE_STATUS, + BpmTaskStatusEnum.CANCEL.getStatus()); + verify(flowableTaskService).setVariableLocal("task-1", BpmnVariableConstants.TASK_VARIABLE_REASON, + BpmReasonEnum.CANCEL_BY_SYSTEM.getReason()); + } + + @Test + public void testProcessTaskAssigned_noAssignee() { + // 准备参数 + when(task.getAssignee()).thenReturn(null); + + // 调用 + executeWithSynchronization(() -> taskService.processTaskAssigned(task), + TransactionSynchronization.STATUS_COMMITTED); + // 断言 + verifyNoInteractions(processInstanceService, messageService, selfService); + } + + @Test + public void testProcessTaskAssigned_rolledBack() { + // 调用 + executeWithSynchronization(() -> taskService.processTaskAssigned(task), + TransactionSynchronization.STATUS_ROLLED_BACK); + // 断言 + verifyNoInteractions(task, processInstanceService, messageService, selfService); + } + + @Test + public void testProcessTaskAssigned_unknownStatusTaskNotExists() { + // 准备参数:事务状态未知,且任务已经不存在 + BpmTaskServiceImpl spyService = spy(taskService); + when(task.getId()).thenReturn("task-1"); + doReturn(null).when(spyService).getTask("task-1"); + + // 调用 + executeWithSynchronization(() -> spyService.processTaskAssigned(task), + TransactionSynchronization.STATUS_UNKNOWN); + // 断言 + verifyNoInteractions(processInstanceService, messageService, selfService); + } + + @Test + public void testProcessTaskAssigned_processInstanceNotExists() { + // 准备参数 + when(task.getAssignee()).thenReturn("1"); + when(task.getProcessInstanceId()).thenReturn("process-instance-1"); + + // 调用 + executeWithSynchronization(() -> taskService.processTaskAssigned(task), + TransactionSynchronization.STATUS_COMMITTED); + // 断言 + verifyNoInteractions(processDefinitionService, messageService, selfService); + } + + @Test + public void testProcessTaskAssigned_startUserSkipWhenReturn() { + // 准备参数:退回到该节点时,不自动跳过,而是发送通知 + mockProcessTaskAssigned("task2", "1", null); + when(processInstance.getStartUserId()).thenReturn("1"); + when(processInstance.getProcessInstanceId()).thenReturn("process-instance-1"); + when(runtimeService.getVariable("process-instance-1", + String.format(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, "task2"), Boolean.class)) + .thenReturn(true); + when(modelService.getBpmnModelByDefinitionId("definition-1")).thenReturn(buildBpmnModel("task2", + Map.of(BpmnModelConstants.USER_TASK_ASSIGN_START_USER_HANDLER_TYPE, + BpmUserTaskAssignStartUserHandlerTypeEnum.SKIP.getType()))); + when(adminUserApi.getUser(1L)).thenReturn(new AdminUserRespDTO().setId(1L).setNickname("张三")); + + // 调用 + executeWithSynchronization(() -> taskService.processTaskAssigned(task), + TransactionSynchronization.STATUS_COMMITTED); + // 断言 + verifyNoInteractions(selfService); + verify(messageService).sendMessageWhenTaskAssigned(any(BpmMessageSendWhenTaskCreatedReqDTO.class)); + } + + @Test + public void testProcessTaskCompleted_processInstanceNotExists() { + // 准备参数 + when(task.getProcessInstanceId()).thenReturn("process-instance-1"); + + // 调用 + taskService.processTaskCompleted(task); + // 断言 + verifyNoInteractions(processDefinitionService); + } + + @Test + public void testProcessTaskCompleted_noTriggerSetting() { + // 准备参数 + when(task.getProcessInstanceId()).thenReturn("process-instance-1"); + when(processInstanceService.getProcessInstance("process-instance-1")).thenReturn(processInstance); + when(processInstance.getProcessDefinitionId()).thenReturn("definition-1"); + when(processDefinitionService.getProcessDefinitionInfo("definition-1")) + .thenReturn(new BpmProcessDefinitionInfoDO()); + + // 调用 + taskService.processTaskCompleted(task); + // 断言 + verify(processDefinitionService).getProcessDefinitionInfo("definition-1"); + } + + @Test + public void testProcessTaskCompleted_taskAfterTrigger() { + // 准备参数:配置任务后置通知 + when(task.getProcessInstanceId()).thenReturn("process-instance-1"); + when(processInstanceService.getProcessInstance("process-instance-1")).thenReturn(processInstance); + when(processInstance.getProcessDefinitionId()).thenReturn("definition-1"); + BpmModelMetaInfoVO.HttpRequestSetting setting = new BpmModelMetaInfoVO.HttpRequestSetting() + .setUrl("http://127.0.0.1/task-after"); + when(processDefinitionService.getProcessDefinitionInfo("definition-1")) + .thenReturn(new BpmProcessDefinitionInfoDO().setTaskAfterTriggerSetting(setting)); + + // 调用 + try (MockedStatic httpRequestUtilsMockedStatic = mockStatic(BpmHttpRequestUtils.class)) { + taskService.processTaskCompleted(task); + // 断言 + httpRequestUtilsMockedStatic.verify(() -> BpmHttpRequestUtils.executeBpmHttpRequest(processInstance, + "http://127.0.0.1/task-after", null, null, true, null)); + } + } + + @Test + public void testProcessTaskTimeout_processInstanceNotExists() { + // 调用 + taskService.processTaskTimeout("process-instance-1", "task1", + BpmUserTaskTimeoutHandlerTypeEnum.REMINDER.getType()); + // 断言 + verifyNoInteractions(flowableTaskService, messageService); + } + + @Test + public void testProcessTaskTimeout_taskNotExists() { + // 准备参数 + BpmTaskServiceImpl spyService = spy(taskService); + when(processInstanceService.getProcessInstance("process-instance-1")).thenReturn(processInstance); + doReturn(List.of()).when(spyService).getRunningTaskListByProcessInstanceId("process-instance-1", true, "task1"); + + // 调用 + spyService.processTaskTimeout("process-instance-1", "task1", + BpmUserTaskTimeoutHandlerTypeEnum.REMINDER.getType()); + // 断言 + verifyNoInteractions(messageService); + } + + // ========== 私有方法 ========== + + private static TaskEntityImpl buildTaskEntity(String id, String parentTaskId) { + TaskEntityImpl taskEntity = new TaskEntityImpl(); + taskEntity.setId(id); + taskEntity.setParentTaskId(parentTaskId); + return taskEntity; + } + + private static Attachment buildAttachment(String taskId, String type) { + AttachmentEntityImpl attachment = new AttachmentEntityImpl(); + attachment.setTaskId(taskId); + attachment.setType(type); + return attachment; + } + + /** + * 构建 task1 -> task2 的两级审批流程模型 + */ + private static BpmnModel buildTwoStepBpmnModel() { + return BpmnModelUtils.getBpmnModel(ResourceUtil.readBytes("bpmn/two-step-approve.bpmn20.xml")); + } + + /** + * 构建只有一个 UserTask 的流程模型 + */ + private static BpmnModel buildBpmnModel(String userTaskId, Map extensions) { + UserTask userTask = new UserTask(); + userTask.setId(userTaskId); + extensions.forEach((name, value) -> BpmnModelUtils.addExtensionElement(userTask, name, value)); + Process process = new Process(); + process.setId("process"); + process.addFlowElement(userTask); + BpmnModel bpmnModel = new BpmnModel(); + bpmnModel.addProcess(process); + return bpmnModel; + } + + private void mockApproveTask(BpmTaskServiceImpl spyService, String extensionName) { + doReturn(task).when(spyService).validateTask(1L, "task-1"); + when(task.getProcessInstanceId()).thenReturn("process-instance-1"); + when(task.getProcessDefinitionId()).thenReturn("definition-1"); + when(task.getTaskDefinitionKey()).thenReturn("task1"); + when(processInstanceService.getProcessInstance("process-instance-1")).thenReturn(processInstance); + BpmnModel bpmnModel = buildTwoStepBpmnModel(); + BpmnModelUtils.addExtensionElement(bpmnModel.getFlowElement("task1"), extensionName, "true"); + when(modelService.getBpmnModelByDefinitionId("definition-1")).thenReturn(bpmnModel); + } + + /** + * 模拟审批 task1:下一个节点 task2 使用指定的审批人策略 + */ + private BpmnModel mockApproveTaskSuccess(BpmTaskServiceImpl spyService, BpmTaskCandidateStrategyEnum task2Strategy, + Map processVariables) { + doReturn(task).when(spyService).validateTask(1L, "task-1"); + when(task.getId()).thenReturn("task-1"); + when(task.getProcessInstanceId()).thenReturn("process-instance-1"); + when(task.getProcessDefinitionId()).thenReturn("definition-1"); + when(task.getTaskDefinitionKey()).thenReturn("task1"); + when(processInstanceService.getProcessInstance("process-instance-1")).thenReturn(processInstance); + when(processInstance.getProcessVariables()).thenReturn(processVariables); + BpmnModel bpmnModel = buildTwoStepBpmnModel(); + FlowElement task2 = bpmnModel.getFlowElement("task2"); + task2.getExtensionElements().remove(BpmnModelConstants.USER_TASK_CANDIDATE_STRATEGY); + BpmnModelUtils.addExtensionElement(task2, BpmnModelConstants.USER_TASK_CANDIDATE_STRATEGY, + task2Strategy.getStrategy()); + when(modelService.getBpmnModelByDefinitionId("definition-1")).thenReturn(bpmnModel); + return bpmnModel; + } + + @SuppressWarnings("unchecked") + private Map captureProcessVariables() { + ArgumentCaptor> variablesCaptor = ArgumentCaptor.forClass(Map.class); + verify(runtimeService).setVariables(eq("process-instance-1"), variablesCaptor.capture()); + return variablesCaptor.getValue(); + } + + private HistoricTaskInstance mockWithdrawHistoricTask(String taskDefinitionKey) { + HistoricTaskInstanceQuery historicTaskQuery = mockHistoricTaskQuery(); + HistoricTaskInstance historicTask = mock(HistoricTaskInstance.class); + lenient().when(historicTask.getTaskDefinitionKey()).thenReturn(taskDefinitionKey); + when(historicTaskQuery.singleResult()).thenReturn(historicTask); + return historicTask; + } + + private HistoricTaskInstanceQuery mockHistoricTaskQuery() { + HistoricTaskInstanceQuery historicTaskQuery = mock(HistoricTaskInstanceQuery.class, RETURNS_SELF); + when(historyService.createHistoricTaskInstanceQuery()).thenReturn(historicTaskQuery); + return historicTaskQuery; + } + + private void mockTask(String assignee) { + when(task.getId()).thenReturn("task-1"); + when(task.getProcessInstanceId()).thenReturn("process-instance-1"); + when(task.getTaskLocalVariables()).thenReturn(new HashMap<>()); + lenient().when(task.getAssignee()).thenReturn(assignee); + } + + private void mockProcessTaskCreated(String assignee, Map extensions) { + mockProcessTaskCreated(assignee, extensions, new BpmProcessDefinitionInfoDO()); + } + + private void mockProcessTaskCreated(String assignee, Map extensions, + BpmProcessDefinitionInfoDO processDefinitionInfo) { + mockTask(assignee); + when(task.getTaskDefinitionKey()).thenReturn("task1"); + when(processInstanceService.getProcessInstance("process-instance-1")).thenReturn(processInstance); + when(processInstance.getProcessDefinitionId()).thenReturn("definition-1"); + when(processDefinitionService.getProcessDefinitionInfo("definition-1")).thenReturn(processDefinitionInfo); + when(modelService.getBpmnModelByDefinitionId("definition-1")).thenReturn(buildBpmnModel("task1", extensions)); + } + + private void mockProcessTaskAssigned(String taskDefinitionKey, String assignee, Integer autoApprovalType) { + when(task.getId()).thenReturn("task-1"); + when(task.getAssignee()).thenReturn(assignee); + when(task.getProcessInstanceId()).thenReturn("process-instance-1"); + when(task.getProcessDefinitionId()).thenReturn("definition-1"); + lenient().when(task.getTaskDefinitionKey()).thenReturn(taskDefinitionKey); + when(processInstanceService.getProcessInstance("process-instance-1")).thenReturn(processInstance); + lenient().when(processInstance.getProcessDefinitionId()).thenReturn("definition-1"); + when(processDefinitionService.getProcessDefinitionInfo("definition-1")) + .thenReturn(new BpmProcessDefinitionInfoDO().setAutoApprovalType(autoApprovalType)); + } + + /** + * 在事务同步上下文中执行,并模拟事务完成回调 + */ + private static void executeWithSynchronization(Runnable runnable, int transactionStatus) { + TransactionSynchronizationManager.initSynchronization(); + try { + runnable.run(); + TransactionSynchronizationManager.getSynchronizations() + .forEach(synchronization -> synchronization.afterCompletion(transactionStatus)); + } finally { + TransactionSynchronizationManager.clearSynchronization(); + } + } + + private BpmTaskApproveReqVO captureApproveTaskInternal(Long userId) { + ArgumentCaptor reqVOCaptor = ArgumentCaptor.forClass(BpmTaskApproveReqVO.class); + verify(selfService).approveTaskInternal(eq(userId), reqVOCaptor.capture()); + return reqVOCaptor.getValue(); + } + +} diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmUserTaskListenerTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmUserTaskListenerTest.java new file mode 100644 index 0000000..6436141 --- /dev/null +++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmUserTaskListenerTest.java @@ -0,0 +1,74 @@ +package cn.iocoder.yudao.module.bpm.service.task.listener; + +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmHttpRequestUtils; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; +import org.flowable.common.engine.api.delegate.Expression; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.task.service.delegate.DelegateTask; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; + +import java.util.List; + +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +/** + * {@link BpmUserTaskListener} 的单元测试 + * + * @author HUIHUI + */ +public class BpmUserTaskListenerTest extends BaseMockitoUnitTest { + + @InjectMocks + private BpmUserTaskListener userTaskListener; + + @Mock + private BpmProcessInstanceService processInstanceService; + @Mock + private Expression listenerConfig; + + @Test + @SuppressWarnings("unchecked") + public void testNotify_bodyNull() { + // mock 方法:监听器未配置 body + ProcessInstance processInstance = mock(ProcessInstance.class); + when(processInstanceService.getProcessInstance("process-instance-1")).thenReturn(processInstance); + BpmSimpleModelNodeVO.ListenerHandler listenerHandler = new BpmSimpleModelNodeVO.ListenerHandler() + .setPath("http://127.0.0.1/listener"); + // 准备参数 + DelegateTask delegateTask = mock(DelegateTask.class); + when(delegateTask.getProcessInstanceId()).thenReturn("process-instance-1"); + when(delegateTask.getAssignee()).thenReturn("1"); + when(delegateTask.getTaskDefinitionKey()).thenReturn("task1"); + when(delegateTask.getId()).thenReturn("task-1"); + + try (MockedStatic bpmnModelUtilsMockedStatic = mockStatic(BpmnModelUtils.class); + MockedStatic httpRequestUtilsMockedStatic = mockStatic(BpmHttpRequestUtils.class)) { + bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.parseListenerConfig(listenerConfig)) + .thenReturn(listenerHandler); + + // 调用 + userTaskListener.notify(delegateTask); + + // 断言:body 为空时不会空指针,并追加任务相关的固定参数 + ArgumentCaptor> bodyCaptor = ArgumentCaptor.forClass(List.class); + httpRequestUtilsMockedStatic.verify(() -> BpmHttpRequestUtils.executeBpmHttpRequest(same(processInstance), + eq("http://127.0.0.1/listener"), isNull(), bodyCaptor.capture(), eq(false), isNull())); + List body = bodyCaptor.getValue(); + assertEquals(List.of("processInstanceId", "assignee", "taskDefinitionKey", "taskId"), + convertList(body, BpmSimpleModelNodeVO.HttpRequestParam::getKey)); + assertEquals(List.of("process-instance-1", "1", "task1", "task-1"), + convertList(body, BpmSimpleModelNodeVO.HttpRequestParam::getValue)); + } + } + +} diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/task/trigger/http/BpmHttpCallbackTriggerTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/task/trigger/http/BpmHttpCallbackTriggerTest.java new file mode 100644 index 0000000..b2006f4 --- /dev/null +++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/task/trigger/http/BpmHttpCallbackTriggerTest.java @@ -0,0 +1,61 @@ +package cn.iocoder.yudao.module.bpm.service.task.trigger.http; + +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmHttpRequestUtils; +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; +import org.flowable.engine.runtime.ProcessInstance; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +/** + * {@link BpmHttpCallbackTrigger} 的单元测试 + * + * @author HUIHUI + */ +public class BpmHttpCallbackTriggerTest extends BaseMockitoUnitTest { + + @InjectMocks + private BpmHttpCallbackTrigger httpCallbackTrigger; + + @Mock + private BpmProcessInstanceService processInstanceService; + + @Test + @SuppressWarnings("unchecked") + public void testExecute_bodyNull() { + // mock 方法 + ProcessInstance processInstance = mock(ProcessInstance.class); + when(processInstanceService.getProcessInstance("process-instance-1")).thenReturn(processInstance); + // 准备参数:回调配置未配置 body + BpmSimpleModelNodeVO.TriggerSetting.HttpRequestTriggerSetting setting = + new BpmSimpleModelNodeVO.TriggerSetting.HttpRequestTriggerSetting(); + setting.setUrl("http://127.0.0.1/callback"); + setting.setCallbackTaskDefineKey("task1"); + + try (MockedStatic httpRequestUtilsMockedStatic = mockStatic(BpmHttpRequestUtils.class)) { + // 调用 + httpCallbackTrigger.execute("process-instance-1", JsonUtils.toJsonString(setting)); + + // 断言:body 为空时不会空指针,并追加回调的 taskDefineKey + ArgumentCaptor> bodyCaptor = ArgumentCaptor.forClass(List.class); + httpRequestUtilsMockedStatic.verify(() -> BpmHttpRequestUtils.executeBpmHttpRequest(same(processInstance), + eq("http://127.0.0.1/callback"), isNull(), bodyCaptor.capture(), eq(false), isNull())); + List body = bodyCaptor.getValue(); + assertEquals(1, body.size()); + assertEquals("taskDefineKey", body.get(0).getKey()); + assertEquals("task1", body.get(0).getValue()); + } + } + +} diff --git a/yudao-module-bpm/src/test/resources/application-unit-test.yaml b/yudao-module-bpm/src/test/resources/application-unit-test.yaml index dd9b95b..0b2b606 100644 --- a/yudao-module-bpm/src/test/resources/application-unit-test.yaml +++ b/yudao-module-bpm/src/test/resources/application-unit-test.yaml @@ -9,7 +9,7 @@ spring: # 数据源配置项 datasource: name: ruoyi-vue-pro - url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写 + url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value,day; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写 driver-class-name: org.h2.Driver username: sa password: diff --git a/yudao-module-bpm/src/test/resources/bpmn/call-activity.bpmn20.xml b/yudao-module-bpm/src/test/resources/bpmn/call-activity.bpmn20.xml new file mode 100644 index 0000000..50fe187 --- /dev/null +++ b/yudao-module-bpm/src/test/resources/bpmn/call-activity.bpmn20.xml @@ -0,0 +1,21 @@ + + + + + + + + + + 3 + + + PT1H + + + + + + diff --git a/yudao-module-bpm/src/test/resources/bpmn/delay-timer.bpmn20.xml b/yudao-module-bpm/src/test/resources/bpmn/delay-timer.bpmn20.xml new file mode 100644 index 0000000..b95d965 --- /dev/null +++ b/yudao-module-bpm/src/test/resources/bpmn/delay-timer.bpmn20.xml @@ -0,0 +1,21 @@ + + + + + + + + + + 2 + + + PT1H + + + + + + diff --git a/yudao-module-bpm/src/test/resources/bpmn/lifecycle/assign-empty-approve.bpmn20.xml b/yudao-module-bpm/src/test/resources/bpmn/lifecycle/assign-empty-approve.bpmn20.xml new file mode 100644 index 0000000..f54cd87 --- /dev/null +++ b/yudao-module-bpm/src/test/resources/bpmn/lifecycle/assign-empty-approve.bpmn20.xml @@ -0,0 +1,19 @@ + + + + + + + + + 30 + 1 + 1 + + + + + + diff --git a/yudao-module-bpm/src/test/resources/bpmn/lifecycle/assign-empty-reject.bpmn20.xml b/yudao-module-bpm/src/test/resources/bpmn/lifecycle/assign-empty-reject.bpmn20.xml new file mode 100644 index 0000000..070b282 --- /dev/null +++ b/yudao-module-bpm/src/test/resources/bpmn/lifecycle/assign-empty-reject.bpmn20.xml @@ -0,0 +1,19 @@ + + + + + + + + + 30 + 1 + 2 + + + + + + diff --git a/yudao-module-bpm/src/test/resources/bpmn/lifecycle/child-parent.bpmn20.xml b/yudao-module-bpm/src/test/resources/bpmn/lifecycle/child-parent.bpmn20.xml new file mode 100644 index 0000000..f18e484 --- /dev/null +++ b/yudao-module-bpm/src/test/resources/bpmn/lifecycle/child-parent.bpmn20.xml @@ -0,0 +1,29 @@ + + + + + + + + + 20 + + + + + + + + + + + + 30 + + + + + + diff --git a/yudao-module-bpm/src/test/resources/bpmn/lifecycle/child.bpmn20.xml b/yudao-module-bpm/src/test/resources/bpmn/lifecycle/child.bpmn20.xml new file mode 100644 index 0000000..75b6cfc --- /dev/null +++ b/yudao-module-bpm/src/test/resources/bpmn/lifecycle/child.bpmn20.xml @@ -0,0 +1,17 @@ + + + + + + + + + 30 + + + + + + diff --git a/yudao-module-bpm/src/test/resources/bpmn/lifecycle/start-user-node.bpmn20.xml b/yudao-module-bpm/src/test/resources/bpmn/lifecycle/start-user-node.bpmn20.xml new file mode 100644 index 0000000..56039d8 --- /dev/null +++ b/yudao-module-bpm/src/test/resources/bpmn/lifecycle/start-user-node.bpmn20.xml @@ -0,0 +1,23 @@ + + + + + + + + + 36 + + + + + + 30 + + + + + + diff --git a/yudao-module-bpm/src/test/resources/bpmn/lifecycle/start-user-skip.bpmn20.xml b/yudao-module-bpm/src/test/resources/bpmn/lifecycle/start-user-skip.bpmn20.xml new file mode 100644 index 0000000..18a05c4 --- /dev/null +++ b/yudao-module-bpm/src/test/resources/bpmn/lifecycle/start-user-skip.bpmn20.xml @@ -0,0 +1,18 @@ + + + + + + + + + 30 + 2 + + + + + + diff --git a/yudao-module-bpm/src/test/resources/bpmn/lifecycle/start-user-transfer-dept-leader.bpmn20.xml b/yudao-module-bpm/src/test/resources/bpmn/lifecycle/start-user-transfer-dept-leader.bpmn20.xml new file mode 100644 index 0000000..fdd96a8 --- /dev/null +++ b/yudao-module-bpm/src/test/resources/bpmn/lifecycle/start-user-transfer-dept-leader.bpmn20.xml @@ -0,0 +1,18 @@ + + + + + + + + + 30 + 3 + + + + + + diff --git a/yudao-module-bpm/src/test/resources/bpmn/multi-instance/call-activity.bpmn20.xml b/yudao-module-bpm/src/test/resources/bpmn/multi-instance/call-activity.bpmn20.xml new file mode 100644 index 0000000..3a6904b --- /dev/null +++ b/yudao-module-bpm/src/test/resources/bpmn/multi-instance/call-activity.bpmn20.xml @@ -0,0 +1,78 @@ + + + + + + + + + 30 + + + + + + + + + + + + 1 + + + + + + + + 2 + ${ nrOfCompletedInstances/nrOfInstances >= 1.00} + + + + + + + + + + + + 2 + + + + + + + + ${ nrOfCompletedInstances/nrOfInstances >= 1.00} + + + + + + + + + + + + 3 + + + + + + + + ${ nrOfCompletedInstances/nrOfInstances >= 1.00} + + + + + + diff --git a/yudao-module-bpm/src/test/resources/bpmn/multi-instance/user-task.bpmn20.xml b/yudao-module-bpm/src/test/resources/bpmn/multi-instance/user-task.bpmn20.xml new file mode 100644 index 0000000..7282736 --- /dev/null +++ b/yudao-module-bpm/src/test/resources/bpmn/multi-instance/user-task.bpmn20.xml @@ -0,0 +1,95 @@ + + + + + + + + + + 30 + 3 + + + ${ nrOfCompletedInstances > 0 } + + + + + + + + + + + + 30 + 2 + + + ${ nrOfCompletedInstances/nrOfInstances >= 1.00} + + + + + + + + + + + + 30 + 2 + + + ${ nrOfCompletedInstances/nrOfInstances >= 0.60} + + + + + + + + + + + + 30 + + + + + + 30 + 2 + 2 + multiReturn-first + + + ${ nrOfCompletedInstances/nrOfInstances >= 1.00} + + + + + + + + + + + + 30 + 4 + + + 1 + ${ nrOfCompletedInstances >= nrOfInstances } + + + + + + diff --git a/yudao-module-bpm/src/test/resources/bpmn/simple-approve.bpmn20.xml b/yudao-module-bpm/src/test/resources/bpmn/simple-approve.bpmn20.xml new file mode 100644 index 0000000..5ff9772 --- /dev/null +++ b/yudao-module-bpm/src/test/resources/bpmn/simple-approve.bpmn20.xml @@ -0,0 +1,16 @@ + + + + + + + + 36 + + + + + + diff --git a/yudao-module-bpm/src/test/resources/bpmn/timeout-approve.bpmn20.xml b/yudao-module-bpm/src/test/resources/bpmn/timeout-approve.bpmn20.xml new file mode 100644 index 0000000..1677694 --- /dev/null +++ b/yudao-module-bpm/src/test/resources/bpmn/timeout-approve.bpmn20.xml @@ -0,0 +1,26 @@ + + + + + + + + + 30 + + + + + 1 + 2 + + + PT1H + + + + + + diff --git a/yudao-module-bpm/src/test/resources/bpmn/trigger-task.bpmn20.xml b/yudao-module-bpm/src/test/resources/bpmn/trigger-task.bpmn20.xml new file mode 100644 index 0000000..18a7122 --- /dev/null +++ b/yudao-module-bpm/src/test/resources/bpmn/trigger-task.bpmn20.xml @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/yudao-module-bpm/src/test/resources/bpmn/two-step-approve.bpmn20.xml b/yudao-module-bpm/src/test/resources/bpmn/two-step-approve.bpmn20.xml new file mode 100644 index 0000000..5d18420 --- /dev/null +++ b/yudao-module-bpm/src/test/resources/bpmn/two-step-approve.bpmn20.xml @@ -0,0 +1,24 @@ + + + + + + + + 30 + + + + + + 30 + 2 + task1 + + + + + + diff --git a/yudao-module-bpm/src/test/resources/sql/clean.sql b/yudao-module-bpm/src/test/resources/sql/clean.sql index d4f93bb..3e6ab26 100644 --- a/yudao-module-bpm/src/test/resources/sql/clean.sql +++ b/yudao-module-bpm/src/test/resources/sql/clean.sql @@ -1,3 +1,6 @@ DELETE FROM "bpm_form"; DELETE FROM "bpm_user_group"; DELETE FROM "bpm_category"; +DELETE FROM "bpm_process_expression"; +DELETE FROM "bpm_process_listener"; +DELETE FROM "bpm_oa_leave"; diff --git a/yudao-module-bpm/src/test/resources/sql/create_tables.sql b/yudao-module-bpm/src/test/resources/sql/create_tables.sql index 1034962..e2e6a45 100644 --- a/yudao-module-bpm/src/test/resources/sql/create_tables.sql +++ b/yudao-module-bpm/src/test/resources/sql/create_tables.sql @@ -41,3 +41,50 @@ CREATE TABLE IF NOT EXISTS "bpm_form" ( "deleted" bit NOT NULL DEFAULT FALSE, PRIMARY KEY ("id") ) COMMENT '动态表单'; + +CREATE TABLE IF NOT EXISTS "bpm_process_expression" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(63) NOT NULL, + "status" tinyint NOT NULL, + "expression" varchar(255) NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '流程表达式'; + +CREATE TABLE IF NOT EXISTS "bpm_process_listener" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "name" varchar(63) NOT NULL, + "status" tinyint NOT NULL, + "type" varchar(30) NOT NULL, + "event" varchar(30) NOT NULL, + "value_type" varchar(30) NOT NULL, + "value" varchar(255) NOT NULL, + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT '流程监听器'; + +CREATE TABLE IF NOT EXISTS "bpm_oa_leave" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "user_id" bigint NOT NULL, + "type" tinyint NOT NULL, + "reason" varchar(255) NOT NULL, + "start_time" timestamp NOT NULL, + "end_time" timestamp NOT NULL, + "day" bigint NOT NULL, + "status" tinyint NOT NULL, + "process_instance_id" varchar(64) DEFAULT '', + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT 'OA 请假申请'; diff --git a/yudao-module-bpm/src/test/resources/sql/flowable/clean.sql b/yudao-module-bpm/src/test/resources/sql/flowable/clean.sql new file mode 100644 index 0000000..53c866e --- /dev/null +++ b/yudao-module-bpm/src/test/resources/sql/flowable/clean.sql @@ -0,0 +1 @@ +DELETE FROM "bpm_process_definition_info"; diff --git a/yudao-module-bpm/src/test/resources/sql/flowable/create_tables.sql b/yudao-module-bpm/src/test/resources/sql/flowable/create_tables.sql new file mode 100644 index 0000000..bdbbec1 --- /dev/null +++ b/yudao-module-bpm/src/test/resources/sql/flowable/create_tables.sql @@ -0,0 +1,39 @@ +-- Flowable 集成测试(H2 LEGACY 模式)使用的业务表,不支持表注释语法 +CREATE TABLE IF NOT EXISTS "bpm_process_definition_info" ( + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, + "process_definition_id" varchar(64) NOT NULL, + "model_id" varchar(64) NOT NULL, + "model_type" tinyint NOT NULL, + "category" varchar(64), + "icon" varchar(512), + "description" varchar(255), + "form_type" tinyint, + "form_id" bigint, + "form_conf" varchar(1000), + "form_fields" varchar(5000), + "form_custom_create_path" varchar(255), + "form_custom_view_path" varchar(255), + "simple_model" varchar(10000), + "visible" bit NOT NULL DEFAULT TRUE, + "sort" bigint, + "start_user_ids" varchar(255), + "start_dept_ids" varchar(255), + "manager_user_ids" varchar(255), + "allow_cancel_running_process" bit, + "allow_withdraw_task" bit, + "process_id_rule" varchar(255), + "auto_approval_type" tinyint, + "title_setting" varchar(512), + "summary_setting" varchar(512), + "process_before_trigger_setting" varchar(512), + "process_after_trigger_setting" varchar(512), + "task_before_trigger_setting" varchar(512), + "task_after_trigger_setting" varchar(512), + "print_template_setting" varchar(512), + "creator" varchar(64) DEFAULT '', + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updater" varchar(64) DEFAULT '', + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "deleted" bit NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +);