Commit e0f0cab7f6a81d95e7aee1ac89fa6f059a3a7c86

Authored by YunaiV
1 parent c8236e25

fix(bpm): 完善 Flowable 流程测试与边界处理

Showing 68 changed files with 6533 additions and 47 deletions
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;
17 17 import org.flowable.common.engine.api.delegate.Expression;
18 18  
19 19 import java.util.List;
  20 +import java.util.Objects;
20 21 import java.util.Set;
21 22  
22 23 /**
... ... @@ -37,6 +38,10 @@ public class BpmParallelMultiInstanceBehavior extends ParallelMultiInstanceBehav
37 38 super(activity, innerActivityBehavior);
38 39 // 关联 Pull Request:https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1483
39 40 // 在解析/构造阶段基于 activityId 初始化与 activity 绑定且不变的字段,避免在运行期修改 Behavior 实例状态
  41 + // 注意:仅 UserTask 使用自定义变量;CallActivity 等节点,需要保留 Flowable 解析的 collection 配置(例如说,子流程多实例的表单字段)
  42 + if (!(activity instanceof UserTask)) {
  43 + return;
  44 + }
40 45 super.collectionExpression = null; // collectionExpression 和 collectionVariable 是互斥的
41 46 super.collectionVariable = FlowableUtils.formatExecutionCollectionVariable(activity.getId());
42 47 // 从 execution.getVariable() 读取当前所有任务处理的人的 key
... ... @@ -77,10 +82,10 @@ public class BpmParallelMultiInstanceBehavior extends ParallelMultiInstanceBehav
77 82 if (execution.getCurrentFlowElement() instanceof CallActivity) {
78 83 FlowElement flowElement = execution.getCurrentFlowElement();
79 84 Integer sourceType = BpmnModelUtils.parseMultiInstanceSourceType(flowElement);
80   - if (sourceType.equals(BpmChildProcessMultiInstanceSourceTypeEnum.NUMBER_FORM.getType())) {
  85 + if (Objects.equals(sourceType, BpmChildProcessMultiInstanceSourceTypeEnum.NUMBER_FORM.getType())) {
81 86 return execution.getVariable(super.collectionExpression.getExpressionText(), Integer.class);
82 87 }
83   - if (sourceType.equals(BpmChildProcessMultiInstanceSourceTypeEnum.MULTIPLE_FORM.getType())) {
  88 + if (Objects.equals(sourceType, BpmChildProcessMultiInstanceSourceTypeEnum.MULTIPLE_FORM.getType())) {
84 89 return execution.getVariable(super.collectionExpression.getExpressionText(), List.class).size();
85 90 }
86 91 }
... ... @@ -92,17 +97,29 @@ public class BpmParallelMultiInstanceBehavior extends ParallelMultiInstanceBehav
92 97  
93 98 @Override
94 99 public void setCollectionExpression(Expression collectionExpression) {
95   - // 保持自定义变量名,忽略解析器写入的 collection 表达式
  100 + // UserTask 保持自定义变量名,忽略解析器写入的 collection 表达式
  101 + if (activity instanceof UserTask) {
  102 + return;
  103 + }
  104 + super.setCollectionExpression(collectionExpression);
96 105 }
97 106  
98 107 @Override
99 108 public void setCollectionVariable(String collectionVariable) {
100   - // 保持自定义变量名,忽略解析器写入的 collection 变量名
  109 + // UserTask 保持自定义变量名,忽略解析器写入的 collection 变量名
  110 + if (activity instanceof UserTask) {
  111 + return;
  112 + }
  113 + super.setCollectionVariable(collectionVariable);
101 114 }
102 115  
103 116 @Override
104 117 public void setCollectionElementVariable(String collectionElementVariable) {
105   - // 保持自定义变量名,忽略解析器写入的单元素变量名
  118 + // UserTask 保持自定义变量名,忽略解析器写入的单元素变量名
  119 + if (activity instanceof UserTask) {
  120 + return;
  121 + }
  122 + super.setCollectionElementVariable(collectionElementVariable);
106 123 }
107 124  
108 125 }
... ...
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;
16 16  
17 17 import java.util.LinkedHashSet;
18 18 import java.util.List;
  19 +import java.util.Objects;
19 20 import java.util.Set;
20 21  
21 22 /**
... ... @@ -34,6 +35,10 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB
34 35 super(activity, innerActivityBehavior);
35 36 // 关联 Pull Request:https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1483
36 37 // 在解析/构造阶段基于 activityId 初始化与 activity 绑定且不变的字段,避免在运行期修改 Behavior 实例状态
  38 + // 注意:仅 UserTask 使用自定义变量;CallActivity 等节点,需要保留 Flowable 解析的 collection 配置(例如说,子流程多实例的表单字段)
  39 + if (!(activity instanceof UserTask)) {
  40 + return;
  41 + }
37 42 super.collectionExpression = null; // collectionExpression 和 collectionVariable 是互斥的
38 43 super.collectionVariable = FlowableUtils.formatExecutionCollectionVariable(activity.getId());
39 44 // 从 execution.getVariable() 读取当前所有任务处理的人的 key
... ... @@ -70,10 +75,10 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB
70 75 if (execution.getCurrentFlowElement() instanceof CallActivity) {
71 76 FlowElement flowElement = execution.getCurrentFlowElement();
72 77 Integer sourceType = BpmnModelUtils.parseMultiInstanceSourceType(flowElement);
73   - if (sourceType.equals(BpmChildProcessMultiInstanceSourceTypeEnum.NUMBER_FORM.getType())) {
  78 + if (Objects.equals(sourceType, BpmChildProcessMultiInstanceSourceTypeEnum.NUMBER_FORM.getType())) {
74 79 return execution.getVariable(super.collectionExpression.getExpressionText(), Integer.class);
75 80 }
76   - if (sourceType.equals(BpmChildProcessMultiInstanceSourceTypeEnum.MULTIPLE_FORM.getType())) {
  81 + if (Objects.equals(sourceType, BpmChildProcessMultiInstanceSourceTypeEnum.MULTIPLE_FORM.getType())) {
77 82 return execution.getVariable(super.collectionExpression.getExpressionText(), List.class).size();
78 83 }
79 84 }
... ... @@ -96,17 +101,29 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB
96 101  
97 102 @Override
98 103 public void setCollectionExpression(Expression collectionExpression) {
99   - // 保持自定义变量名,忽略解析器写入的 collection 表达式
  104 + // UserTask 保持自定义变量名,忽略解析器写入的 collection 表达式
  105 + if (activity instanceof UserTask) {
  106 + return;
  107 + }
  108 + super.setCollectionExpression(collectionExpression);
100 109 }
101 110  
102 111 @Override
103 112 public void setCollectionVariable(String collectionVariable) {
104   - // 保持自定义变量名,忽略解析器写入的 collection 变量名
  113 + // UserTask 保持自定义变量名,忽略解析器写入的 collection 变量名
  114 + if (activity instanceof UserTask) {
  115 + return;
  116 + }
  117 + super.setCollectionVariable(collectionVariable);
105 118 }
106 119  
107 120 @Override
108 121 public void setCollectionElementVariable(String collectionElementVariable) {
109   - // 保持自定义变量名,忽略解析器写入的单元素变量名
  122 + // UserTask 保持自定义变量名,忽略解析器写入的单元素变量名
  123 + if (activity instanceof UserTask) {
  124 + return;
  125 + }
  126 + super.setCollectionElementVariable(collectionElementVariable);
110 127 }
111 128  
112 129 }
... ...
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 {
116 116 log.error("[timerFired][解析 entity({}) elementId 为空,跳过处理]", entity);
117 117 return;
118 118 }
119   - FlowElement element = BpmnModelUtils.getFlowElementById(bpmnModel, entity.getElementId());
  119 + FlowElement element = BpmnModelUtils.getFlowElementById(bpmnModel, elementId);
120 120 if (!(element instanceof BoundaryEvent)) {
121 121 return;
122 122 }
... ...
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmHttpRequestUtils.java
... ... @@ -89,7 +89,7 @@ public class BpmHttpRequestUtils {
89 89 BpmProcessInstanceService processInstanceService = SpringUtils.getBean(BpmProcessInstanceService.class);
90 90 ProcessInstance processInstance = processInstanceService.getProcessInstance(event.getId());
91 91 if (processInstance != null) {
92   - headers.add(HEADER_TENANT_ID, String.valueOf(TenantContextHolder.getTenantId()));
  92 + headers.add(HEADER_TENANT_ID, processInstance.getTenantId());
93 93 }
94 94 }
95 95 // 1.2 设置请求体
... ...
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java
... ... @@ -876,11 +876,11 @@ public class SimpleModelUtils {
876 876 if (childProcessSetting.getTimeoutSetting() != null && Boolean.TRUE.equals(childProcessSetting.getTimeoutSetting().getEnable())) {
877 877 BoundaryEvent boundaryEvent = null;
878 878 if (childProcessSetting.getTimeoutSetting().getType().equals(BpmDelayTimerTypeEnum.FIXED_DATE_TIME.getType())) {
879   - boundaryEvent = buildTimeoutBoundaryEvent(callActivity, BpmBoundaryEventTypeEnum.DELAY_TIMER_TIMEOUT.getType(),
880   - childProcessSetting.getTimeoutSetting().getTimeExpression(), null, null);
881   - } else if (childProcessSetting.getTimeoutSetting().getType().equals(BpmDelayTimerTypeEnum.FIXED_TIME_DURATION.getType())) {
882 879 boundaryEvent = buildTimeoutBoundaryEvent(callActivity, BpmBoundaryEventTypeEnum.CHILD_PROCESS_TIMEOUT.getType(),
883 880 null, null, childProcessSetting.getTimeoutSetting().getTimeExpression());
  881 + } else if (childProcessSetting.getTimeoutSetting().getType().equals(BpmDelayTimerTypeEnum.FIXED_TIME_DURATION.getType())) {
  882 + boundaryEvent = buildTimeoutBoundaryEvent(callActivity, BpmBoundaryEventTypeEnum.CHILD_PROCESS_TIMEOUT.getType(),
  883 + childProcessSetting.getTimeoutSetting().getTimeExpression(), null, null);
884 884 }
885 885 flowElements.add(boundaryEvent);
886 886 }
... ... @@ -931,7 +931,7 @@ public class SimpleModelUtils {
931 931 if (ObjUtil.isNotNull(timeDuration)) {
932 932 eventDefinition.setTimeDuration(timeDuration);
933 933 }
934   - if (ObjUtil.isNotNull(timeDuration)) {
  934 + if (ObjUtil.isNotNull(timeCycle)) {
935 935 eventDefinition.setTimeCycle(timeCycle);
936 936 }
937 937 if (ObjUtil.isNotNull(timeDate)) {
... ...
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java
... ... @@ -210,7 +210,7 @@ public class BpmModelServiceImpl implements BpmModelService {
210 210  
211 211 // 保存排序
212 212 long sort = System.currentTimeMillis(); // 使用时间戳 - i 作为排序
213   - for (int i = ids.size() - 1; i > 0; i--) {
  213 + for (int i = ids.size() - 1; i >= 0; i--) {
214 214 Model model = modelMap.get(ids.get(i));
215 215 // 更新模型
216 216 BpmModelMetaInfoVO metaInfo = BpmModelConvert.INSTANCE.parseMetaInfo(model).setSort(sort);
... ...
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java
... ... @@ -57,7 +57,12 @@ public class BpmProcessDefinitionServiceImpl implements BpmProcessDefinitionServ
57 57  
58 58 @Override
59 59 public ProcessDefinition getProcessDefinition(String id) {
60   - return repositoryService.getProcessDefinition(id);
  60 + // 不使用 repositoryService.getProcessDefinition(id) 的原因:流程定义不存在时,它会抛出 FlowableObjectNotFoundException,
  61 + // 而调用方都是通过返回 null 判断流程定义不存在
  62 + if (StrUtil.isEmpty(id)) {
  63 + return null;
  64 + }
  65 + return repositoryService.createProcessDefinitionQuery().processDefinitionId(id).singleResult();
61 66 }
62 67  
63 68 @Override
... ...
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java
... ... @@ -198,6 +198,9 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
198 198 ProcessDefinition processDefinition = processDefinitionService.getProcessDefinition(
199 199 historicProcessInstance != null ? historicProcessInstance.getProcessDefinitionId()
200 200 : reqVO.getProcessDefinitionId());
  201 + if (processDefinition == null) {
  202 + throw exception(ErrorCodeConstants.PROCESS_DEFINITION_NOT_EXISTS);
  203 + }
201 204 BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService
202 205 .getProcessDefinitionInfo(processDefinition.getId());
203 206 BpmnModel bpmnModel = processDefinitionService.getProcessDefinitionBpmnModel(processDefinition.getId());
... ... @@ -777,6 +780,10 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
777 780 * @see <a href="https://github.com/YunaiV/ruoyi-vue-pro/pull/1245">相关 issue</a>
778 781 */
779 782 private void validateDynamicFormRequiredFields(ProcessDefinition definition, Map<String, Object> variables) {
  783 + // 流程定义不存在时,交给 createProcessInstance0 统一校验,避免空指针
  784 + if (definition == null) {
  785 + return;
  786 + }
780 787 BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService
781 788 .getProcessDefinitionInfo(definition.getId());
782 789 if (processDefinitionInfo == null
... ...
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java
... ... @@ -1066,7 +1066,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
1066 1066 String taskId = reqVO.getId();
1067 1067 // 1.1 校验任务
1068 1068 Task task = validateTask(userId, reqVO.getId());
1069   - if (task.getAssignee().equals(reqVO.getDelegateUserId().toString())) { // 校验当前审批人和被委派人不是同一人
  1069 + if (StrUtil.equals(task.getAssignee(), reqVO.getDelegateUserId().toString())) { // 校验当前审批人和被委派人不是同一人
1070 1070 throw exception(TASK_DELEGATE_FAIL_USER_REPEAT);
1071 1071 }
1072 1072 // 1.2 校验目标用户存在
... ... @@ -1097,7 +1097,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
1097 1097 String taskId = reqVO.getId();
1098 1098 // 1.1 校验任务
1099 1099 Task task = validateTask(userId, reqVO.getId());
1100   - if (task.getAssignee().equals(reqVO.getAssigneeUserId().toString())) { // 校验当前审批人和被转派人不是同一人
  1100 + if (StrUtil.equals(task.getAssignee(), reqVO.getAssigneeUserId().toString())) { // 校验当前审批人和被转派人不是同一人
1101 1101 throw exception(TASK_TRANSFER_FAIL_USER_REPEAT);
1102 1102 }
1103 1103 // 1.2 校验目标用户存在
... ...
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;
14 14 import org.springframework.context.annotation.Scope;
15 15 import org.springframework.stereotype.Component;
16 16  
  17 +import java.util.ArrayList;
  18 +import java.util.List;
  19 +
17 20 import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.parseListenerConfig;
18 21  
19 22 // TODO @芋艿:可能会想换个包地址
... ... @@ -43,17 +46,19 @@ public class BpmUserTaskListener implements TaskListener {
43 46  
44 47 // 2. 发起请求
45 48 // TODO @芋艿:哪些默认参数,后续再调研下;感觉可以搞个 task 字段,把整个 delegateTask 放进去;
46   - listenerHandler.getBody().add(new BpmSimpleModelNodeVO.HttpRequestParam().setKey("processInstanceId")
  49 + List<BpmSimpleModelNodeVO.HttpRequestParam> body = listenerHandler.getBody() == null
  50 + ? new ArrayList<>() : new ArrayList<>(listenerHandler.getBody());
  51 + body.add(new BpmSimpleModelNodeVO.HttpRequestParam().setKey("processInstanceId")
47 52 .setType(BpmHttpRequestParamTypeEnum.FIXED_VALUE.getType()).setValue(delegateTask.getProcessInstanceId()));
48   - listenerHandler.getBody().add(new BpmSimpleModelNodeVO.HttpRequestParam().setKey("assignee")
  53 + body.add(new BpmSimpleModelNodeVO.HttpRequestParam().setKey("assignee")
49 54 .setType(BpmHttpRequestParamTypeEnum.FIXED_VALUE.getType()).setValue(delegateTask.getAssignee()));
50   - listenerHandler.getBody().add(new BpmSimpleModelNodeVO.HttpRequestParam().setKey("taskDefinitionKey")
  55 + body.add(new BpmSimpleModelNodeVO.HttpRequestParam().setKey("taskDefinitionKey")
51 56 .setType(BpmHttpRequestParamTypeEnum.FIXED_VALUE.getType()).setValue(delegateTask.getTaskDefinitionKey()));
52   - listenerHandler.getBody().add(new BpmSimpleModelNodeVO.HttpRequestParam().setKey("taskId")
  57 + body.add(new BpmSimpleModelNodeVO.HttpRequestParam().setKey("taskId")
53 58 .setType(BpmHttpRequestParamTypeEnum.FIXED_VALUE.getType()).setValue(delegateTask.getId()));
54 59 BpmHttpRequestUtils.executeBpmHttpRequest(processInstance,
55   - listenerHandler.getPath(), listenerHandler.getHeader(), listenerHandler.getBody(), false, null);
  60 + listenerHandler.getPath(), listenerHandler.getHeader(), body, false, null);
56 61  
57 62 // 3. 是否需要后续操作?TODO 芋艿:待定!
58 63 }
59   -}
60 64 \ No newline at end of file
  65 +}
... ...
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;
12 12 import org.springframework.stereotype.Component;
13 13 import org.springframework.web.client.RestTemplate;
14 14  
  15 +import java.util.ArrayList;
  16 +import java.util.List;
  17 +
15 18 /**
16 19 * BPM HTTP 回调触发器
17 20 *
... ... @@ -41,11 +44,13 @@ public class BpmHttpCallbackTrigger extends BpmAbstractHttpRequestTrigger {
41 44  
42 45 // 2. 发起请求
43 46 ProcessInstance processInstance = processInstanceService.getProcessInstance(processInstanceId);
44   - setting.getBody().add(new BpmSimpleModelNodeVO.HttpRequestParam()
  47 + List<BpmSimpleModelNodeVO.HttpRequestParam> body = setting.getBody() == null
  48 + ? new ArrayList<>() : new ArrayList<>(setting.getBody());
  49 + body.add(new BpmSimpleModelNodeVO.HttpRequestParam()
45 50 .setKey("taskDefineKey") // 重要:回调请求 taskDefineKey 需要传给被调用方,用于回调执行
46 51 .setType(BpmHttpRequestParamTypeEnum.FIXED_VALUE.getType()).setValue(setting.getCallbackTaskDefineKey()));
47 52 BpmHttpRequestUtils.executeBpmHttpRequest(processInstance,
48   - setting.getUrl(), setting.getHeader(), setting.getBody(), false, null);
  53 + setting.getUrl(), setting.getHeader(), body, false, null);
49 54 }
50 55  
51 56 }
... ...
yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BaseFlowableUnitTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.bpm.framework.flowable;
  2 +
  3 +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
  4 +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
  5 +import cn.iocoder.yudao.module.bpm.dal.redis.BpmProcessIdRedisDAO;
  6 +import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum;
  7 +import cn.iocoder.yudao.module.bpm.framework.flowable.config.BpmFlowableConfiguration;
  8 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker;
  9 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.listener.BpmProcessInstanceEventListener;
  10 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.listener.BpmTaskEventListener;
  11 +import cn.iocoder.yudao.module.bpm.service.comment.BpmCommentService;
  12 +import cn.iocoder.yudao.module.bpm.service.definition.BpmFormService;
  13 +import cn.iocoder.yudao.module.bpm.service.definition.BpmModelService;
  14 +import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
  15 +import cn.iocoder.yudao.module.bpm.service.message.BpmMessageService;
  16 +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceCopyService;
  17 +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceServiceImpl;
  18 +import cn.iocoder.yudao.module.bpm.service.task.BpmTaskServiceImpl;
  19 +import cn.iocoder.yudao.module.bpm.service.task.listener.BpmCallActivityListener;
  20 +import cn.iocoder.yudao.module.system.api.dept.DeptApi;
  21 +import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
  22 +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
  23 +import jakarta.annotation.Resource;
  24 +import org.flowable.engine.RepositoryService;
  25 +import org.flowable.spring.boot.ProcessEngineAutoConfiguration;
  26 +import org.flowable.spring.boot.ProcessEngineServicesAutoConfiguration;
  27 +import org.junit.jupiter.api.BeforeEach;
  28 +import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
  29 +import org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration;
  30 +import org.springframework.boot.test.context.SpringBootTest;
  31 +import org.springframework.context.annotation.Bean;
  32 +import org.springframework.context.annotation.Import;
  33 +import org.springframework.context.annotation.Primary;
  34 +import org.springframework.test.context.ActiveProfiles;
  35 +import org.springframework.test.context.TestPropertySource;
  36 +import org.springframework.test.context.bean.override.mockito.MockitoBean;
  37 +
  38 +import java.util.Collection;
  39 +import java.util.Set;
  40 +import java.util.function.Function;
  41 +import java.util.stream.Collectors;
  42 +
  43 +import static org.mockito.ArgumentMatchers.*;
  44 +import static org.mockito.Mockito.*;
  45 +
  46 +/**
  47 + * 依赖 Flowable 引擎的单元测试
  48 + *
  49 + * <p>使用 H2 数据源,Flowable 表由测试环境的 Flowable 自动配置创建;需要业务表的测试自行导入对应 SQL。</p>
  50 + * <p>加载真实的 {@link BpmTaskServiceImpl}、{@link BpmProcessInstanceServiceImpl} 及 Flowable 事件监听器,
  51 + * 保证事务代理、getSelf() 自调用、任务/流程实例状态回调与生产一致;其它模块、DB 相关的 Service 统一 Mock。</p>
  52 + * <p>注意:Mock 统一声明在基类,保证子类共享同一个 Spring 上下文(同一个 Flowable 引擎),子类不要再额外声明 {@link MockitoBean}。</p>
  53 + *
  54 + * @author HUIHUI
  55 + */
  56 +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE,
  57 + classes = BaseFlowableUnitTest.Application.class)
  58 +@ActiveProfiles("unit-test")
  59 +@TestPropertySource(properties = {
  60 + // Flowable 8 的 H2 脚本使用旧式 identity 语法,LEGACY 模式兼容该语法;业务表脚本在需要时由测试显式导入。
  61 + "spring.datasource.url=jdbc:h2:mem:bpm-flowable-testdb;MODE=LEGACY;DATABASE_TO_UPPER=false;NON_KEYWORDS=value",
  62 + "spring.sql.init.mode=never",
  63 + // 关闭异步执行器,定时器由测试显式触发,避免后台线程与测试交错执行
  64 + "flowable.async-executor-activate=false"
  65 +})
  66 +public abstract class BaseFlowableUnitTest {
  67 +
  68 + @MockitoBean
  69 + protected BpmProcessDefinitionService processDefinitionService;
  70 + @MockitoBean
  71 + protected BpmModelService modelService;
  72 + @MockitoBean
  73 + protected BpmCommentService commentService;
  74 + @MockitoBean
  75 + protected BpmMessageService messageService;
  76 + @MockitoBean
  77 + protected BpmFormService formService;
  78 + @MockitoBean
  79 + protected BpmProcessInstanceCopyService processInstanceCopyService;
  80 + @MockitoBean
  81 + protected BpmProcessIdRedisDAO processIdRedisDAO;
  82 + @MockitoBean
  83 + protected AdminUserApi adminUserApi;
  84 + @MockitoBean
  85 + protected DeptApi deptApi;
  86 +
  87 + @Import({BaseDbUnitTest.Application.class, BpmFlowableConfiguration.class,
  88 + BpmTaskServiceImpl.class, BpmProcessInstanceServiceImpl.class,
  89 + BpmTaskEventListener.class, BpmProcessInstanceEventListener.class})
  90 + @ImportAutoConfiguration({ProcessEngineAutoConfiguration.class, ProcessEngineServicesAutoConfiguration.class,
  91 + TransactionAutoConfiguration.class})
  92 + public static class Application {
  93 +
  94 + @Bean("testBpmTaskCandidateInvoker")
  95 + @Primary
  96 + public BpmTaskCandidateInvoker bpmTaskCandidateInvoker() {
  97 + BpmTaskCandidateInvoker invoker = mock(BpmTaskCandidateInvoker.class);
  98 + when(invoker.calculateUsersByTask(any())).thenReturn(Set.of(1L));
  99 + return invoker;
  100 + }
  101 +
  102 + /**
  103 + * 子流程发起人监听器:BPMN 通过 ${bpmCallActivityListener} 引用,需与组件扫描的 Bean 名称一致
  104 + */
  105 + @Bean("bpmCallActivityListener")
  106 + public BpmCallActivityListener bpmCallActivityListener() {
  107 + return new BpmCallActivityListener();
  108 + }
  109 + }
  110 +
  111 + /**
  112 + * 审批人计算的 Mock,属于上下文单例(非 {@link MockitoBean}),每个测试前重置,避免测试间相互影响
  113 + */
  114 + @Resource(name = "testBpmTaskCandidateInvoker")
  115 + protected BpmTaskCandidateInvoker taskCandidateInvoker;
  116 +
  117 + @BeforeEach
  118 + public void resetTaskCandidateInvoker() {
  119 + // 默认:任务的审批人为用户 1
  120 + reset(taskCandidateInvoker);
  121 + when(taskCandidateInvoker.calculateUsersByTask(any())).thenReturn(Set.of(1L));
  122 + }
  123 +
  124 + /**
  125 + * 将流程定义、模型、用户相关的 Mock 委托给真实 Flowable 引擎,模拟业务表中已有对应数据
  126 + *
  127 + * <p>流程定义信息默认为允许撤回的 BPMN 流程;用户编号为 id 时,昵称为「用户 + id」。</p>
  128 + */
  129 + @SuppressWarnings("unchecked")
  130 + protected void mockFlowableBackedServices(RepositoryService repositoryService) {
  131 + when(processDefinitionService.getActiveProcessDefinition(anyString())).thenAnswer(invocation ->
  132 + repositoryService.createProcessDefinitionQuery().processDefinitionKey(invocation.getArgument(0))
  133 + .latestVersion().singleResult());
  134 + when(processDefinitionService.getProcessDefinition(anyString())).thenAnswer(invocation ->
  135 + repositoryService.createProcessDefinitionQuery().processDefinitionId(invocation.getArgument(0))
  136 + .singleResult());
  137 + when(processDefinitionService.getProcessDefinitionBpmnModel(anyString())).thenAnswer(invocation ->
  138 + repositoryService.getBpmnModel(invocation.getArgument(0)));
  139 + when(processDefinitionService.getProcessDefinitionInfo(anyString())).thenAnswer(invocation ->
  140 + new BpmProcessDefinitionInfoDO().setProcessDefinitionId(invocation.getArgument(0))
  141 + .setModelType(BpmModelTypeEnum.BPMN.getType()).setAllowWithdrawTask(true));
  142 + when(processDefinitionService.canUserStartProcessDefinition(any(), any())).thenReturn(true);
  143 + when(modelService.getBpmnModelByDefinitionId(anyString())).thenAnswer(invocation ->
  144 + repositoryService.getBpmnModel(invocation.getArgument(0)));
  145 + when(adminUserApi.getUser(any())).thenAnswer(invocation -> buildUser(invocation.getArgument(0)));
  146 + when(adminUserApi.getUserList(anyCollection())).thenAnswer(invocation ->
  147 + ((Collection<Long>) invocation.getArgument(0)).stream().map(BaseFlowableUnitTest::buildUser).toList());
  148 + when(adminUserApi.getUserMap(anyCollection())).thenAnswer(invocation ->
  149 + ((Collection<Long>) invocation.getArgument(0)).stream()
  150 + .collect(Collectors.toMap(Function.identity(), BaseFlowableUnitTest::buildUser)));
  151 + }
  152 +
  153 + private static AdminUserRespDTO buildUser(Long id) {
  154 + return new AdminUserRespDTO().setId(id).setNickname("用户" + id);
  155 + }
  156 +
  157 +}
... ...
yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BpmModelDeployFlowableTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.bpm.framework.flowable;
  2 +
  3 +import cn.iocoder.yudao.framework.common.exception.ErrorCode;
  4 +import cn.iocoder.yudao.framework.common.exception.ServiceException;
  5 +import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
  6 +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelSaveReqVO;
  7 +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
  8 +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskApproveReqVO;
  9 +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
  10 +import cn.iocoder.yudao.module.bpm.dal.mysql.definition.BpmProcessDefinitionInfoMapper;
  11 +import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
  12 +import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum;
  13 +import cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeTypeEnum;
  14 +import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskApproveMethodEnum;
  15 +import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskApproveTypeEnum;
  16 +import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum;
  17 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
  18 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
  19 +import cn.iocoder.yudao.module.bpm.service.definition.BpmModelServiceImpl;
  20 +import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionServiceImpl;
  21 +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceServiceImpl;
  22 +import cn.iocoder.yudao.module.bpm.service.task.BpmTaskServiceImpl;
  23 +import jakarta.annotation.Resource;
  24 +import org.flowable.common.engine.impl.db.SuspensionState;
  25 +import org.flowable.engine.HistoryService;
  26 +import org.flowable.engine.RepositoryService;
  27 +import org.flowable.engine.RuntimeService;
  28 +import org.flowable.engine.TaskService;
  29 +import org.flowable.engine.repository.Model;
  30 +import org.flowable.engine.repository.ProcessDefinition;
  31 +import org.flowable.task.api.Task;
  32 +import org.junit.jupiter.api.AfterEach;
  33 +import org.junit.jupiter.api.BeforeEach;
  34 +import org.junit.jupiter.api.Test;
  35 +import org.springframework.test.context.jdbc.Sql;
  36 +import org.springframework.test.util.ReflectionTestUtils;
  37 +
  38 +import java.util.List;
  39 +
  40 +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
  41 +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.MODEL_UPDATE_FAIL_NOT_MANAGER;
  42 +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_DEFINITION_NOT_EXISTS;
  43 +import static org.junit.jupiter.api.Assertions.*;
  44 +import static org.mockito.AdditionalAnswers.delegatesTo;
  45 +import static org.mockito.ArgumentMatchers.any;
  46 +import static org.mockito.ArgumentMatchers.anyString;
  47 +import static org.mockito.Mockito.doAnswer;
  48 +import static org.mockito.Mockito.verify;
  49 +
  50 +/**
  51 + * 流程模型部署、挂起、排序、清理、删除的 Flowable 集成测试
  52 + *
  53 + * <p>使用真实的 {@link BpmModelServiceImpl}、{@link BpmProcessDefinitionServiceImpl}(Flowable + bpm_process_definition_info 表),
  54 + * 并让流程实例 Service 依赖的 {@link #processDefinitionService} 委托给真实实现,验证【模型部署 → 发起 → 审批】的完整闭环。</p>
  55 + *
  56 + * @author HUIHUI
  57 + */
  58 +@Sql(scripts = "/sql/flowable/create_tables.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
  59 +@Sql(scripts = "/sql/flowable/clean.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
  60 +public class BpmModelDeployFlowableTest extends BaseFlowableUnitTest {
  61 +
  62 + private static final String MODEL_KEY = "modelDeploy";
  63 +
  64 + @Resource
  65 + private RepositoryService repositoryService;
  66 + @Resource
  67 + private RuntimeService runtimeService;
  68 + @Resource
  69 + private HistoryService historyService;
  70 + @Resource
  71 + private TaskService flowableTaskService;
  72 + @Resource
  73 + private BpmProcessDefinitionInfoMapper processDefinitionMapper;
  74 +
  75 + @Resource
  76 + private BpmTaskServiceImpl taskService;
  77 + @Resource
  78 + private BpmProcessInstanceServiceImpl processInstanceService;
  79 +
  80 + private final BpmProcessDefinitionServiceImpl processDefinitionServiceImpl = new BpmProcessDefinitionServiceImpl();
  81 + private final BpmModelServiceImpl modelServiceImpl = new BpmModelServiceImpl();
  82 +
  83 + @BeforeEach
  84 + public void setUp() {
  85 + // 真实的流程定义、流程模型 Service,依赖的 Flowable、Mapper 使用 Spring 中的真实 Bean
  86 + ReflectionTestUtils.setField(processDefinitionServiceImpl, "repositoryService", repositoryService);
  87 + ReflectionTestUtils.setField(processDefinitionServiceImpl, "processDefinitionMapper", processDefinitionMapper);
  88 + ReflectionTestUtils.setField(processDefinitionServiceImpl, "adminUserApi", adminUserApi);
  89 + ReflectionTestUtils.setField(modelServiceImpl, "repositoryService", repositoryService);
  90 + ReflectionTestUtils.setField(modelServiceImpl, "runtimeService", runtimeService);
  91 + ReflectionTestUtils.setField(modelServiceImpl, "historyService", historyService);
  92 + ReflectionTestUtils.setField(modelServiceImpl, "taskService", flowableTaskService);
  93 + ReflectionTestUtils.setField(modelServiceImpl, "processDefinitionService", processDefinitionServiceImpl);
  94 + ReflectionTestUtils.setField(modelServiceImpl, "bpmFormService", formService);
  95 + ReflectionTestUtils.setField(modelServiceImpl, "taskCandidateInvoker", taskCandidateInvoker);
  96 + ReflectionTestUtils.setField(modelServiceImpl, "processInstanceCopyService", processInstanceCopyService);
  97 +
  98 + // 发起、审批流程时,流程定义相关的查询委托给真实实现,读取部署后写入的 bpm_process_definition_info
  99 + mockFlowableBackedServices(repositoryService);
  100 + doAnswer(delegatesTo(processDefinitionServiceImpl)).when(processDefinitionService)
  101 + .getActiveProcessDefinition(anyString());
  102 + doAnswer(delegatesTo(processDefinitionServiceImpl)).when(processDefinitionService)
  103 + .getProcessDefinition(anyString());
  104 + doAnswer(delegatesTo(processDefinitionServiceImpl)).when(processDefinitionService)
  105 + .getProcessDefinitionInfo(anyString());
  106 + doAnswer(delegatesTo(processDefinitionServiceImpl)).when(processDefinitionService)
  107 + .canUserStartProcessDefinition(any(), any());
  108 + }
  109 +
  110 + @AfterEach
  111 + public void tearDown() {
  112 + repositoryService.createDeploymentQuery().deploymentKey(MODEL_KEY).list()
  113 + .forEach(deployment -> repositoryService.deleteDeployment(deployment.getId(), true));
  114 + repositoryService.createModelQuery().modelKey(MODEL_KEY).list()
  115 + .forEach(model -> repositoryService.deleteModel(model.getId()));
  116 + }
  117 +
  118 + @Test
  119 + public void testDeployModel_redeploy() {
  120 + // 准备参数:创建 Simple 模型
  121 + String modelId = createModel();
  122 +
  123 + // 调用:首次部署
  124 + modelServiceImpl.deployModel(1L, modelId);
  125 + // 断言:生成流程定义及拓展信息,模型关联最新部署
  126 + ProcessDefinition definition1 = getActiveProcessDefinition();
  127 + assertEquals("模型部署", definition1.getName());
  128 + assertEquals("OA", definition1.getCategory());
  129 + assertEquals(definition1.getDeploymentId(), repositoryService.getModel(modelId).getDeploymentId());
  130 + BpmProcessDefinitionInfoDO definitionInfo1 = processDefinitionMapper.selectByProcessDefinitionId(definition1.getId());
  131 + assertEquals(modelId, definitionInfo1.getModelId());
  132 + assertEquals(BpmModelTypeEnum.SIMPLE.getType(), definitionInfo1.getModelType());
  133 + assertEquals(List.of(1L), definitionInfo1.getManagerUserIds());
  134 + assertNotNull(definitionInfo1.getSimpleModel());
  135 + // 断言:可基于该流程定义发起流程,停留在审批节点
  136 + String processInstanceId1 = startProcessInstance();
  137 + Task task1 = getRunningTask(processInstanceId1);
  138 + assertEquals(definition1.getId(), task1.getProcessDefinitionId());
  139 + assertEquals("approve", task1.getTaskDefinitionKey());
  140 +
  141 + // 调用:再次部署
  142 + modelServiceImpl.deployModel(1L, modelId);
  143 + // 断言:老的流程定义被挂起,新发起的流程使用新的流程定义
  144 + ProcessDefinition definition2 = getActiveProcessDefinition();
  145 + assertNotEquals(definition1.getId(), definition2.getId());
  146 + assertTrue(repositoryService.getProcessDefinition(definition1.getId()).isSuspended());
  147 + assertEquals(definition2.getDeploymentId(), repositoryService.getModel(modelId).getDeploymentId());
  148 + String processInstanceId2 = startProcessInstance();
  149 + assertEquals(definition2.getId(), getRunningTask(processInstanceId2).getProcessDefinitionId());
  150 +
  151 + // 调用 + 断言:老流程定义上进行中的流程不受挂起影响,仍可审批通过
  152 + approveTask(task1);
  153 + assertProcessInstanceStatus(processInstanceId1, BpmProcessInstanceStatusEnum.APPROVE.getStatus());
  154 + approveTask(getRunningTask(processInstanceId2));
  155 + assertProcessInstanceStatus(processInstanceId2, BpmProcessInstanceStatusEnum.APPROVE.getStatus());
  156 +
  157 + // 调用 + 断言:调整排序,同一模型的所有流程定义拓展信息同步更新
  158 + modelServiceImpl.updateModelSortBatch(1L, List.of(modelId));
  159 + Long sort = processDefinitionMapper.selectByProcessDefinitionId(definition2.getId()).getSort();
  160 + assertEquals(sort, processDefinitionMapper.selectByProcessDefinitionId(definition1.getId()).getSort());
  161 + assertTrue(sort > definitionInfo1.getSort());
  162 + }
  163 +
  164 + @Test
  165 + public void testDeployModel_notManager() {
  166 + // 准备参数
  167 + String modelId = createModel();
  168 +
  169 + // 调用,并断言异常:非模型管理员不允许部署
  170 + assertServiceException(() -> modelServiceImpl.deployModel(2L, modelId), MODEL_UPDATE_FAIL_NOT_MANAGER, "模型部署");
  171 + assertEquals(0, repositoryService.createProcessDefinitionQuery().processDefinitionKey(MODEL_KEY).count());
  172 + }
  173 +
  174 + @Test
  175 + public void testUpdateModelState_suspendAndActive() {
  176 + // 准备参数
  177 + String modelId = createModel();
  178 + modelServiceImpl.deployModel(1L, modelId);
  179 + String processDefinitionId = getActiveProcessDefinition().getId();
  180 +
  181 + // 调用 + 断言:挂起后不允许发起
  182 + modelServiceImpl.updateModelState(1L, modelId, SuspensionState.SUSPENDED.getStateCode());
  183 + assertTrue(repositoryService.getProcessDefinition(processDefinitionId).isSuspended());
  184 + assertStartProcessInstanceFail(PROCESS_DEFINITION_NOT_EXISTS);
  185 + // 调用 + 断言:激活后恢复发起
  186 + modelServiceImpl.updateModelState(1L, modelId, SuspensionState.ACTIVE.getStateCode());
  187 + assertFalse(repositoryService.getProcessDefinition(processDefinitionId).isSuspended());
  188 + assertEquals("approve", getRunningTask(startProcessInstance()).getTaskDefinitionKey());
  189 + }
  190 +
  191 + @Test
  192 + public void testCleanModel_success() {
  193 + // 准备参数:一个进行中的流程,一个已结束的流程
  194 + String modelId = createModel();
  195 + modelServiceImpl.deployModel(1L, modelId);
  196 + String runningId = startProcessInstance();
  197 + String finishedId = startProcessInstance();
  198 + approveTask(getRunningTask(finishedId));
  199 + assertProcessInstanceStatus(finishedId, BpmProcessInstanceStatusEnum.APPROVE.getStatus());
  200 +
  201 + // 调用
  202 + modelServiceImpl.cleanModel(1L, modelId);
  203 + // 断言:流程实例、历史、任务、抄送全部清理,模型与流程定义保留
  204 + assertEquals(0, runtimeService.createProcessInstanceQuery().processDefinitionKey(MODEL_KEY).count());
  205 + assertEquals(0, historyService.createHistoricProcessInstanceQuery().processDefinitionKey(MODEL_KEY).count());
  206 + assertEquals(0, flowableTaskService.createTaskQuery().processDefinitionKey(MODEL_KEY).count());
  207 + verify(processInstanceCopyService).deleteProcessInstanceCopy(runningId);
  208 + verify(processInstanceCopyService).deleteProcessInstanceCopy(finishedId);
  209 + assertNotNull(repositoryService.getModel(modelId));
  210 + assertNotNull(getActiveProcessDefinition());
  211 + }
  212 +
  213 + @Test
  214 + public void testDeleteModel_success() {
  215 + // 准备参数:一个进行中的流程
  216 + String modelId = createModel();
  217 + modelServiceImpl.deployModel(1L, modelId);
  218 + String processDefinitionId = getActiveProcessDefinition().getId();
  219 + String processInstanceId = startProcessInstance();
  220 +
  221 + // 调用
  222 + modelServiceImpl.deleteModel(1L, modelId);
  223 + // 断言:模型被删除,流程定义被挂起,不允许再发起
  224 + assertNull(repositoryService.getModel(modelId));
  225 + assertTrue(repositoryService.getProcessDefinition(processDefinitionId).isSuspended());
  226 + assertStartProcessInstanceFail(PROCESS_DEFINITION_NOT_EXISTS);
  227 + // 断言:进行中的流程不受影响,仍可审批通过
  228 + approveTask(getRunningTask(processInstanceId));
  229 + assertProcessInstanceStatus(processInstanceId, BpmProcessInstanceStatusEnum.APPROVE.getStatus());
  230 + }
  231 +
  232 + // ========== 私有方法 ==========
  233 +
  234 + /**
  235 + * 创建 Simple 模型:发起人 -> 审批(用户 1)-> 结束,管理员为用户 1
  236 + */
  237 + private String createModel() {
  238 + BpmSimpleModelNodeVO approveNode = new BpmSimpleModelNodeVO().setId("approve").setName("审批")
  239 + .setType(BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType())
  240 + .setApproveType(BpmUserTaskApproveTypeEnum.USER.getType())
  241 + .setApproveMethod(BpmUserTaskApproveMethodEnum.RANDOM.getMethod())
  242 + .setCandidateStrategy(BpmTaskCandidateStrategyEnum.USER.getStrategy()).setCandidateParam("1")
  243 + .setChildNode(new BpmSimpleModelNodeVO().setId("end").setType(BpmSimpleModelNodeTypeEnum.END_NODE.getType()));
  244 + BpmSimpleModelNodeVO startUserNode = new BpmSimpleModelNodeVO().setId("start-user").setName("发起人")
  245 + .setType(BpmSimpleModelNodeTypeEnum.START_USER_NODE.getType())
  246 + .setChildNode(approveNode);
  247 + BpmModelSaveReqVO reqVO = new BpmModelSaveReqVO();
  248 + reqVO.setKey(MODEL_KEY);
  249 + reqVO.setName("模型部署");
  250 + reqVO.setCategory("OA");
  251 + reqVO.setSimpleModel(startUserNode);
  252 + reqVO.setType(BpmModelTypeEnum.SIMPLE.getType());
  253 + reqVO.setFormType(BpmModelFormTypeEnum.CUSTOM.getType());
  254 + reqVO.setFormCustomCreatePath("/bpm/oa/leave/create");
  255 + reqVO.setFormCustomViewPath("/bpm/oa/leave/detail");
  256 + reqVO.setVisible(true);
  257 + reqVO.setManagerUserIds(List.of(1L));
  258 + return modelServiceImpl.createModel(reqVO);
  259 + }
  260 +
  261 + private ProcessDefinition getActiveProcessDefinition() {
  262 + ProcessDefinition definition = processDefinitionServiceImpl.getActiveProcessDefinition(MODEL_KEY);
  263 + assertNotNull(definition);
  264 + return definition;
  265 + }
  266 +
  267 + private String startProcessInstance() {
  268 + return processInstanceService.createProcessInstance(1L, new BpmProcessInstanceCreateReqDTO()
  269 + .setProcessDefinitionKey(MODEL_KEY).setBusinessKey("business-1"));
  270 + }
  271 +
  272 + /**
  273 + * 发起流程失败:ServiceException 被 FlowableUtils#executeAuthenticatedUserId 包装,由全局异常处理器解包
  274 + */
  275 + private void assertStartProcessInstanceFail(ErrorCode errorCode) {
  276 + RuntimeException exception = assertThrows(RuntimeException.class, this::startProcessInstance);
  277 + ServiceException serviceException = assertInstanceOf(ServiceException.class, exception.getCause());
  278 + assertEquals(errorCode.getCode(), serviceException.getCode());
  279 + }
  280 +
  281 + private Task getRunningTask(String processInstanceId) {
  282 + List<Task> tasks = taskService.getRunningTaskListByProcessInstanceId(processInstanceId, null, null);
  283 + assertEquals(1, tasks.size());
  284 + return tasks.get(0);
  285 + }
  286 +
  287 + private void approveTask(Task task) {
  288 + taskService.approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId())
  289 + .setReason("同意"));
  290 + }
  291 +
  292 + private void assertProcessInstanceStatus(String processInstanceId, Integer status) {
  293 + assertEquals(status, FlowableUtils.getProcessInstanceStatus(
  294 + processInstanceService.getHistoricProcessInstance(processInstanceId)));
  295 + }
  296 +
  297 +}
... ...
yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BpmMultiInstanceFlowableTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.bpm.framework.flowable;
  2 +
  3 +import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
  4 +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskApproveReqVO;
  5 +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskRejectReqVO;
  6 +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
  7 +import cn.iocoder.yudao.module.bpm.enums.definition.BpmChildProcessMultiInstanceSourceTypeEnum;
  8 +import cn.iocoder.yudao.module.bpm.enums.definition.BpmChildProcessStartUserEmptyTypeEnum;
  9 +import cn.iocoder.yudao.module.bpm.enums.definition.BpmChildProcessStartUserTypeEnum;
  10 +import cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeTypeEnum;
  11 +import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskApproveMethodEnum;
  12 +import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskApproveTypeEnum;
  13 +import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum;
  14 +import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum;
  15 +import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
  16 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior.BpmParallelMultiInstanceBehavior;
  17 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior.BpmSequentialMultiInstanceBehavior;
  18 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
  19 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
  20 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.SimpleModelUtils;
  21 +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceServiceImpl;
  22 +import cn.iocoder.yudao.module.bpm.service.task.BpmTaskServiceImpl;
  23 +import jakarta.annotation.Resource;
  24 +import org.flowable.bpmn.model.BpmnModel;
  25 +import org.flowable.engine.HistoryService;
  26 +import org.flowable.engine.RepositoryService;
  27 +import org.flowable.engine.RuntimeService;
  28 +import org.flowable.engine.delegate.DelegateExecution;
  29 +import org.flowable.engine.history.HistoricProcessInstance;
  30 +import org.flowable.engine.runtime.ProcessInstance;
  31 +import org.flowable.task.api.Task;
  32 +import org.flowable.task.api.history.HistoricTaskInstance;
  33 +import org.junit.jupiter.api.AfterEach;
  34 +import org.junit.jupiter.api.BeforeEach;
  35 +import org.junit.jupiter.api.Test;
  36 +
  37 +import java.util.ArrayList;
  38 +import java.util.Comparator;
  39 +import java.util.HashMap;
  40 +import java.util.LinkedHashSet;
  41 +import java.util.List;
  42 +import java.util.Map;
  43 +import java.util.Set;
  44 +
  45 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  46 +import static org.junit.jupiter.api.Assertions.*;
  47 +import static org.mockito.ArgumentMatchers.any;
  48 +import static org.mockito.Mockito.when;
  49 +
  50 +/**
  51 + * 多实例(会签、或签、依次审批、子流程多实例)的 Flowable 集成测试
  52 + *
  53 + * <p>覆盖 {@link BpmParallelMultiInstanceBehavior}、{@link BpmSequentialMultiInstanceBehavior} 在真实引擎中的实例创建、
  54 + * 审批人分配,以及 {@link BpmTaskServiceImpl} 审批后完成条件的流转。</p>
  55 + *
  56 + * @author HUIHUI
  57 + */
  58 +public class BpmMultiInstanceFlowableTest extends BaseFlowableUnitTest {
  59 +
  60 + @Resource
  61 + private RepositoryService repositoryService;
  62 + @Resource
  63 + private RuntimeService runtimeService;
  64 + @Resource
  65 + private HistoryService historyService;
  66 +
  67 + @Resource
  68 + private BpmTaskServiceImpl taskService;
  69 + @Resource
  70 + private BpmProcessInstanceServiceImpl processInstanceService;
  71 +
  72 + private final List<String> deploymentIds = new ArrayList<>();
  73 +
  74 + @BeforeEach
  75 + public void setUp() {
  76 + deploymentIds.add(repositoryService.createDeployment()
  77 + .addClasspathResource("bpmn/multi-instance/user-task.bpmn20.xml")
  78 + .addClasspathResource("bpmn/multi-instance/call-activity.bpmn20.xml")
  79 + .deploy().getId());
  80 + mockFlowableBackedServices(repositoryService);
  81 + // 多人审批的审批人:用户 1、用户 2(有序)
  82 + when(taskCandidateInvoker.calculateUsersByTask(any())).thenReturn(new LinkedHashSet<>(List.of(1L, 2L)));
  83 + }
  84 +
  85 + @AfterEach
  86 + public void tearDown() {
  87 + deploymentIds.forEach(id -> repositoryService.deleteDeployment(id, true));
  88 + }
  89 +
  90 + // ========== 多人审批 ==========
  91 +
  92 + @Test
  93 + public void testApproveTask_any() {
  94 + // 准备参数
  95 + String processInstanceId = startProcessInstance("multiAny", null);
  96 + List<Task> tasks = getRunningTasks(processInstanceId);
  97 + assertEquals(List.of("1", "2"), convertList(tasks, Task::getAssignee));
  98 +
  99 + // 调用:用户 1 审批通过
  100 + approveTask(tasks.get(0));
  101 + // 断言:或签一人通过即可,用户 2 的任务被取消,流程审批通过
  102 + assertTaskStatus(tasks.get(1).getId(), BpmTaskStatusEnum.CANCEL.getStatus(),
  103 + BpmReasonEnum.CANCEL_BY_SYSTEM.getReason());
  104 + assertProcessInstanceStatus(processInstanceId, BpmProcessInstanceStatusEnum.APPROVE.getStatus());
  105 + }
  106 +
  107 + @Test
  108 + public void testRejectTask_any() {
  109 + // 准备参数
  110 + String processInstanceId = startProcessInstance("multiAny", null);
  111 + List<Task> tasks = getRunningTasks(processInstanceId);
  112 +
  113 + // 调用:用户 1 审批不通过
  114 + taskService.rejectTask(1L, new BpmTaskRejectReqVO().setId(tasks.get(0).getId()).setReason("不同意"));
  115 + // 断言:或签一人不通过即可,用户 2 的任务被取消,流程不通过
  116 + assertTaskStatus(tasks.get(0).getId(), BpmTaskStatusEnum.REJECT.getStatus(), "不同意");
  117 + assertTaskStatus(tasks.get(1).getId(), BpmTaskStatusEnum.CANCEL.getStatus(),
  118 + BpmReasonEnum.CANCEL_BY_SYSTEM.getReason());
  119 + assertTrue(getRunningTasks(processInstanceId).isEmpty());
  120 + assertProcessInstanceStatus(processInstanceId, BpmProcessInstanceStatusEnum.REJECT.getStatus());
  121 + }
  122 +
  123 + @Test
  124 + public void testApproveTask_ratio() {
  125 + // 准备参数
  126 + String processInstanceId = startProcessInstance("multiRatio", null);
  127 + List<Task> tasks = getRunningTasks(processInstanceId);
  128 + assertEquals(List.of("1", "2"), convertList(tasks, Task::getAssignee));
  129 +
  130 + // 调用 + 断言:用户 1 审批通过,仍需等待用户 2 审批
  131 + approveTask(tasks.get(0));
  132 + assertEquals(List.of(tasks.get(1).getId()), convertList(getRunningTasks(processInstanceId), Task::getId));
  133 + // 调用 + 断言:用户 2 审批通过,流程审批通过
  134 + approveTask(tasks.get(1));
  135 + assertProcessInstanceStatus(processInstanceId, BpmProcessInstanceStatusEnum.APPROVE.getStatus());
  136 + }
  137 +
  138 + @Test
  139 + public void testApproveTask_ratioPartial() {
  140 + // 准备参数:通过比例 60%,审批人为用户 1、用户 2、用户 3
  141 + when(taskCandidateInvoker.calculateUsersByTask(any())).thenReturn(new LinkedHashSet<>(List.of(1L, 2L, 3L)));
  142 + String processInstanceId = startProcessInstance("multiRatioPartial", null);
  143 + List<Task> tasks = getRunningTasks(processInstanceId);
  144 + assertEquals(List.of("1", "2", "3"), convertList(tasks, Task::getAssignee));
  145 +
  146 + // 调用 + 断言:用户 1 审批通过(1/3 < 60%),仍需等待其他人审批
  147 + approveTask(tasks.get(0));
  148 + assertEquals(List.of(tasks.get(1).getId(), tasks.get(2).getId()),
  149 + convertList(getRunningTasks(processInstanceId), Task::getId));
  150 + // 调用 + 断言:用户 2 审批通过(2/3 >= 60%),用户 3 的任务被取消,流程审批通过
  151 + approveTask(tasks.get(1));
  152 + assertTaskStatus(tasks.get(2).getId(), BpmTaskStatusEnum.CANCEL.getStatus(),
  153 + BpmReasonEnum.CANCEL_BY_SYSTEM.getReason());
  154 + assertProcessInstanceStatus(processInstanceId, BpmProcessInstanceStatusEnum.APPROVE.getStatus());
  155 + }
  156 +
  157 + @Test
  158 + public void testRejectTask_ratio() {
  159 + // 准备参数:用户 1 已审批通过
  160 + String processInstanceId = startProcessInstance("multiRatio", null);
  161 + List<Task> tasks = getRunningTasks(processInstanceId);
  162 + approveTask(tasks.get(0));
  163 +
  164 + // 调用:用户 2 审批不通过
  165 + taskService.rejectTask(2L, new BpmTaskRejectReqVO().setId(tasks.get(1).getId()).setReason("不同意"));
  166 + // 断言:会签任一人不通过,流程不通过
  167 + assertTaskStatus(tasks.get(0).getId(), BpmTaskStatusEnum.APPROVE.getStatus(), "同意");
  168 + assertTaskStatus(tasks.get(1).getId(), BpmTaskStatusEnum.REJECT.getStatus(), "不同意");
  169 + assertProcessInstanceStatus(processInstanceId, BpmProcessInstanceStatusEnum.REJECT.getStatus());
  170 + }
  171 +
  172 + @Test
  173 + public void testRejectTask_ratioReturn() {
  174 + // 准备参数:发起审批节点由用户 1 审批,会签节点由用户 1、用户 2 审批
  175 + when(taskCandidateInvoker.calculateUsersByTask(any())).thenAnswer(invocation ->
  176 + "multiReturn-task".equals(((DelegateExecution) invocation.getArgument(0)).getCurrentActivityId())
  177 + ? new LinkedHashSet<>(List.of(1L, 2L)) : Set.of(1L));
  178 + String processInstanceId = startProcessInstance("multiReturn", null);
  179 + approveTask(getRunningTask(processInstanceId));
  180 + List<Task> tasks = getRunningTasks(processInstanceId);
  181 + assertEquals(List.of("1", "2"), convertList(tasks, Task::getAssignee));
  182 +
  183 + // 调用:用户 2 审批不通过,按配置退回到发起审批节点
  184 + taskService.rejectTask(2L, new BpmTaskRejectReqVO().setId(tasks.get(1).getId()).setReason("退回修改"));
  185 + // 断言:用户 2 的任务标记为退回,用户 1 的会签任务被取消,流程回到发起审批节点
  186 + assertTaskStatus(tasks.get(1).getId(), BpmTaskStatusEnum.RETURN.getStatus(), "退回修改");
  187 + assertTaskStatus(tasks.get(0).getId(), BpmTaskStatusEnum.CANCEL.getStatus(),
  188 + BpmReasonEnum.CANCEL_BY_SYSTEM.getReason());
  189 + Task firstTask = getRunningTask(processInstanceId);
  190 + assertEquals("multiReturn-first", firstTask.getTaskDefinitionKey());
  191 +
  192 + // 调用 + 断言:再次审批发起审批节点后,重新生成用户 1、用户 2 的会签任务
  193 + approveTask(firstTask);
  194 + List<Task> newTasks = getRunningTasks(processInstanceId);
  195 + assertEquals(List.of("1", "2"), convertList(newTasks, Task::getAssignee));
  196 + assertEquals(List.of("multiReturn-task", "multiReturn-task"), convertList(newTasks, Task::getTaskDefinitionKey));
  197 + // 调用 + 断言:全部审批通过后,流程审批通过
  198 + newTasks.forEach(this::approveTask);
  199 + assertProcessInstanceStatus(processInstanceId, BpmProcessInstanceStatusEnum.APPROVE.getStatus());
  200 + }
  201 +
  202 + @Test
  203 + public void testApproveTask_sequential() {
  204 + // 准备参数
  205 + String processInstanceId = startProcessInstance("multiSequential", null);
  206 + Task task1 = getRunningTask(processInstanceId);
  207 + assertEquals("1", task1.getAssignee());
  208 +
  209 + // 调用 + 断言:用户 1 审批通过后,才轮到用户 2
  210 + approveTask(task1);
  211 + Task task2 = getRunningTask(processInstanceId);
  212 + assertEquals("2", task2.getAssignee());
  213 + // 调用 + 断言:用户 2 审批通过,流程审批通过
  214 + approveTask(task2);
  215 + assertProcessInstanceStatus(processInstanceId, BpmProcessInstanceStatusEnum.APPROVE.getStatus());
  216 + }
  217 +
  218 + // ========== 子流程多实例 ==========
  219 +
  220 + @Test
  221 + public void testCallActivity_fixedQuantity() {
  222 + // 调用
  223 + String processInstanceId = startProcessInstance("multiChildFixedQuantity", null);
  224 + // 断言:并行发起 2 个子流程,全部审批通过后,主流程结束
  225 + List<ProcessInstance> children = getChildProcessInstances(processInstanceId);
  226 + assertEquals(2, children.size());
  227 + children.forEach(child -> approveTask(getRunningTask(child.getId())));
  228 + assertProcessInstanceStatus(processInstanceId, BpmProcessInstanceStatusEnum.APPROVE.getStatus());
  229 + }
  230 +
  231 + @Test
  232 + public void testCallActivity_numberForm() {
  233 + // 调用:表单 count = 3
  234 + String processInstanceId = startProcessInstance("multiChildNumberForm", new HashMap<>(Map.of("count", 3)));
  235 + // 断言:并行发起 3 个子流程,全部审批通过后,主流程结束
  236 + List<ProcessInstance> children = getChildProcessInstances(processInstanceId);
  237 + assertEquals(3, children.size());
  238 + children.forEach(child -> approveTask(getRunningTask(child.getId())));
  239 + assertProcessInstanceStatus(processInstanceId, BpmProcessInstanceStatusEnum.APPROVE.getStatus());
  240 + }
  241 +
  242 + @Test
  243 + public void testCallActivity_multipleFormSequential() {
  244 + // 调用:表单 users 选择了 2 个用户
  245 + String processInstanceId = startProcessInstance("multiChildMultipleForm",
  246 + new HashMap<>(Map.of("users", List.of(1L, 2L))));
  247 + // 断言:依次发起子流程,前一个子流程结束后才发起下一个
  248 + for (int i = 0; i < 2; i++) {
  249 + List<ProcessInstance> children = getChildProcessInstances(processInstanceId);
  250 + assertEquals(1, children.size());
  251 + approveTask(getRunningTask(children.get(0).getId()));
  252 + }
  253 + assertEquals(2, historyService.createHistoricProcessInstanceQuery()
  254 + .superProcessInstanceId(processInstanceId).count());
  255 + assertProcessInstanceStatus(processInstanceId, BpmProcessInstanceStatusEnum.APPROVE.getStatus());
  256 + }
  257 +
  258 + // ========== Simple 设计器模型 ==========
  259 +
  260 + @Test
  261 + public void testSimpleModel_ratioApproveThenChildProcess() {
  262 + // 准备参数:由 SimpleModelUtils 生成【会签 -> 子流程(固定数量 2 个)】的流程,与 Simple 设计器保存后部署的模型一致
  263 + BpmSimpleModelNodeVO childProcessNode = new BpmSimpleModelNodeVO().setId("child").setName("子流程")
  264 + .setType(BpmSimpleModelNodeTypeEnum.CHILD_PROCESS.getType())
  265 + .setChildProcessSetting(new BpmSimpleModelNodeVO.ChildProcessSetting()
  266 + .setCalledProcessDefinitionKey("multiChild").setCalledProcessDefinitionName("子流程")
  267 + .setAsync(false).setSkipStartUserNode(false)
  268 + .setStartUserSetting(new BpmSimpleModelNodeVO.ChildProcessSetting.StartUserSetting()
  269 + .setType(BpmChildProcessStartUserTypeEnum.MAIN_PROCESS_START_USER.getType())
  270 + .setEmptyType(BpmChildProcessStartUserEmptyTypeEnum.MAIN_PROCESS_START_USER.getType()))
  271 + .setMultiInstanceSetting(new BpmSimpleModelNodeVO.ChildProcessSetting.MultiInstanceSetting()
  272 + .setEnable(true).setSequential(false).setApproveRatio(100)
  273 + .setSourceType(BpmChildProcessMultiInstanceSourceTypeEnum.FIXED_QUANTITY.getType())
  274 + .setSource("2")))
  275 + .setChildNode(new BpmSimpleModelNodeVO().setId("end").setType(BpmSimpleModelNodeTypeEnum.END_NODE.getType()));
  276 + BpmSimpleModelNodeVO approveNode = new BpmSimpleModelNodeVO().setId("approve").setName("会签审批")
  277 + .setType(BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType())
  278 + .setApproveType(BpmUserTaskApproveTypeEnum.USER.getType())
  279 + .setApproveMethod(BpmUserTaskApproveMethodEnum.RATIO.getMethod()).setApproveRatio(100)
  280 + .setCandidateStrategy(BpmTaskCandidateStrategyEnum.USER.getStrategy()).setCandidateParam("1,2")
  281 + .setChildNode(childProcessNode);
  282 + BpmnModel bpmnModel = SimpleModelUtils.buildBpmnModel("simpleMulti", "Simple 多实例", approveNode);
  283 + deploymentIds.add(repositoryService.createDeployment().addBpmnModel("simpleMulti.bpmn20.xml", bpmnModel)
  284 + .deploy().getId());
  285 + String processInstanceId = startProcessInstance("simpleMulti", null);
  286 +
  287 + // 调用 + 断言:会签需要用户 1、用户 2 都审批通过
  288 + List<Task> tasks = getRunningTasks(processInstanceId);
  289 + assertEquals(List.of("1", "2"), convertList(tasks, Task::getAssignee));
  290 + tasks.forEach(this::approveTask);
  291 + // 调用 + 断言:并行发起 2 个子流程,全部审批通过后,主流程结束
  292 + List<ProcessInstance> children = getChildProcessInstances(processInstanceId);
  293 + assertEquals(2, children.size());
  294 + children.forEach(child -> approveTask(getRunningTask(child.getId())));
  295 + assertProcessInstanceStatus(processInstanceId, BpmProcessInstanceStatusEnum.APPROVE.getStatus());
  296 + }
  297 +
  298 + // ========== 私有方法 ==========
  299 +
  300 + private String startProcessInstance(String processDefinitionKey, Map<String, Object> variables) {
  301 + return processInstanceService.createProcessInstance(1L, new BpmProcessInstanceCreateReqDTO()
  302 + .setProcessDefinitionKey(processDefinitionKey).setVariables(variables));
  303 + }
  304 +
  305 + private List<ProcessInstance> getChildProcessInstances(String processInstanceId) {
  306 + return runtimeService.createProcessInstanceQuery().superProcessInstanceId(processInstanceId).list();
  307 + }
  308 +
  309 + private List<Task> getRunningTasks(String processInstanceId) {
  310 + // 按审批人排序,便于断言
  311 + return taskService.getRunningTaskListByProcessInstanceId(processInstanceId, null, null).stream()
  312 + .sorted(Comparator.comparing(Task::getAssignee)).toList();
  313 + }
  314 +
  315 + private Task getRunningTask(String processInstanceId) {
  316 + List<Task> tasks = getRunningTasks(processInstanceId);
  317 + assertEquals(1, tasks.size());
  318 + return tasks.get(0);
  319 + }
  320 +
  321 + private void approveTask(Task task) {
  322 + taskService.approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId())
  323 + .setReason("同意"));
  324 + }
  325 +
  326 + private void assertTaskStatus(String taskId, Integer status, String reason) {
  327 + HistoricTaskInstance task = taskService.getHistoricTask(taskId);
  328 + assertEquals(status, FlowableUtils.getTaskStatus(task));
  329 + assertEquals(reason, FlowableUtils.getTaskReason(task));
  330 + }
  331 +
  332 + private void assertProcessInstanceStatus(String processInstanceId, Integer status) {
  333 + HistoricProcessInstance processInstance = processInstanceService.getHistoricProcessInstance(processInstanceId);
  334 + assertNotNull(processInstance.getEndTime());
  335 + assertEquals(status, FlowableUtils.getProcessInstanceStatus(processInstance));
  336 + }
  337 +
  338 +}
... ...
yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BpmProcessDefinitionServiceFlowableTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.bpm.framework.flowable;
  2 +
  3 +import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionServiceImpl;
  4 +import jakarta.annotation.Resource;
  5 +import org.flowable.engine.RepositoryService;
  6 +import org.flowable.engine.repository.ProcessDefinition;
  7 +import org.junit.jupiter.api.AfterEach;
  8 +import org.junit.jupiter.api.BeforeEach;
  9 +import org.junit.jupiter.api.Test;
  10 +import org.springframework.test.util.ReflectionTestUtils;
  11 +
  12 +import static org.junit.jupiter.api.Assertions.*;
  13 +
  14 +/**
  15 + * {@link BpmProcessDefinitionServiceImpl} 依赖 Flowable 引擎的集成测试
  16 + *
  17 + * <p>基类将 BpmProcessDefinitionService 声明为 Mock,这里直接构造真实实现,并注入真实的 {@link RepositoryService}。</p>
  18 + *
  19 + * @author HUIHUI
  20 + */
  21 +public class BpmProcessDefinitionServiceFlowableTest extends BaseFlowableUnitTest {
  22 +
  23 + @Resource
  24 + private RepositoryService repositoryService;
  25 +
  26 + private final BpmProcessDefinitionServiceImpl processDefinitionServiceImpl = new BpmProcessDefinitionServiceImpl();
  27 +
  28 + private String deploymentId;
  29 +
  30 + @BeforeEach
  31 + public void setUp() {
  32 + ReflectionTestUtils.setField(processDefinitionServiceImpl, "repositoryService", repositoryService);
  33 + deploymentId = repositoryService.createDeployment()
  34 + .addClasspathResource("bpmn/simple-approve.bpmn20.xml")
  35 + .deploy().getId();
  36 + }
  37 +
  38 + @AfterEach
  39 + public void tearDown() {
  40 + repositoryService.deleteDeployment(deploymentId, true);
  41 + }
  42 +
  43 + @Test
  44 + public void testGetProcessDefinition_success() {
  45 + // 准备参数
  46 + String id = repositoryService.createProcessDefinitionQuery().deploymentId(deploymentId).singleResult().getId();
  47 +
  48 + // 调用
  49 + ProcessDefinition processDefinition = processDefinitionServiceImpl.getProcessDefinition(id);
  50 + // 断言
  51 + assertEquals("simpleApprove", processDefinition.getKey());
  52 + }
  53 +
  54 + @Test
  55 + public void testGetProcessDefinition_notExists() {
  56 + // 调用,并断言:不存在时返回 null,而不是抛出 Flowable 异常,调用方据此提示“流程定义不存在”
  57 + assertNull(processDefinitionServiceImpl.getProcessDefinition("not-exists"));
  58 + assertNull(processDefinitionServiceImpl.getProcessDefinition(null));
  59 + }
  60 +
  61 +}
... ...
yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BpmProcessInstanceQueryFlowableTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.bpm.framework.flowable;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  4 +import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
  5 +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailReqVO;
  6 +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailRespVO;
  7 +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailRespVO.ActivityNode;
  8 +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceBpmnModelViewRespVO;
  9 +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstancePageReqVO;
  10 +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskApproveReqVO;
  11 +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskRejectReqVO;
  12 +import cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeTypeEnum;
  13 +import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum;
  14 +import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
  15 +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceServiceImpl;
  16 +import cn.iocoder.yudao.module.bpm.service.task.BpmTaskServiceImpl;
  17 +import jakarta.annotation.Resource;
  18 +import org.flowable.engine.RepositoryService;
  19 +import org.flowable.engine.RuntimeService;
  20 +import org.flowable.engine.TaskService;
  21 +import org.flowable.engine.history.HistoricProcessInstance;
  22 +import org.flowable.engine.repository.Deployment;
  23 +import org.flowable.engine.runtime.ProcessInstance;
  24 +import org.flowable.task.api.Task;
  25 +import org.junit.jupiter.api.AfterEach;
  26 +import org.junit.jupiter.api.BeforeEach;
  27 +import org.junit.jupiter.api.Test;
  28 +
  29 +import java.time.LocalDateTime;
  30 +import java.util.*;
  31 +
  32 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  33 +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
  34 +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_DEFINITION_NOT_EXISTS;
  35 +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_INSTANCE_NOT_EXISTS;
  36 +import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.START_USER_NODE_ID;
  37 +import static org.junit.jupiter.api.Assertions.*;
  38 +import static org.mockito.ArgumentMatchers.any;
  39 +import static org.mockito.Mockito.when;
  40 +
  41 +/**
  42 + * {@link BpmProcessInstanceServiceImpl} 查询相关方法的 Flowable 集成测试
  43 + *
  44 + * <p>流程实例、任务的状态由真实的 Flowable 事件监听器维护,覆盖审批详情、流程图进度、分页、下一节点预测等基于历史数据的查询。</p>
  45 + *
  46 + * @author HUIHUI
  47 + */
  48 +public class BpmProcessInstanceQueryFlowableTest extends BaseFlowableUnitTest {
  49 +
  50 + @Resource
  51 + private RepositoryService repositoryService;
  52 + @Resource
  53 + private RuntimeService runtimeService;
  54 + @Resource
  55 + private TaskService flowableTaskService;
  56 +
  57 + @Resource
  58 + private BpmTaskServiceImpl taskService;
  59 + @Resource
  60 + private BpmProcessInstanceServiceImpl processInstanceService;
  61 +
  62 + private final List<String> deploymentIds = new ArrayList<>();
  63 +
  64 + @BeforeEach
  65 + public void setUp() {
  66 + // 两级审批流程:task1 -> task2;审批人均为用户 1,候选人预测同样为用户 1
  67 + deploy("bpmn/two-step-approve.bpmn20.xml");
  68 + mockFlowableBackedServices(repositoryService);
  69 + when(taskCandidateInvoker.calculateUsersByActivity(any(), any(), any(), any(), any()))
  70 + .thenReturn(new LinkedHashSet<>(List.of(1L)));
  71 + }
  72 +
  73 + @AfterEach
  74 + public void tearDown() {
  75 + deploymentIds.forEach(id -> repositoryService.deleteDeployment(id, true));
  76 + }
  77 +
  78 + // ========== getApprovalDetail ==========
  79 +
  80 + @Test
  81 + public void testGetApprovalDetail_running() {
  82 + // 准备参数:task1 已审批,task2 待审批
  83 + String processInstanceId = startProcessInstance(1L, null);
  84 + approveRunningTask(processInstanceId);
  85 + Task task2 = getRunningTask(processInstanceId);
  86 +
  87 + // 调用
  88 + BpmApprovalDetailRespVO respVO = processInstanceService.getApprovalDetail(1L,
  89 + new BpmApprovalDetailReqVO().setProcessInstanceId(processInstanceId));
  90 + // 断言:已结束(发起人、task1) + 进行中(task2) + 预测(结束节点)
  91 + assertEquals(BpmProcessInstanceStatusEnum.RUNNING.getStatus(), respVO.getStatus());
  92 + List<ActivityNode> nodes = respVO.getActivityNodes();
  93 + assertEquals(List.of(START_USER_NODE_ID, "task1", "task2", "end"), convertList(nodes, ActivityNode::getId));
  94 + assertEquals(List.of(BpmTaskStatusEnum.APPROVE.getStatus(), BpmTaskStatusEnum.APPROVE.getStatus(),
  95 + BpmTaskStatusEnum.RUNNING.getStatus(), BpmTaskStatusEnum.NOT_START.getStatus()),
  96 + convertList(nodes, ActivityNode::getStatus));
  97 + assertEquals(BpmSimpleModelNodeTypeEnum.START_USER_NODE.getType(), nodes.get(0).getNodeType());
  98 + assertEquals(1L, nodes.get(0).getTasks().get(0).getAssignee());
  99 + assertEquals("同意", nodes.get(1).getTasks().get(0).getReason());
  100 + assertEquals("用户1", nodes.get(2).getTasks().get(0).getAssigneeUser().getNickname());
  101 + assertEquals(BpmSimpleModelNodeTypeEnum.END_NODE.getType(), nodes.get(3).getNodeType());
  102 + // 断言:当前用户的待办为 task2,并返回流程实例信息
  103 + assertEquals(task2.getId(), respVO.getTodoTask().getId());
  104 + assertEquals(processInstanceId, respVO.getProcessInstance().getId());
  105 + assertEquals("用户1", respVO.getProcessInstance().getStartUser().getNickname());
  106 + }
  107 +
  108 + @Test
  109 + public void testGetApprovalDetail_approved() {
  110 + // 准备参数:task1、task2 均审批通过
  111 + String processInstanceId = startProcessInstance(1L, null);
  112 + approveRunningTask(processInstanceId);
  113 + approveRunningTask(processInstanceId);
  114 +
  115 + // 调用
  116 + BpmApprovalDetailRespVO respVO = processInstanceService.getApprovalDetail(1L,
  117 + new BpmApprovalDetailReqVO().setProcessInstanceId(processInstanceId));
  118 + // 断言:流程已结束,不再预测;最后为审批通过的结束节点
  119 + assertEquals(BpmProcessInstanceStatusEnum.APPROVE.getStatus(), respVO.getStatus());
  120 + List<ActivityNode> nodes = respVO.getActivityNodes();
  121 + assertEquals(4, nodes.size());
  122 + assertEquals(List.of(START_USER_NODE_ID, "task1", "task2"), convertList(nodes.subList(0, 3), ActivityNode::getId));
  123 + ActivityNode endNode = nodes.get(3);
  124 + assertEquals(BpmSimpleModelNodeTypeEnum.END_NODE.getType(), endNode.getNodeType());
  125 + assertEquals(BpmProcessInstanceStatusEnum.APPROVE.getStatus(), endNode.getStatus());
  126 + assertNotNull(endNode.getEndTime());
  127 + assertNull(respVO.getTodoTask());
  128 + }
  129 +
  130 + @Test
  131 + public void testGetApprovalDetail_rejected() {
  132 + // 准备参数:task1 审批不通过
  133 + String processInstanceId = startProcessInstance(1L, null);
  134 + Task task1 = getRunningTask(processInstanceId);
  135 + taskService.rejectTask(1L, new BpmTaskRejectReqVO().setId(task1.getId()).setReason("不同意"));
  136 +
  137 + // 调用
  138 + BpmApprovalDetailRespVO respVO = processInstanceService.getApprovalDetail(1L,
  139 + new BpmApprovalDetailReqVO().setProcessInstanceId(processInstanceId));
  140 + // 断言:不通过时不展示结束节点
  141 + assertEquals(BpmProcessInstanceStatusEnum.REJECT.getStatus(), respVO.getStatus());
  142 + List<ActivityNode> nodes = respVO.getActivityNodes();
  143 + assertEquals(List.of(START_USER_NODE_ID, "task1"), convertList(nodes, ActivityNode::getId));
  144 + assertEquals(BpmTaskStatusEnum.REJECT.getStatus(), nodes.get(1).getStatus());
  145 + assertEquals("不同意", nodes.get(1).getTasks().get(0).getReason());
  146 + assertNull(respVO.getTodoTask());
  147 + }
  148 +
  149 + @Test
  150 + public void testGetApprovalDetail_runningCallActivity() {
  151 + // 准备参数:主流程停留在子流程节点
  152 + deploy("bpmn/lifecycle/child.bpmn20.xml");
  153 + deploy("bpmn/lifecycle/child-parent.bpmn20.xml");
  154 + String processInstanceId = processInstanceService.createProcessInstance(1L, new BpmProcessInstanceCreateReqDTO()
  155 + .setProcessDefinitionKey("childParent"));
  156 + ProcessInstance child = runtimeService.createProcessInstanceQuery()
  157 + .superProcessInstanceId(processInstanceId).singleResult();
  158 +
  159 + // 调用
  160 + BpmApprovalDetailRespVO respVO = processInstanceService.getApprovalDetail(1L,
  161 + new BpmApprovalDetailReqVO().setProcessInstanceId(processInstanceId));
  162 + // 断言:进行中的子流程节点关联子流程实例(CallActivity 没有自身的任务)
  163 + // 注意:BPMN 设计器的预测(BpmnModelUtils#simulateProcess)不会越过 CallActivity,因此不断言后续预测节点
  164 + List<ActivityNode> nodes = respVO.getActivityNodes();
  165 + assertEquals(List.of(START_USER_NODE_ID, "child"), convertList(nodes.subList(0, 2), ActivityNode::getId));
  166 + ActivityNode childNode = nodes.get(1);
  167 + assertEquals(BpmSimpleModelNodeTypeEnum.CHILD_PROCESS.getType(), childNode.getNodeType());
  168 + assertEquals(BpmTaskStatusEnum.RUNNING.getStatus(), childNode.getStatus());
  169 + assertEquals(child.getId(), childNode.getProcessInstanceId());
  170 + assertTrue(childNode.getTasks().isEmpty());
  171 + // 断言:用户 1 在主流程中没有待办
  172 + assertNull(respVO.getTodoTask());
  173 + }
  174 +
  175 + @Test
  176 + public void testGetApprovalDetail_notExists() {
  177 + // 调用,并断言异常
  178 + assertServiceException(() -> processInstanceService.getApprovalDetail(1L,
  179 + new BpmApprovalDetailReqVO().setProcessInstanceId("not-exists")), PROCESS_INSTANCE_NOT_EXISTS);
  180 + }
  181 +
  182 + @Test
  183 + public void testGetApprovalDetail_definitionNotExists() {
  184 + // 调用,并断言异常:发起前预览审批详情时,流程定义已被删除
  185 + assertServiceException(() -> processInstanceService.getApprovalDetail(1L,
  186 + new BpmApprovalDetailReqVO().setProcessDefinitionId("not-exists")), PROCESS_DEFINITION_NOT_EXISTS);
  187 + }
  188 +
  189 + // ========== getProcessInstanceBpmnModelView ==========
  190 +
  191 + @Test
  192 + public void testGetProcessInstanceBpmnModelView_running() {
  193 + // 准备参数:task1 已审批,task2 待审批
  194 + String processInstanceId = startProcessInstance(1L, null);
  195 + approveRunningTask(processInstanceId);
  196 +
  197 + // 调用
  198 + BpmProcessInstanceBpmnModelViewRespVO respVO = processInstanceService.getProcessInstanceBpmnModelView(processInstanceId);
  199 + // 断言:进度信息
  200 + assertEquals(Set.of("task2"), respVO.getUnfinishedTaskActivityIds());
  201 + assertTrue(respVO.getFinishedTaskActivityIds().containsAll(Set.of("start", "task1")));
  202 + assertFalse(respVO.getFinishedTaskActivityIds().contains("task2"));
  203 + assertEquals(Set.of("flow-start", "flow-task1"), respVO.getFinishedSequenceFlowActivityIds());
  204 + assertTrue(respVO.getRejectedTaskActivityIds().isEmpty());
  205 + // 断言:基础信息
  206 + assertEquals(BpmProcessInstanceStatusEnum.RUNNING.getStatus(), respVO.getProcessInstance().getStatus());
  207 + assertEquals("用户1", respVO.getProcessInstance().getStartUser().getNickname());
  208 + assertEquals(List.of(BpmTaskStatusEnum.APPROVE.getStatus(), BpmTaskStatusEnum.RUNNING.getStatus()),
  209 + convertList(respVO.getTasks(), task -> task.getStatus()));
  210 + assertTrue(respVO.getBpmnXml().contains("twoStepApprove"));
  211 + assertNull(respVO.getSimpleModel());
  212 + }
  213 +
  214 + @Test
  215 + public void testGetProcessInstanceBpmnModelView_rejected() {
  216 + // 准备参数:task1 已审批,task2 审批不通过(驳回到 task1),task1 再次审批不通过
  217 + String processInstanceId = startProcessInstance(1L, null);
  218 + approveRunningTask(processInstanceId);
  219 + Task task = getRunningTask(processInstanceId);
  220 + taskService.rejectTask(1L, new BpmTaskRejectReqVO().setId(task.getId()).setReason("驳回"));
  221 + task = getRunningTask(processInstanceId);
  222 + assertEquals("task1", task.getTaskDefinitionKey());
  223 + taskService.rejectTask(1L, new BpmTaskRejectReqVO().setId(task.getId()).setReason("不同意"));
  224 +
  225 + // 调用
  226 + BpmProcessInstanceBpmnModelViewRespVO respVO = processInstanceService.getProcessInstanceBpmnModelView(processInstanceId);
  227 + // 断言:只取最后一个不通过的节点
  228 + assertEquals(BpmProcessInstanceStatusEnum.REJECT.getStatus(), respVO.getProcessInstance().getStatus());
  229 + assertEquals(Set.of("task1"), respVO.getRejectedTaskActivityIds());
  230 + assertFalse(respVO.getFinishedTaskActivityIds().contains("task1"));
  231 + assertTrue(respVO.getUnfinishedTaskActivityIds().isEmpty());
  232 + }
  233 +
  234 + @Test
  235 + public void testGetProcessInstanceBpmnModelView_notExists() {
  236 + assertNull(processInstanceService.getProcessInstanceBpmnModelView("not-exists"));
  237 + }
  238 +
  239 + // ========== getProcessInstancePage ==========
  240 +
  241 + @Test
  242 + public void testGetProcessInstancePage() {
  243 + // 准备参数:用户 1 发起并审批通过;用户 2 发起且审批中,表单 day = 3
  244 + String approvedId = startProcessInstance(1L, null);
  245 + approveRunningTask(approvedId);
  246 + approveRunningTask(approvedId);
  247 + String runningId = startProcessInstance(2L, new HashMap<>(Map.of("day", 3)));
  248 + LocalDateTime now = LocalDateTime.now();
  249 +
  250 + // 调用 + 断言:【我的流程】按发起人过滤
  251 + assertPageIds(List.of(approvedId), processInstanceService.getProcessInstancePage(1L, new BpmProcessInstancePageReqVO()));
  252 + // 调用 + 断言:【管理流程】按 startUserId 过滤
  253 + assertPageIds(List.of(runningId), processInstanceService.getProcessInstancePage(null,
  254 + (BpmProcessInstancePageReqVO) new BpmProcessInstancePageReqVO().setStartUserId(2L)));
  255 + // 调用 + 断言:名称、流程标识、发起时间,按发起时间倒序
  256 + BpmProcessInstancePageReqVO reqVO = new BpmProcessInstancePageReqVO();
  257 + reqVO.setName("两级").setProcessDefinitionKey("twoStepApprove")
  258 + .setCreateTime(new LocalDateTime[]{now.minusHours(1), now.plusHours(1)});
  259 + assertEquals(Set.of(approvedId, runningId),
  260 + new HashSet<>(convertList(processInstanceService.getProcessInstancePage(null, reqVO).getList(),
  261 + HistoricProcessInstance::getId)));
  262 + // 调用 + 断言:状态、结束时间
  263 + reqVO = new BpmProcessInstancePageReqVO();
  264 + reqVO.setStatus(BpmProcessInstanceStatusEnum.APPROVE.getStatus())
  265 + .setEndTime(new LocalDateTime[]{now.minusHours(1), now.plusHours(1)});
  266 + assertPageIds(List.of(approvedId), processInstanceService.getProcessInstancePage(null, reqVO));
  267 + // 调用 + 断言:表单字段,空值的字段被忽略
  268 + reqVO = new BpmProcessInstancePageReqVO();
  269 + reqVO.setFormFieldsParams("{\"day\": 3, \"reason\": \"\"}");
  270 + assertPageIds(List.of(runningId), processInstanceService.getProcessInstancePage(null, reqVO));
  271 + // 调用 + 断言:分类不匹配时返回空
  272 + reqVO = new BpmProcessInstancePageReqVO();
  273 + reqVO.setCategory("not-exists");
  274 + PageResult<HistoricProcessInstance> pageResult = processInstanceService.getProcessInstancePage(null, reqVO);
  275 + assertEquals(0, pageResult.getTotal());
  276 + assertTrue(pageResult.getList().isEmpty());
  277 + }
  278 +
  279 + // ========== getNextApprovalNodes ==========
  280 +
  281 + @Test
  282 + public void testGetNextApprovalNodes() {
  283 + // 准备参数
  284 + String processInstanceId = startProcessInstance(1L, null);
  285 + Task task1 = getRunningTask(processInstanceId);
  286 +
  287 + // 调用 + 断言:task1 的下一个节点为 task2,并预测审批人
  288 + List<ActivityNode> nodes = processInstanceService.getNextApprovalNodes(1L,
  289 + new BpmApprovalDetailReqVO().setTaskId(task1.getId()));
  290 + assertEquals(1, nodes.size());
  291 + assertEquals("task2", nodes.get(0).getId());
  292 + assertEquals(BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType(), nodes.get(0).getNodeType());
  293 + assertEquals(BpmTaskStatusEnum.RUNNING.getStatus(), nodes.get(0).getStatus());
  294 + assertEquals(List.of(1L), nodes.get(0).getCandidateUserIds());
  295 + assertEquals("用户1", nodes.get(0).getCandidateUsers().get(0).getNickname());
  296 +
  297 + // 调用 + 断言:task2 的下一个节点为结束节点,无需审批
  298 + approveRunningTask(processInstanceId);
  299 + Task task2 = getRunningTask(processInstanceId);
  300 + assertTrue(processInstanceService.getNextApprovalNodes(1L,
  301 + new BpmApprovalDetailReqVO().setTaskId(task2.getId())).isEmpty());
  302 + }
  303 +
  304 + // ========== 私有方法 ==========
  305 +
  306 + private void deploy(String resource) {
  307 + Deployment deployment = repositoryService.createDeployment().addClasspathResource(resource).deploy();
  308 + deploymentIds.add(deployment.getId());
  309 + }
  310 +
  311 + private String startProcessInstance(Long userId, Map<String, Object> variables) {
  312 + return processInstanceService.createProcessInstance(userId, new BpmProcessInstanceCreateReqDTO()
  313 + .setProcessDefinitionKey("twoStepApprove").setVariables(variables));
  314 + }
  315 +
  316 + private Task getRunningTask(String processInstanceId) {
  317 + List<Task> tasks = flowableTaskService.createTaskQuery().processInstanceId(processInstanceId).list();
  318 + assertEquals(1, tasks.size());
  319 + return tasks.get(0);
  320 + }
  321 +
  322 + private void approveRunningTask(String processInstanceId) {
  323 + Task task = getRunningTask(processInstanceId);
  324 + taskService.approveTask(1L, new BpmTaskApproveReqVO().setId(task.getId()).setReason("同意"));
  325 + }
  326 +
  327 + private static void assertPageIds(List<String> expected, PageResult<HistoricProcessInstance> pageResult) {
  328 + assertEquals(expected.size(), pageResult.getTotal());
  329 + assertEquals(expected, convertList(pageResult.getList(), HistoricProcessInstance::getId));
  330 + }
  331 +
  332 +}
... ...
yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BpmProcessLifecycleFlowableTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.bpm.framework.flowable;
  2 +
  3 +import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
  4 +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
  5 +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
  6 +import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum;
  7 +import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum;
  8 +import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum;
  9 +import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
  10 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.listener.BpmTaskEventListener;
  11 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
  12 +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceServiceImpl;
  13 +import cn.iocoder.yudao.module.bpm.service.task.BpmTaskServiceImpl;
  14 +import cn.iocoder.yudao.module.bpm.service.task.listener.BpmCallActivityListener;
  15 +import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
  16 +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
  17 +import jakarta.annotation.Resource;
  18 +import org.flowable.engine.HistoryService;
  19 +import org.flowable.engine.RepositoryService;
  20 +import org.flowable.engine.RuntimeService;
  21 +import org.flowable.engine.TaskService;
  22 +import org.flowable.engine.history.HistoricProcessInstance;
  23 +import org.flowable.engine.runtime.ProcessInstance;
  24 +import org.flowable.task.api.Task;
  25 +import org.flowable.task.api.history.HistoricTaskInstance;
  26 +import org.junit.jupiter.api.AfterEach;
  27 +import org.junit.jupiter.api.BeforeEach;
  28 +import org.junit.jupiter.api.Test;
  29 +
  30 +import java.util.ArrayList;
  31 +import java.util.List;
  32 +import java.util.Set;
  33 +
  34 +import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.START_USER_NODE_ID;
  35 +import static org.junit.jupiter.api.Assertions.*;
  36 +import static org.mockito.ArgumentMatchers.*;
  37 +import static org.mockito.Mockito.*;
  38 +
  39 +/**
  40 + * 任务创建、分配事件后自动处理,以及子流程发起的 Flowable 集成测试
  41 + *
  42 + * <p>补充 {@link BpmTaskServiceFlowableTest} 未覆盖的 {@link BpmTaskServiceImpl#processTaskCreated}、
  43 + * {@link BpmTaskServiceImpl#processTaskAssigned} 分支,以及 {@link BpmCallActivityListener}、
  44 + * {@link BpmProcessInstanceServiceImpl#processProcessInstanceCreated} 在真实事务提交后的回调。
  45 + * 事件均由 {@link BpmTaskEventListener} 等真实监听器触发。</p>
  46 + *
  47 + * @author HUIHUI
  48 + */
  49 +public class BpmProcessLifecycleFlowableTest extends BaseFlowableUnitTest {
  50 +
  51 + @Resource
  52 + private RepositoryService repositoryService;
  53 + @Resource
  54 + private RuntimeService runtimeService;
  55 + @Resource
  56 + private HistoryService historyService;
  57 + @Resource
  58 + private TaskService flowableTaskService;
  59 +
  60 + @Resource
  61 + private BpmProcessInstanceServiceImpl processInstanceService;
  62 +
  63 + private final List<String> deploymentIds = new ArrayList<>();
  64 +
  65 + @BeforeEach
  66 + public void setUp() {
  67 + mockFlowableBackedServices(repositoryService);
  68 + }
  69 +
  70 + @AfterEach
  71 + public void tearDown() {
  72 + // 级联删除流程定义、流程实例与历史数据
  73 + deploymentIds.forEach(id -> repositoryService.deleteDeployment(id, true));
  74 + }
  75 +
  76 + // ========== 任务创建后的自动处理 ==========
  77 +
  78 + @Test
  79 + public void testProcessTaskCreated_assignEmptyApprove() {
  80 + // 准备参数:人工审批,计算不到审批人时自动通过
  81 + deploy("bpmn/lifecycle/assign-empty-approve.bpmn20.xml");
  82 + when(taskCandidateInvoker.calculateUsersByTask(any())).thenReturn(Set.of());
  83 +
  84 + // 调用
  85 + String processInstanceId = startProcessInstance("assignEmptyApprove", 1L);
  86 + // 断言:任务没有审批人,在事务完成后自动通过,且不会发送待办通知
  87 + HistoricTaskInstance task = getHistoricTask(processInstanceId, "task");
  88 + assertNull(task.getAssignee());
  89 + assertTaskStatus(task.getId(), BpmTaskStatusEnum.APPROVE.getStatus(), BpmReasonEnum.ASSIGN_EMPTY_APPROVE.getReason());
  90 + assertProcessInstanceStatus(processInstanceId, BpmProcessInstanceStatusEnum.APPROVE.getStatus());
  91 + verify(messageService, never()).sendMessageWhenTaskAssigned(any());
  92 + }
  93 +
  94 + @Test
  95 + public void testProcessTaskCreated_assignEmptyReject() {
  96 + // 准备参数:人工审批,计算不到审批人时自动拒绝
  97 + deploy("bpmn/lifecycle/assign-empty-reject.bpmn20.xml");
  98 + when(taskCandidateInvoker.calculateUsersByTask(any())).thenReturn(Set.of());
  99 +
  100 + // 调用
  101 + String processInstanceId = startProcessInstance("assignEmptyReject", 1L);
  102 + // 断言:任务没有审批人,在事务完成后自动拒绝,流程不通过
  103 + HistoricTaskInstance task = getHistoricTask(processInstanceId, "task");
  104 + assertNull(task.getAssignee());
  105 + assertTaskStatus(task.getId(), BpmTaskStatusEnum.REJECT.getStatus(), BpmReasonEnum.ASSIGN_EMPTY_REJECT.getReason());
  106 + assertProcessInstanceStatus(processInstanceId, BpmProcessInstanceStatusEnum.REJECT.getStatus());
  107 + verify(messageService, never()).sendMessageWhenTaskAssigned(any());
  108 + }
  109 +
  110 + // ========== 任务分配后的自动处理 ==========
  111 +
  112 + @Test
  113 + public void testProcessTaskAssigned_startUserNodeAutoApprove() {
  114 + // 准备参数
  115 + deploy("bpmn/lifecycle/start-user-node.bpmn20.xml");
  116 +
  117 + // 调用
  118 + String processInstanceId = startProcessInstance("startUserNode", 1L);
  119 + // 断言:发起人节点首次自动通过,流转到下一个审批节点
  120 + assertTaskStatus(getHistoricTask(processInstanceId, START_USER_NODE_ID).getId(),
  121 + BpmTaskStatusEnum.APPROVE.getStatus(),
  122 + BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_SKIP_START_USER_NODE.getReason());
  123 + Task task = getRunningTask(processInstanceId);
  124 + assertEquals("task", task.getTaskDefinitionKey());
  125 + assertEquals(BpmTaskStatusEnum.RUNNING.getStatus(), FlowableUtils.getTaskStatus(task));
  126 + }
  127 +
  128 + @Test
  129 + public void testProcessTaskAssigned_startUserSkip() {
  130 + // 准备参数:审批人与发起人相同时,自动跳过
  131 + deploy("bpmn/lifecycle/start-user-skip.bpmn20.xml");
  132 +
  133 + // 调用 + 断言:发起人 1 = 审批人 1,自动通过
  134 + String processInstanceId = startProcessInstance("startUserSkip", 1L);
  135 + assertTaskStatus(getHistoricTask(processInstanceId, "task").getId(), BpmTaskStatusEnum.APPROVE.getStatus(),
  136 + BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_SKIP.getReason());
  137 + assertProcessInstanceStatus(processInstanceId, BpmProcessInstanceStatusEnum.APPROVE.getStatus());
  138 +
  139 + // 调用 + 断言:发起人 2 ≠ 审批人 1,正常待审批并发送待办通知
  140 + String otherProcessInstanceId = startProcessInstance("startUserSkip", 2L);
  141 + Task task = getRunningTask(otherProcessInstanceId);
  142 + assertEquals("1", task.getAssignee());
  143 + assertEquals(BpmTaskStatusEnum.RUNNING.getStatus(), FlowableUtils.getTaskStatus(task));
  144 + verify(messageService).sendMessageWhenTaskAssigned(argThat(reqDTO -> task.getId().equals(reqDTO.getTaskId())));
  145 + }
  146 +
  147 + @Test
  148 + public void testProcessTaskAssigned_startUserTransferDeptLeader() {
  149 + // 准备参数:审批人与发起人相同时转交部门负责人;发起人 1 的部门负责人为 2
  150 + deploy("bpmn/lifecycle/start-user-transfer-dept-leader.bpmn20.xml");
  151 + when(adminUserApi.getUser(1L)).thenReturn(new AdminUserRespDTO().setId(1L).setNickname("用户1").setDeptId(10L));
  152 + when(deptApi.getDept(10L)).thenReturn(new DeptRespDTO().setId(10L).setLeaderUserId(2L));
  153 +
  154 + // 调用
  155 + String processInstanceId = startProcessInstance("startUserTransferDeptLeader", 1L);
  156 + // 断言:任务转交给部门负责人,原审批人成为 owner,仅向部门负责人发送待办通知
  157 + Task task = getRunningTask(processInstanceId);
  158 + assertEquals("2", task.getAssignee());
  159 + assertEquals("1", task.getOwner());
  160 + verify(messageService).sendMessageWhenTaskAssigned(argThat(reqDTO ->
  161 + task.getId().equals(reqDTO.getTaskId()) && reqDTO.getAssigneeUserId().equals(2L)));
  162 + verify(messageService, times(1)).sendMessageWhenTaskAssigned(any());
  163 + }
  164 +
  165 + @Test
  166 + public void testProcessTaskAssigned_startUserDeptLeaderNotFound() {
  167 + // 准备参数:审批人与发起人相同时转交部门负责人;发起人 1 的部门没有负责人
  168 + deploy("bpmn/lifecycle/start-user-transfer-dept-leader.bpmn20.xml");
  169 + when(adminUserApi.getUser(1L)).thenReturn(new AdminUserRespDTO().setId(1L).setNickname("用户1").setDeptId(10L));
  170 + when(deptApi.getDept(10L)).thenReturn(new DeptRespDTO().setId(10L));
  171 +
  172 + // 调用
  173 + String processInstanceId = startProcessInstance("startUserTransferDeptLeader", 1L);
  174 + // 断言:找不到部门负责人时自动通过
  175 + assertTaskStatus(getHistoricTask(processInstanceId, "task").getId(), BpmTaskStatusEnum.APPROVE.getStatus(),
  176 + BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_DEPT_LEADER_NOT_FOUND.getReason());
  177 + assertProcessInstanceStatus(processInstanceId, BpmProcessInstanceStatusEnum.APPROVE.getStatus());
  178 + }
  179 +
  180 + // ========== 子流程发起 ==========
  181 +
  182 + @Test
  183 + public void testCallActivity_startUserAndTitle() {
  184 + // 准备参数:开启自定义标题
  185 + deploy("bpmn/lifecycle/child.bpmn20.xml");
  186 + deploy("bpmn/lifecycle/child-parent.bpmn20.xml");
  187 + when(processDefinitionService.getProcessDefinitionInfo(anyString())).thenAnswer(invocation ->
  188 + new BpmProcessDefinitionInfoDO().setProcessDefinitionId(invocation.getArgument(0))
  189 + .setModelType(BpmModelTypeEnum.BPMN.getType())
  190 + .setTitleSetting(new BpmModelMetaInfoVO.TitleSetting().setEnable(true)
  191 + .setTitle("{PROCESS_DEFINITION_NAME}-{PROCESS_START_USER_ID}")));
  192 +
  193 + // 调用:用户 2 发起主流程
  194 + String processInstanceId = startProcessInstance("childParent", 2L);
  195 + // 断言:主流程标题在发起时生成
  196 + assertEquals("主流程-用户2", processInstanceService.getProcessInstance(processInstanceId).getName());
  197 + // 断言:子流程发起人同主流程(BpmCallActivityListener),标题在 PROCESS_CREATED 事务提交后生成
  198 + ProcessInstance child = runtimeService.createProcessInstanceQuery()
  199 + .superProcessInstanceId(processInstanceId).singleResult();
  200 + assertNotNull(child);
  201 + assertEquals("2", child.getStartUserId());
  202 + assertEquals("子流程-用户2", processInstanceService.getProcessInstance(child.getId()).getName());
  203 + Task childTask = getRunningTask(child.getId());
  204 + assertEquals("task", childTask.getTaskDefinitionKey());
  205 + assertEquals("子流程审批", childTask.getName());
  206 + }
  207 +
  208 + // ========== 私有方法 ==========
  209 +
  210 + private void deploy(String resource) {
  211 + deploymentIds.add(repositoryService.createDeployment().addClasspathResource(resource).deploy().getId());
  212 + }
  213 +
  214 + private String startProcessInstance(String processDefinitionKey, Long userId) {
  215 + return processInstanceService.createProcessInstance(userId, new BpmProcessInstanceCreateReqDTO()
  216 + .setProcessDefinitionKey(processDefinitionKey).setBusinessKey("business-1"));
  217 + }
  218 +
  219 + private Task getRunningTask(String processInstanceId) {
  220 + List<Task> tasks = flowableTaskService.createTaskQuery().processInstanceId(processInstanceId)
  221 + .includeTaskLocalVariables().list();
  222 + assertEquals(1, tasks.size());
  223 + return tasks.get(0);
  224 + }
  225 +
  226 + private HistoricTaskInstance getHistoricTask(String processInstanceId, String taskDefinitionKey) {
  227 + return historyService.createHistoricTaskInstanceQuery().processInstanceId(processInstanceId)
  228 + .taskDefinitionKey(taskDefinitionKey).includeTaskLocalVariables().singleResult();
  229 + }
  230 +
  231 + private void assertTaskStatus(String taskId, Integer status, String reason) {
  232 + HistoricTaskInstance task = historyService.createHistoricTaskInstanceQuery().taskId(taskId)
  233 + .includeTaskLocalVariables().singleResult();
  234 + assertEquals(status, FlowableUtils.getTaskStatus(task));
  235 + assertEquals(reason, FlowableUtils.getTaskReason(task));
  236 + }
  237 +
  238 + private void assertProcessInstanceStatus(String processInstanceId, Integer status) {
  239 + HistoricProcessInstance processInstance = historyService.createHistoricProcessInstanceQuery()
  240 + .processInstanceId(processInstanceId).includeProcessVariables().singleResult();
  241 + assertNotNull(processInstance.getEndTime());
  242 + assertEquals(status, FlowableUtils.getProcessInstanceStatus(processInstance));
  243 + }
  244 +
  245 +}
... ...
yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BpmTaskServiceFlowableTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.bpm.framework.flowable;
  2 +
  3 +import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent;
  4 +import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
  5 +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
  6 +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCancelReqVO;
  7 +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCreateReqVO;
  8 +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.*;
  9 +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
  10 +import cn.iocoder.yudao.module.bpm.enums.definition.BpmAutoApproveTypeEnum;
  11 +import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum;
  12 +import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskApproveTypeEnum;
  13 +import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskTimeoutHandlerTypeEnum;
  14 +import cn.iocoder.yudao.module.bpm.enums.task.BpmAttachmentTypeEnum;
  15 +import cn.iocoder.yudao.module.bpm.enums.task.BpmCommentTypeEnum;
  16 +import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum;
  17 +import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum;
  18 +import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskSignTypeEnum;
  19 +import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
  20 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants;
  21 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants;
  22 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
  23 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
  24 +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceServiceImpl;
  25 +import cn.iocoder.yudao.module.bpm.service.task.BpmTaskServiceImpl;
  26 +import jakarta.annotation.Resource;
  27 +import org.flowable.bpmn.model.BpmnModel;
  28 +import org.flowable.bpmn.model.FlowElement;
  29 +import org.flowable.bpmn.model.UserTask;
  30 +import org.flowable.engine.HistoryService;
  31 +import org.flowable.engine.ManagementService;
  32 +import org.flowable.engine.RepositoryService;
  33 +import org.flowable.engine.RuntimeService;
  34 +import org.flowable.engine.TaskService;
  35 +import org.flowable.engine.delegate.DelegateExecution;
  36 +import org.flowable.engine.repository.Deployment;
  37 +import org.flowable.engine.runtime.ProcessInstance;
  38 +import org.flowable.engine.task.Attachment;
  39 +import org.flowable.job.api.Job;
  40 +import org.flowable.task.api.DelegationState;
  41 +import org.flowable.task.api.Task;
  42 +import org.flowable.task.api.history.HistoricTaskInstance;
  43 +import org.junit.jupiter.api.AfterEach;
  44 +import org.junit.jupiter.api.BeforeEach;
  45 +import org.junit.jupiter.api.Test;
  46 +import org.springframework.test.context.event.ApplicationEvents;
  47 +import org.springframework.test.context.event.RecordApplicationEvents;
  48 +
  49 +import java.util.ArrayList;
  50 +import java.util.HashMap;
  51 +import java.util.List;
  52 +import java.util.Map;
  53 +import java.util.Set;
  54 +import java.util.function.Function;
  55 +import java.util.stream.Collectors;
  56 +
  57 +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
  58 +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*;
  59 +import static org.junit.jupiter.api.Assertions.*;
  60 +import static org.mockito.ArgumentMatchers.*;
  61 +import static org.mockito.Mockito.*;
  62 +
  63 +/**
  64 + * {@link BpmTaskServiceImpl}、{@link BpmProcessInstanceServiceImpl} 的 Flowable 集成测试
  65 + *
  66 + * @author HUIHUI
  67 + */
  68 +@RecordApplicationEvents
  69 +public class BpmTaskServiceFlowableTest extends BaseFlowableUnitTest {
  70 +
  71 + @Resource
  72 + private RepositoryService repositoryService;
  73 + @Resource
  74 + private RuntimeService runtimeService;
  75 + @Resource
  76 + private HistoryService historyService;
  77 + @Resource
  78 + private TaskService flowableTaskService;
  79 + @Resource
  80 + private ManagementService managementService;
  81 +
  82 + @Resource
  83 + private BpmTaskServiceImpl bpmTaskService;
  84 + @Resource
  85 + private BpmProcessInstanceServiceImpl processInstanceService;
  86 +
  87 + @Resource
  88 + private ApplicationEvents applicationEvents;
  89 +
  90 + private Deployment deployment;
  91 + /**
  92 + * 单个测试额外部署的流程,在 {@link #tearDown()} 中先于 {@link #deployment} 删除
  93 + */
  94 + private final List<String> extraDeploymentIds = new ArrayList<>();
  95 +
  96 + @BeforeEach
  97 + public void setUp() {
  98 + // 部署两级审批流程:task1 -> task2,task2 驳回时退回到 task1;审批人统一由测试上下文计算为用户 1
  99 + deployment = repositoryService.createDeployment()
  100 + .addClasspathResource("bpmn/two-step-approve.bpmn20.xml")
  101 + .deploy();
  102 +
  103 + // 依赖的业务 Service 使用 Mock(委托给真实 Flowable 引擎),Flowable 相关 Service、BPM 任务/流程实例 Service 使用 Spring 中的真实 Bean
  104 + mockFlowableBackedServices(repositoryService);
  105 + }
  106 +
  107 + @AfterEach
  108 + public void tearDown() {
  109 + // 清理测试数据:加签子任务没有 execution,需要先删除,否则 Flowable 级联删除流程实例时会空指针
  110 + List<String> signTaskIds = flowableTaskService.createTaskQuery().deploymentId(deployment.getId()).list()
  111 + .stream().filter(task -> task.getExecutionId() == null).map(Task::getId).toList();
  112 + flowableTaskService.deleteTasks(signTaskIds);
  113 + extraDeploymentIds.forEach(id -> repositoryService.deleteDeployment(id, true));
  114 + repositoryService.deleteDeployment(deployment.getId(), true);
  115 + }
  116 +
  117 + @Test
  118 + public void testTriggerTask_success() {
  119 + // 准备参数:流程停留在 receiveTask 等待节点
  120 + deploy("bpmn/trigger-task.bpmn20.xml");
  121 + String processInstanceId = startProcessInstance("triggerTask");
  122 + assertNotNull(runtimeService.createExecutionQuery().processInstanceId(processInstanceId)
  123 + .activityId("wait").singleResult());
  124 +
  125 + // 调用
  126 + bpmTaskService.triggerTask(processInstanceId, "wait");
  127 + // 断言:触发等待节点后,流程结束
  128 + assertNull(processInstanceService.getProcessInstance(processInstanceId));
  129 + assertNotNull(processInstanceService.getHistoricProcessInstance(processInstanceId).getEndTime());
  130 + }
  131 +
  132 + @Test
  133 + public void testTriggerTask_executionNotExists() {
  134 + // 准备参数
  135 + String processInstanceId = startProcessInstance();
  136 +
  137 + // 调用:触发不存在的等待节点
  138 + bpmTaskService.triggerTask(processInstanceId, "notExists");
  139 + // 断言:流程实例不受影响
  140 + assertEquals("task1", getRunningTask(processInstanceId).getTaskDefinitionKey());
  141 + }
  142 +
  143 + @Test
  144 + public void testApproveTask_success() {
  145 + // 准备参数
  146 + String processInstanceId = startProcessInstance();
  147 + Task task1 = getRunningTask(processInstanceId);
  148 + assertEquals("1", task1.getAssignee());
  149 +
  150 + // 调用:审批 task1
  151 + bpmTaskService.approveTask(1L, new BpmTaskApproveReqVO().setId(task1.getId()).setReason("同意"));
  152 + // 断言:task1 审批通过,流程流转到 task2
  153 + assertTaskStatus(task1.getId(), BpmTaskStatusEnum.APPROVE.getStatus(), "同意");
  154 + verify(commentService).createComment(eq(task1.getId()), eq(processInstanceId),
  155 + eq(BpmCommentTypeEnum.APPROVE), eq("同意"));
  156 + Task task2 = getRunningTask(processInstanceId);
  157 + assertEquals("task2", task2.getTaskDefinitionKey());
  158 +
  159 + // 调用:审批 task2
  160 + bpmTaskService.approveTask(1L, new BpmTaskApproveReqVO().setId(task2.getId()).setReason("同意"));
  161 + // 断言:流程实例结束
  162 + assertNull(processInstanceService.getProcessInstance(processInstanceId));
  163 + assertNotNull(processInstanceService.getHistoricProcessInstance(processInstanceId).getEndTime());
  164 + List<HistoricTaskInstance> tasks = bpmTaskService.getTaskListByProcessInstanceId(processInstanceId, true);
  165 + assertEquals(List.of("task1", "task2"), tasks.stream().map(HistoricTaskInstance::getTaskDefinitionKey).toList());
  166 + }
  167 +
  168 + @Test
  169 + public void testApproveTask_withSignAndAttachment() {
  170 + // 准备参数:task1 开启签名
  171 + doAnswer(invocation -> {
  172 + BpmnModel bpmnModel = repositoryService.getBpmnModel(invocation.getArgument(0));
  173 + BpmnModelUtils.addExtensionElement(bpmnModel.getFlowElement("task1"), BpmnModelConstants.SIGN_ENABLE, "true");
  174 + return bpmnModel;
  175 + }).when(modelService).getBpmnModelByDefinitionId(anyString());
  176 + String processInstanceId = startProcessInstance();
  177 + Task task1 = getRunningTask(processInstanceId);
  178 +
  179 + // 调用
  180 + bpmTaskService.approveTask(1L, new BpmTaskApproveReqVO().setId(task1.getId()).setReason("同意")
  181 + .setSignPicUrl("https://www.iocoder.cn/sign.png")
  182 + .setAttachments(List.of("https://www.iocoder.cn/files/a.pdf"))
  183 + .setVariables(Map.of("day", 3)));
  184 + // 断言:签名、表单变量保存到任务上,表单变量同步到流程实例
  185 + HistoricTaskInstance historicTask = bpmTaskService.getHistoricTask(task1.getId());
  186 + assertEquals("https://www.iocoder.cn/sign.png",
  187 + historicTask.getTaskLocalVariables().get(BpmnVariableConstants.TASK_SIGN_PIC_URL));
  188 + assertEquals(3, historicTask.getTaskLocalVariables().get("day"));
  189 + assertEquals(3, runtimeService.getVariable(processInstanceId, "day"));
  190 + // 断言:附件
  191 + List<Attachment> attachments = bpmTaskService.getAttachments(processInstanceId, Set.of(task1.getId()),
  192 + BpmAttachmentTypeEnum.TASK_ATTACHMENT);
  193 + assertEquals(1, attachments.size());
  194 + assertEquals("a.pdf", attachments.get(0).getName());
  195 + assertEquals("https://www.iocoder.cn/files/a.pdf", attachments.get(0).getUrl());
  196 + assertEquals("task2", getRunningTask(processInstanceId).getTaskDefinitionKey());
  197 + }
  198 +
  199 + @Test
  200 + public void testApproveTask_assigneeNotSelf() {
  201 + // 准备参数
  202 + String processInstanceId = startProcessInstance();
  203 + Task task1 = getRunningTask(processInstanceId);
  204 +
  205 + // 调用,并断言异常
  206 + assertServiceException(() -> bpmTaskService.approveTask(2L,
  207 + new BpmTaskApproveReqVO().setId(task1.getId()).setReason("同意")), TASK_OPERATE_FAIL_ASSIGN_NOT_SELF);
  208 + }
  209 +
  210 + @Test
  211 + public void testRejectTask_finishProcessInstance() {
  212 + // 准备参数
  213 + String processInstanceId = startProcessInstance();
  214 + Task task1 = getRunningTask(processInstanceId);
  215 +
  216 + // 调用:task1 未配置驳回策略,默认结束流程
  217 + bpmTaskService.rejectTask(1L, new BpmTaskRejectReqVO().setId(task1.getId()).setReason("不同意")
  218 + .setAttachments(List.of("https://www.iocoder.cn/files/b.pdf")));
  219 + // 断言:任务不通过,附件保存到任务上
  220 + assertTaskStatus(task1.getId(), BpmTaskStatusEnum.REJECT.getStatus(), "不同意");
  221 + List<Attachment> attachments = bpmTaskService.getAttachments(processInstanceId, Set.of(task1.getId()),
  222 + BpmAttachmentTypeEnum.TASK_ATTACHMENT);
  223 + assertEquals(List.of("b.pdf"), attachments.stream().map(Attachment::getName).toList());
  224 + // 断言:流程实例不通过并结束
  225 + assertNull(processInstanceService.getProcessInstance(processInstanceId));
  226 + assertEquals(BpmProcessInstanceStatusEnum.REJECT.getStatus(), getHistoricProcessVariable(processInstanceId,
  227 + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS));
  228 + assertEquals("审批不通过任务,原因:不同意", getHistoricProcessVariable(processInstanceId,
  229 + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_REASON));
  230 + }
  231 +
  232 + @Test
  233 + public void testRejectTask_returnUserTask() {
  234 + // 准备参数:task1 审批通过后,流转到 task2
  235 + String processInstanceId = startProcessInstance();
  236 + approveRunningTask(processInstanceId);
  237 + Task task2 = getRunningTask(processInstanceId);
  238 +
  239 + // 调用:task2 配置驳回到 task1
  240 + bpmTaskService.rejectTask(1L, new BpmTaskRejectReqVO().setId(task2.getId()).setReason("退回修改"));
  241 + // 断言:task2 被标记为退回,流程回到 task1
  242 + assertTaskStatus(task2.getId(), BpmTaskStatusEnum.RETURN.getStatus(), "退回修改");
  243 + Task task1 = getRunningTask(processInstanceId);
  244 + assertEquals("task1", task1.getTaskDefinitionKey());
  245 + assertEquals(Boolean.TRUE, runtimeService.getVariable(processInstanceId,
  246 + String.format(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, "task1")));
  247 + assertEquals(Set.of("task2"), runtimeService.getVariable(processInstanceId,
  248 + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_TASK_IDS));
  249 +
  250 + // 调用:再次审批 task1
  251 + bpmTaskService.approveTask(1L, new BpmTaskApproveReqVO().setId(task1.getId()).setReason("已修改"));
  252 + // 断言:退回标记被清理,流程再次流转到 task2
  253 + assertFalse(runtimeService.hasVariable(processInstanceId,
  254 + String.format(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, "task1")));
  255 + assertEquals("task2", getRunningTask(processInstanceId).getTaskDefinitionKey());
  256 + }
  257 +
  258 + @Test
  259 + public void testRejectTask_signChildTask() {
  260 + // 准备参数:task1 向前加签给用户 2
  261 + String processInstanceId = startProcessInstance();
  262 + Task task1 = getRunningTask(processInstanceId);
  263 + bpmTaskService.createSignTask(1L, new BpmTaskSignCreateReqVO().setId(task1.getId())
  264 + .setType(BpmTaskSignTypeEnum.BEFORE.getType()).setUserIds(Set.of(2L)).setReason("加签"));
  265 + Task childTask = bpmTaskService.getTaskListByParentTaskId(task1.getId()).get(0);
  266 +
  267 + // 调用:用户 2 审批不通过加签任务
  268 + bpmTaskService.rejectTask(2L, new BpmTaskRejectReqVO().setId(childTask.getId()).setReason("不同意"));
  269 + // 断言:加签任务、根任务都标记为不通过,流程实例不通过并结束
  270 + assertTaskStatus(childTask.getId(), BpmTaskStatusEnum.REJECT.getStatus(), "不同意");
  271 + assertTaskStatus(task1.getId(), BpmTaskStatusEnum.REJECT.getStatus(),
  272 + BpmCommentTypeEnum.REJECT.formatComment("加签任务不通过"));
  273 + verify(commentService).createComment(eq(task1.getId()), eq(processInstanceId),
  274 + eq(BpmCommentTypeEnum.REJECT), eq("加签任务不通过"));
  275 + assertNull(processInstanceService.getProcessInstance(processInstanceId));
  276 + assertEquals(BpmProcessInstanceStatusEnum.REJECT.getStatus(), getHistoricProcessVariable(processInstanceId,
  277 + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS));
  278 + }
  279 +
  280 + @Test
  281 + public void testGetUserTaskListByReturn_success() {
  282 + // 准备参数
  283 + String processInstanceId = startProcessInstance();
  284 + assertTrue(bpmTaskService.getUserTaskListByReturn(getRunningTask(processInstanceId).getId()).isEmpty());
  285 + approveRunningTask(processInstanceId);
  286 + Task task2 = getRunningTask(processInstanceId);
  287 +
  288 + // 调用
  289 + List<UserTask> userTasks = bpmTaskService.getUserTaskListByReturn(task2.getId());
  290 + // 断言
  291 + assertEquals(List.of("task1"), userTasks.stream().map(UserTask::getId).toList());
  292 + }
  293 +
  294 + @Test
  295 + public void testReturnTask_success() {
  296 + // 准备参数
  297 + String processInstanceId = startProcessInstance();
  298 + approveRunningTask(processInstanceId);
  299 + Task task2 = getRunningTask(processInstanceId);
  300 +
  301 + // 调用
  302 + bpmTaskService.returnTask(1L, new BpmTaskReturnReqVO().setId(task2.getId())
  303 + .setTargetTaskDefinitionKey("task1").setReason("退回"));
  304 + // 断言
  305 + assertTaskStatus(task2.getId(), BpmTaskStatusEnum.RETURN.getStatus(), "退回");
  306 + verify(commentService).createComment(eq(task2.getId()), eq(processInstanceId),
  307 + eq(BpmCommentTypeEnum.RETURN), eq("退回"));
  308 + assertEquals("task1", getRunningTask(processInstanceId).getTaskDefinitionKey());
  309 + }
  310 +
  311 + @Test
  312 + public void testReturnTask_targetNotExists() {
  313 + // 准备参数
  314 + String processInstanceId = startProcessInstance();
  315 + Task task1 = getRunningTask(processInstanceId);
  316 +
  317 + // 调用,并断言异常
  318 + assertServiceException(() -> bpmTaskService.returnTask(1L, new BpmTaskReturnReqVO().setId(task1.getId())
  319 + .setTargetTaskDefinitionKey("notExists").setReason("退回")), TASK_TARGET_NODE_NOT_EXISTS);
  320 + }
  321 +
  322 + @Test
  323 + public void testReturnTask_sourceTargetError() {
  324 + // 准备参数
  325 + String processInstanceId = startProcessInstance();
  326 + Task task1 = getRunningTask(processInstanceId);
  327 +
  328 + // 调用,并断言异常:task2 位于 task1 之后,不能退回
  329 + assertServiceException(() -> bpmTaskService.returnTask(1L, new BpmTaskReturnReqVO().setId(task1.getId())
  330 + .setTargetTaskDefinitionKey("task2").setReason("退回")), TASK_RETURN_FAIL_SOURCE_TARGET_ERROR);
  331 + }
  332 +
  333 + @Test
  334 + public void testDelegateTask_thenApprove() {
  335 + // 准备参数
  336 + String processInstanceId = startProcessInstance();
  337 + Task task1 = getRunningTask(processInstanceId);
  338 +
  339 + // 调用:委派给用户 2
  340 + bpmTaskService.delegateTask(1L, new BpmTaskDelegateReqVO().setId(task1.getId())
  341 + .setDelegateUserId(2L).setReason("请帮忙审批"));
  342 + // 断言
  343 + Task delegatedTask = bpmTaskService.getTask(task1.getId());
  344 + assertEquals("1", delegatedTask.getOwner());
  345 + assertEquals("2", delegatedTask.getAssignee());
  346 + assertEquals(DelegationState.PENDING, delegatedTask.getDelegationState());
  347 + verify(commentService).createComment(eq(task1.getId()), eq(processInstanceId),
  348 + eq(BpmCommentTypeEnum.DELEGATE_START), eq("用户1"), eq("用户2"), eq("请帮忙审批"));
  349 +
  350 + // 调用:被委派人审批,任务归还给原审批人
  351 + bpmTaskService.approveTask(2L, new BpmTaskApproveReqVO().setId(task1.getId()).setReason("已处理"));
  352 + // 断言:任务仍在 task1,审批人恢复为用户 1
  353 + Task resolvedTask = bpmTaskService.getTask(task1.getId());
  354 + assertEquals("1", resolvedTask.getAssignee());
  355 + assertEquals(DelegationState.RESOLVED, resolvedTask.getDelegationState());
  356 + assertEquals(BpmTaskStatusEnum.RUNNING.getStatus(),
  357 + resolvedTask.getTaskLocalVariables().get(BpmnVariableConstants.TASK_VARIABLE_STATUS));
  358 + }
  359 +
  360 + @Test
  361 + public void testDelegateTask_redelegate() {
  362 + // 准备参数:用户 1 委派给用户 2
  363 + String processInstanceId = startProcessInstance();
  364 + Task task1 = getRunningTask(processInstanceId);
  365 + bpmTaskService.delegateTask(1L, new BpmTaskDelegateReqVO().setId(task1.getId())
  366 + .setDelegateUserId(2L).setReason("请帮忙审批"));
  367 +
  368 + // 调用:用户 2 再委派给用户 3
  369 + bpmTaskService.delegateTask(2L, new BpmTaskDelegateReqVO().setId(task1.getId())
  370 + .setDelegateUserId(3L).setReason("转给用户 3"));
  371 + // 断言:owner 仍为最初的审批人
  372 + Task delegatedTask = bpmTaskService.getTask(task1.getId());
  373 + assertEquals("1", delegatedTask.getOwner());
  374 + assertEquals("3", delegatedTask.getAssignee());
  375 + verify(commentService).createComment(eq(task1.getId()), eq(processInstanceId),
  376 + eq(BpmCommentTypeEnum.DELEGATE_START), eq("用户2"), eq("用户3"), eq("转给用户 3"));
  377 +
  378 + // 调用:用户 3 审批,任务归还给用户 1
  379 + bpmTaskService.approveTask(3L, new BpmTaskApproveReqVO().setId(task1.getId()).setReason("已处理"));
  380 + // 断言
  381 + assertEquals("1", bpmTaskService.getTask(task1.getId()).getAssignee());
  382 + verify(commentService).createComment(eq(task1.getId()), eq(processInstanceId),
  383 + eq(BpmCommentTypeEnum.DELEGATE_END), any(), eq("用户1"), eq("已处理"));
  384 + }
  385 +
  386 + @Test
  387 + public void testDelegateTask_userRepeat() {
  388 + // 准备参数
  389 + String processInstanceId = startProcessInstance();
  390 + Task task1 = getRunningTask(processInstanceId);
  391 +
  392 + // 调用,并断言异常
  393 + assertServiceException(() -> bpmTaskService.delegateTask(1L, new BpmTaskDelegateReqVO().setId(task1.getId())
  394 + .setDelegateUserId(1L).setReason("委派")), TASK_DELEGATE_FAIL_USER_REPEAT);
  395 + }
  396 +
  397 + @Test
  398 + public void testTransferTask_success() {
  399 + // 准备参数
  400 + String processInstanceId = startProcessInstance();
  401 + Task task1 = getRunningTask(processInstanceId);
  402 +
  403 + // 调用
  404 + bpmTaskService.transferTask(1L, new BpmTaskTransferReqVO().setId(task1.getId())
  405 + .setAssigneeUserId(2L).setReason("转办"));
  406 + // 断言
  407 + Task transferredTask = bpmTaskService.getTask(task1.getId());
  408 + assertEquals("1", transferredTask.getOwner());
  409 + assertEquals("2", transferredTask.getAssignee());
  410 + assertNull(transferredTask.getDelegationState());
  411 + verify(commentService).createComment(eq(task1.getId()), eq(processInstanceId),
  412 + eq(BpmCommentTypeEnum.TRANSFER), eq("用户1"), eq("用户2"), eq("转办"));
  413 + }
  414 +
  415 + @Test
  416 + public void testTransferTask_emptyAssignee() {
  417 + // 准备参数:task1 计算不到审批人,且未配置自动处理
  418 + when(taskCandidateInvoker.calculateUsersByTask(any())).thenReturn(Set.of());
  419 + String processInstanceId = startProcessInstance();
  420 + Task task1 = getRunningTask(processInstanceId);
  421 + assertNull(task1.getAssignee());
  422 +
  423 + // 调用:转办给用户 2
  424 + bpmTaskService.transferTask(1L, new BpmTaskTransferReqVO().setId(task1.getId())
  425 + .setAssigneeUserId(2L).setReason("转办"));
  426 + // 断言:用户 2 成为审批人
  427 + Task transferredTask = bpmTaskService.getTask(task1.getId());
  428 + assertEquals("2", transferredTask.getAssignee());
  429 + assertNull(transferredTask.getOwner());
  430 + // 调用 + 断言:用户 2 审批通过,流程流转到 task2
  431 + when(taskCandidateInvoker.calculateUsersByTask(any())).thenReturn(Set.of(1L));
  432 + bpmTaskService.approveTask(2L, new BpmTaskApproveReqVO().setId(task1.getId()).setReason("同意"));
  433 + assertEquals("task2", getRunningTask(processInstanceId).getTaskDefinitionKey());
  434 + }
  435 +
  436 + @Test
  437 + public void testCreateSignTask_before_thenApproveChild() {
  438 + // 准备参数
  439 + String processInstanceId = startProcessInstance();
  440 + Task task1 = getRunningTask(processInstanceId);
  441 +
  442 + // 调用:向前加签给用户 2
  443 + bpmTaskService.createSignTask(1L, new BpmTaskSignCreateReqVO().setId(task1.getId())
  444 + .setType(BpmTaskSignTypeEnum.BEFORE.getType()).setUserIds(Set.of(2L)).setReason("加签"));
  445 + // 断言:父任务等待子任务审批,子任务分配给用户 2
  446 + Task parentTask = bpmTaskService.getTask(task1.getId());
  447 + assertNull(parentTask.getAssignee());
  448 + assertEquals("1", parentTask.getOwner());
  449 + assertEquals(BpmTaskSignTypeEnum.BEFORE.getType(), parentTask.getScopeType());
  450 + assertEquals(BpmTaskStatusEnum.WAIT.getStatus(),
  451 + parentTask.getTaskLocalVariables().get(BpmnVariableConstants.TASK_VARIABLE_STATUS));
  452 + List<Task> childTasks = bpmTaskService.getTaskListByParentTaskId(task1.getId());
  453 + assertEquals(1, childTasks.size());
  454 + assertEquals("2", childTasks.get(0).getAssignee());
  455 + verify(commentService).createComment(eq(task1.getId()), eq(processInstanceId),
  456 + eq(BpmCommentTypeEnum.ADD_SIGN), eq("用户1"), eq("向前加签"), eq("用户2"), eq("加签"));
  457 +
  458 + // 调用:用户 2 审批子任务
  459 + bpmTaskService.approveTask(2L, new BpmTaskApproveReqVO().setId(childTasks.get(0).getId()).setReason("同意"));
  460 + // 断言:父任务恢复给用户 1 审批
  461 + Task resolvedTask = bpmTaskService.getTask(task1.getId());
  462 + assertEquals("1", resolvedTask.getAssignee());
  463 + assertNull(resolvedTask.getScopeType());
  464 + assertEquals(BpmTaskStatusEnum.RUNNING.getStatus(),
  465 + resolvedTask.getTaskLocalVariables().get(BpmnVariableConstants.TASK_VARIABLE_STATUS));
  466 + assertTrue(bpmTaskService.getTaskListByParentTaskId(task1.getId()).isEmpty());
  467 + }
  468 +
  469 + @Test
  470 + public void testCreateSignTask_after_thenApprove() {
  471 + // 准备参数
  472 + String processInstanceId = startProcessInstance();
  473 + Task task1 = getRunningTask(processInstanceId);
  474 +
  475 + // 调用:向后加签给用户 2
  476 + bpmTaskService.createSignTask(1L, new BpmTaskSignCreateReqVO().setId(task1.getId())
  477 + .setType(BpmTaskSignTypeEnum.AFTER.getType()).setUserIds(Set.of(2L)).setReason("加签"));
  478 + // 断言:子任务暂由用户 2 拥有,等待父任务审批
  479 + Task childTask = bpmTaskService.getTaskListByParentTaskId(task1.getId()).get(0);
  480 + assertEquals("2", childTask.getOwner());
  481 + assertNull(childTask.getAssignee());
  482 + assertEquals(BpmTaskStatusEnum.WAIT.getStatus(), getTaskStatus(childTask.getId()));
  483 +
  484 + // 调用:用户 1 审批父任务,激活子任务
  485 + bpmTaskService.approveTask(1L, new BpmTaskApproveReqVO().setId(task1.getId()).setReason("同意"));
  486 + // 断言:父任务处于审批通过中,子任务分配给用户 2
  487 + assertEquals(BpmTaskStatusEnum.APPROVING.getStatus(), getTaskStatus(task1.getId()));
  488 + assertEquals("2", bpmTaskService.getTask(childTask.getId()).getAssignee());
  489 + assertEquals(BpmTaskStatusEnum.RUNNING.getStatus(), getTaskStatus(childTask.getId()));
  490 +
  491 + // 调用:用户 2 审批子任务
  492 + bpmTaskService.approveTask(2L, new BpmTaskApproveReqVO().setId(childTask.getId()).setReason("同意"));
  493 + // 断言:父任务自动完成,流程流转到 task2
  494 + assertTaskStatus(task1.getId(), BpmTaskStatusEnum.APPROVE.getStatus(), "同意");
  495 + assertEquals("task2", getRunningTask(processInstanceId).getTaskDefinitionKey());
  496 + }
  497 +
  498 + @Test
  499 + public void testCreateSignTask_typeError() {
  500 + // 准备参数
  501 + String processInstanceId = startProcessInstance();
  502 + Task task1 = getRunningTask(processInstanceId);
  503 + bpmTaskService.createSignTask(1L, new BpmTaskSignCreateReqVO().setId(task1.getId())
  504 + .setType(BpmTaskSignTypeEnum.AFTER.getType()).setUserIds(Set.of(2L)).setReason("加签"));
  505 +
  506 + // 调用,并断言异常
  507 + assertServiceException(() -> bpmTaskService.createSignTask(1L, new BpmTaskSignCreateReqVO().setId(task1.getId())
  508 + .setType(BpmTaskSignTypeEnum.BEFORE.getType()).setUserIds(Set.of(3L)).setReason("加签")),
  509 + TASK_SIGN_CREATE_TYPE_ERROR, "向后加签", "向前加签");
  510 + }
  511 +
  512 + @Test
  513 + public void testCreateSignTask_userRepeat() {
  514 + // 准备参数
  515 + String processInstanceId = startProcessInstance();
  516 + Task task1 = getRunningTask(processInstanceId);
  517 +
  518 + // 调用,并断言异常:用户 1 已经是当前节点的审批人
  519 + assertServiceException(() -> bpmTaskService.createSignTask(1L, new BpmTaskSignCreateReqVO().setId(task1.getId())
  520 + .setType(BpmTaskSignTypeEnum.BEFORE.getType()).setUserIds(Set.of(1L)).setReason("加签")),
  521 + TASK_SIGN_CREATE_USER_REPEAT, "用户1");
  522 + }
  523 +
  524 + @Test
  525 + public void testDeleteSignTask_success() {
  526 + // 准备参数
  527 + String processInstanceId = startProcessInstance();
  528 + Task task1 = getRunningTask(processInstanceId);
  529 + bpmTaskService.createSignTask(1L, new BpmTaskSignCreateReqVO().setId(task1.getId())
  530 + .setType(BpmTaskSignTypeEnum.BEFORE.getType()).setUserIds(Set.of(2L)).setReason("加签"));
  531 + Task childTask = bpmTaskService.getTaskListByParentTaskId(task1.getId()).get(0);
  532 +
  533 + // 调用
  534 + bpmTaskService.deleteSignTask(1L, new BpmTaskSignDeleteReqVO().setId(childTask.getId()).setReason("减签"));
  535 + // 断言:子任务被取消删除,父任务恢复给用户 1 审批
  536 + assertNull(bpmTaskService.getTask(childTask.getId()));
  537 + assertEquals(BpmTaskStatusEnum.CANCEL.getStatus(), getTaskStatus(childTask.getId()));
  538 + Task parentTask = bpmTaskService.getTask(task1.getId());
  539 + assertEquals("1", parentTask.getAssignee());
  540 + assertNull(parentTask.getScopeType());
  541 + verify(commentService).createComment(eq(task1.getId()), eq(processInstanceId),
  542 + eq(BpmCommentTypeEnum.SUB_SIGN), eq("用户1"), eq("用户2"));
  543 + }
  544 +
  545 + @Test
  546 + public void testDeleteSignTask_partial() {
  547 + // 准备参数:task1 向前加签给用户 2、用户 3
  548 + String processInstanceId = startProcessInstance();
  549 + Task task1 = getRunningTask(processInstanceId);
  550 + bpmTaskService.createSignTask(1L, new BpmTaskSignCreateReqVO().setId(task1.getId())
  551 + .setType(BpmTaskSignTypeEnum.BEFORE.getType()).setUserIds(Set.of(2L, 3L)).setReason("加签"));
  552 + Map<String, Task> childTasks = bpmTaskService.getTaskListByParentTaskId(task1.getId()).stream()
  553 + .collect(Collectors.toMap(Task::getAssignee, Function.identity()));
  554 +
  555 + // 调用:减签用户 2
  556 + bpmTaskService.deleteSignTask(1L, new BpmTaskSignDeleteReqVO().setId(childTasks.get("2").getId())
  557 + .setReason("减签"));
  558 + // 断言:用户 3 的加签任务仍在,父任务继续等待
  559 + assertEquals(BpmTaskStatusEnum.CANCEL.getStatus(), getTaskStatus(childTasks.get("2").getId()));
  560 + assertEquals(List.of(childTasks.get("3").getId()), bpmTaskService.getTaskListByParentTaskId(task1.getId())
  561 + .stream().map(Task::getId).toList());
  562 + Task parentTask = bpmTaskService.getTask(task1.getId());
  563 + assertNull(parentTask.getAssignee());
  564 + assertEquals(BpmTaskSignTypeEnum.BEFORE.getType(), parentTask.getScopeType());
  565 +
  566 + // 调用 + 断言:用户 3 审批通过后,父任务恢复给用户 1 审批
  567 + bpmTaskService.approveTask(3L, new BpmTaskApproveReqVO().setId(childTasks.get("3").getId()).setReason("同意"));
  568 + assertEquals("1", bpmTaskService.getTask(task1.getId()).getAssignee());
  569 + }
  570 +
  571 + @Test
  572 + public void testDeleteSignTask_afterSignNotApproved() {
  573 + // 准备参数:task1 向后加签给用户 2,但 task1 尚未审批
  574 + String processInstanceId = startProcessInstance();
  575 + Task task1 = getRunningTask(processInstanceId);
  576 + bpmTaskService.createSignTask(1L, new BpmTaskSignCreateReqVO().setId(task1.getId())
  577 + .setType(BpmTaskSignTypeEnum.AFTER.getType()).setUserIds(Set.of(2L)).setReason("加签"));
  578 + Task childTask = bpmTaskService.getTaskListByParentTaskId(task1.getId()).get(0);
  579 +
  580 + // 调用
  581 + bpmTaskService.deleteSignTask(1L, new BpmTaskSignDeleteReqVO().setId(childTask.getId()).setReason("减签"));
  582 + // 断言:父任务不会因为加签任务被减签而自动完成,仍由用户 1 审批
  583 + Task parentTask = getRunningTask(processInstanceId);
  584 + assertEquals(task1.getId(), parentTask.getId());
  585 + assertEquals("1", parentTask.getAssignee());
  586 + assertNull(parentTask.getScopeType());
  587 + assertEquals(BpmTaskStatusEnum.RUNNING.getStatus(), getTaskStatus(task1.getId()));
  588 + }
  589 +
  590 + @Test
  591 + public void testDeleteSignTask_noParent() {
  592 + // 准备参数
  593 + String processInstanceId = startProcessInstance();
  594 + Task task1 = getRunningTask(processInstanceId);
  595 +
  596 + // 调用,并断言异常
  597 + assertServiceException(() -> bpmTaskService.deleteSignTask(1L,
  598 + new BpmTaskSignDeleteReqVO().setId(task1.getId()).setReason("减签")), TASK_SIGN_DELETE_NO_PARENT);
  599 + }
  600 +
  601 + @Test
  602 + public void testWithdrawTask_success() {
  603 + // 准备参数
  604 + String processInstanceId = startProcessInstance();
  605 + Task task1 = getRunningTask(processInstanceId);
  606 + approveRunningTask(processInstanceId);
  607 + Task task2 = getRunningTask(processInstanceId);
  608 +
  609 + // 调用
  610 + bpmTaskService.withdrawTask(1L, task1.getId());
  611 + // 断言:task2 被取消,流程回到 task1
  612 + assertTaskStatus(task2.getId(), BpmTaskStatusEnum.CANCEL.getStatus(), "前一任务撤回,系统自动取消");
  613 + verify(commentService).createComment(eq(task2.getId()), eq(processInstanceId),
  614 + eq(BpmCommentTypeEnum.CANCEL), eq("前一节点撤回"));
  615 + assertEquals("task1", getRunningTask(processInstanceId).getTaskDefinitionKey());
  616 + }
  617 +
  618 + @Test
  619 + public void testWithdrawTask_taskNotExists() {
  620 + // 准备参数:task1 尚未审批
  621 + String processInstanceId = startProcessInstance();
  622 + Task task1 = getRunningTask(processInstanceId);
  623 +
  624 + // 调用,并断言异常
  625 + assertServiceException(() -> bpmTaskService.withdrawTask(1L, task1.getId()), TASK_WITHDRAW_FAIL_TASK_NOT_EXISTS);
  626 + }
  627 +
  628 + @Test
  629 + public void testWithdrawTask_nextTaskApproved() {
  630 + // 准备参数:task1、task2 均已审批,流程已经结束
  631 + String processInstanceId = startProcessInstance();
  632 + Task task1 = getRunningTask(processInstanceId);
  633 + approveRunningTask(processInstanceId);
  634 + approveRunningTask(processInstanceId);
  635 +
  636 + // 调用,并断言异常
  637 + assertServiceException(() -> bpmTaskService.withdrawTask(1L, task1.getId()),
  638 + TASK_WITHDRAW_FAIL_PROCESS_NOT_RUNNING);
  639 + }
  640 +
  641 + @Test
  642 + public void testWithdrawTask_notAllow() {
  643 + // 准备参数
  644 + String processInstanceId = startProcessInstance();
  645 + Task task1 = getRunningTask(processInstanceId);
  646 + approveRunningTask(processInstanceId);
  647 + when(processDefinitionService.getProcessDefinitionInfo(anyString()))
  648 + .thenReturn(new BpmProcessDefinitionInfoDO().setAllowWithdrawTask(false));
  649 +
  650 + // 调用,并断言异常
  651 + assertServiceException(() -> bpmTaskService.withdrawTask(1L, task1.getId()), TASK_WITHDRAW_FAIL_NOT_ALLOW);
  652 + }
  653 +
  654 + @Test
  655 + public void testMoveTaskToEnd_success() {
  656 + // 准备参数
  657 + String processInstanceId = startProcessInstance();
  658 + Task task1 = getRunningTask(processInstanceId);
  659 +
  660 + // 调用
  661 + bpmTaskService.moveTaskToEnd(processInstanceId, "结束流程");
  662 + // 断言:运行中的任务被取消,流程实例结束
  663 + assertTaskStatus(task1.getId(), BpmTaskStatusEnum.CANCEL.getStatus(), "系统自动取消");
  664 + assertNull(processInstanceService.getProcessInstance(processInstanceId));
  665 + }
  666 +
  667 + @Test
  668 + public void testMoveTaskToEnd_withSignTask() {
  669 + // 准备参数:task1 向前加签给用户 2,子任务未审批
  670 + String processInstanceId = startProcessInstance();
  671 + Task task1 = getRunningTask(processInstanceId);
  672 + bpmTaskService.createSignTask(1L, new BpmTaskSignCreateReqVO().setId(task1.getId())
  673 + .setType(BpmTaskSignTypeEnum.BEFORE.getType()).setUserIds(Set.of(2L)).setReason("加签"));
  674 + Task childTask = bpmTaskService.getTaskListByParentTaskId(task1.getId()).get(0);
  675 +
  676 + // 调用
  677 + bpmTaskService.moveTaskToEnd(processInstanceId, "结束流程");
  678 + // 断言:父任务、子任务都被取消,流程实例结束
  679 + assertTaskStatus(task1.getId(), BpmTaskStatusEnum.CANCEL.getStatus(), "系统自动取消");
  680 + assertTaskStatus(childTask.getId(), BpmTaskStatusEnum.CANCEL.getStatus(), "系统自动取消");
  681 + assertNull(processInstanceService.getProcessInstance(processInstanceId));
  682 + assertNull(bpmTaskService.getTask(childTask.getId()));
  683 + }
  684 +
  685 + @Test
  686 + public void testProcessTaskTimeout_approve() {
  687 + // 准备参数
  688 + String processInstanceId = startProcessInstance();
  689 + Task task1 = getRunningTask(processInstanceId);
  690 +
  691 + // 调用:task1 超时自动通过
  692 + bpmTaskService.processTaskTimeout(processInstanceId, "task1",
  693 + BpmUserTaskTimeoutHandlerTypeEnum.APPROVE.getType());
  694 + // 断言
  695 + assertTaskStatus(task1.getId(), BpmTaskStatusEnum.APPROVE.getStatus(), BpmReasonEnum.TIMEOUT_APPROVE.getReason());
  696 + assertEquals("task2", getRunningTask(processInstanceId).getTaskDefinitionKey());
  697 + }
  698 +
  699 + @Test
  700 + public void testProcessTaskTimeout_reject() {
  701 + // 准备参数
  702 + String processInstanceId = startProcessInstance();
  703 + Task task1 = getRunningTask(processInstanceId);
  704 +
  705 + // 调用:task1 超时自动拒绝
  706 + bpmTaskService.processTaskTimeout(processInstanceId, "task1",
  707 + BpmUserTaskTimeoutHandlerTypeEnum.REJECT.getType());
  708 + // 断言:任务不通过,流程实例不通过并结束
  709 + assertEquals(BpmTaskStatusEnum.REJECT.getStatus(), getTaskStatus(task1.getId()));
  710 + assertNull(processInstanceService.getProcessInstance(processInstanceId));
  711 + assertEquals(BpmProcessInstanceStatusEnum.REJECT.getStatus(), getHistoricProcessVariable(processInstanceId,
  712 + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS));
  713 + }
  714 +
  715 + // ========== 流程实例相关 ==========
  716 +
  717 + @Test
  718 + public void testCreateProcessInstance_withTitle() {
  719 + // 准备参数:开启标题设置
  720 + BpmModelMetaInfoVO.TitleSetting titleSetting = new BpmModelMetaInfoVO.TitleSetting().setEnable(true)
  721 + .setTitle("{" + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_ID + "}的"
  722 + + "{" + BpmnVariableConstants.PROCESS_DEFINITION_NAME + "},请假{day}天");
  723 + doAnswer(invocation -> new BpmProcessDefinitionInfoDO().setProcessDefinitionId(invocation.getArgument(0))
  724 + .setModelType(BpmModelTypeEnum.BPMN.getType()).setTitleSetting(titleSetting))
  725 + .when(processDefinitionService).getProcessDefinitionInfo(anyString());
  726 + String processDefinitionId = repositoryService.createProcessDefinitionQuery()
  727 + .deploymentId(deployment.getId()).singleResult().getId();
  728 +
  729 + // 调用:模拟 FlowableWebFilter 设置发起人
  730 + String processInstanceId = FlowableUtils.executeAuthenticatedUserId(1L,
  731 + () -> processInstanceService.createProcessInstance(1L, new BpmProcessInstanceCreateReqVO()
  732 + .setProcessDefinitionId(processDefinitionId).setVariables(new HashMap<>(Map.of("day", 3)))));
  733 + // 断言
  734 + ProcessInstance processInstance = processInstanceService.getProcessInstance(processInstanceId);
  735 + assertEquals("用户1的两级审批,请假3天", processInstance.getName());
  736 + assertEquals(3, runtimeService.getVariable(processInstanceId, "day"));
  737 + assertEquals(1L, runtimeService.getVariable(processInstanceId,
  738 + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_ID));
  739 + assertEquals(BpmProcessInstanceStatusEnum.RUNNING.getStatus(), runtimeService.getVariable(processInstanceId,
  740 + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS));
  741 + assertEquals("task1", getRunningTask(processInstanceId).getTaskDefinitionKey());
  742 + }
  743 +
  744 + @Test
  745 + public void testCancelProcessInstanceByStartUser_success() {
  746 + // 准备参数
  747 + String processInstanceId = startProcessInstance();
  748 + Task task1 = getRunningTask(processInstanceId);
  749 +
  750 + // 调用
  751 + processInstanceService.cancelProcessInstanceByStartUser(1L, new BpmProcessInstanceCancelReqVO()
  752 + .setId(processInstanceId).setReason("不想请了"));
  753 + // 断言:流程实例取消并结束,运行中的任务被取消
  754 + assertNull(processInstanceService.getProcessInstance(processInstanceId));
  755 + assertEquals(BpmProcessInstanceStatusEnum.CANCEL.getStatus(), getHistoricProcessVariable(processInstanceId,
  756 + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS));
  757 + assertEquals(BpmReasonEnum.CANCEL_PROCESS_INSTANCE_BY_START_USER.format("不想请了"),
  758 + getHistoricProcessVariable(processInstanceId, BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_REASON));
  759 + assertTaskStatus(task1.getId(), BpmTaskStatusEnum.CANCEL.getStatus(), BpmReasonEnum.CANCEL_BY_SYSTEM.getReason());
  760 + }
  761 +
  762 + @Test
  763 + public void testCancelProcessInstanceByStartUser_notSelf() {
  764 + // 准备参数
  765 + String processInstanceId = startProcessInstance();
  766 +
  767 + // 调用,并断言异常
  768 + assertServiceException(() -> processInstanceService.cancelProcessInstanceByStartUser(2L,
  769 + new BpmProcessInstanceCancelReqVO().setId(processInstanceId).setReason("取消")),
  770 + PROCESS_INSTANCE_CANCEL_FAIL_NOT_SELF);
  771 + assertNotNull(processInstanceService.getProcessInstance(processInstanceId));
  772 + }
  773 +
  774 + @Test
  775 + public void testCancelProcessInstanceByAdmin_success() {
  776 + // 准备参数:task1 已审批,流程处于 task2
  777 + String processInstanceId = startProcessInstance();
  778 + approveRunningTask(processInstanceId);
  779 +
  780 + // 调用
  781 + processInstanceService.cancelProcessInstanceByAdmin(100L, new BpmProcessInstanceCancelReqVO()
  782 + .setId(processInstanceId).setReason("流程配置错误"));
  783 + // 断言
  784 + assertNull(processInstanceService.getProcessInstance(processInstanceId));
  785 + assertEquals(BpmProcessInstanceStatusEnum.CANCEL.getStatus(), getHistoricProcessVariable(processInstanceId,
  786 + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS));
  787 + assertEquals(BpmReasonEnum.CANCEL_PROCESS_INSTANCE_BY_ADMIN.format("用户100", "流程配置错误"),
  788 + getHistoricProcessVariable(processInstanceId, BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_REASON));
  789 + }
  790 +
  791 + @Test
  792 + public void testUpdateAndRemoveProcessInstanceVariables_success() {
  793 + // 准备参数
  794 + String processInstanceId = startProcessInstance();
  795 +
  796 + // 调用 + 断言:更新变量
  797 + processInstanceService.updateProcessInstanceVariables(processInstanceId, Map.of("day", 5));
  798 + assertEquals(5, runtimeService.getVariable(processInstanceId, "day"));
  799 + // 调用 + 断言:删除变量
  800 + processInstanceService.removeProcessInstanceVariables(processInstanceId, Set.of("day"));
  801 + assertFalse(runtimeService.hasVariable(processInstanceId, "day"));
  802 + }
  803 +
  804 + @Test
  805 + public void testGetTodoTask_success() {
  806 + // 准备参数
  807 + String processInstanceId = startProcessInstance();
  808 + Task task1 = getRunningTask(processInstanceId);
  809 +
  810 + // 调用:不传 taskId 时,按流程实例查询首个待办
  811 + BpmTaskRespVO todoTask = bpmTaskService.getTodoTask(1L, null, processInstanceId);
  812 + // 断言
  813 + assertEquals(task1.getId(), todoTask.getId());
  814 + assertEquals("一级审批", todoTask.getName());
  815 + assertFalse(todoTask.getSignEnable());
  816 + assertFalse(todoTask.getReasonRequire());
  817 + // 断言:其他用户没有待办
  818 + assertNull(bpmTaskService.getTodoTask(2L, task1.getId(), processInstanceId));
  819 + }
  820 +
  821 + @Test
  822 + public void testGetTaskTodoPageAndDonePage_success() {
  823 + // 准备参数
  824 + String processInstanceId = startProcessInstance();
  825 + approveRunningTask(processInstanceId);
  826 + BpmTaskPageReqVO pageReqVO = new BpmTaskPageReqVO();
  827 + pageReqVO.setName("审批");
  828 + pageReqVO.setProcessDefinitionKey("twoStepApprove");
  829 +
  830 + // 调用 + 断言:待办为 task2
  831 + List<Task> todoTasks = bpmTaskService.getTaskTodoPage(1L, pageReqVO).getList();
  832 + assertEquals(List.of("task2"), todoTasks.stream().map(Task::getTaskDefinitionKey).toList());
  833 + // 调用 + 断言:已办为 task1
  834 + pageReqVO.setStatus(BpmTaskStatusEnum.APPROVE.getStatus());
  835 + List<HistoricTaskInstance> doneTasks = bpmTaskService.getTaskDonePage(1L, pageReqVO).getList();
  836 + assertEquals(List.of("task1"), doneTasks.stream().map(HistoricTaskInstance::getTaskDefinitionKey).toList());
  837 + // 调用 + 断言:其它用户没有待办
  838 + assertEquals(0, bpmTaskService.getTaskTodoPage(2L, new BpmTaskPageReqVO()).getTotal());
  839 + }
  840 +
  841 + // ========== 状态回调相关(依赖 BpmTaskEventListener、BpmProcessInstanceEventListener) ==========
  842 +
  843 + @Test
  844 + public void testProcessCallback_approve() {
  845 + // 调用:发起流程
  846 + String processInstanceId = startProcessInstance();
  847 + // 断言:任务创建回调将 task1 置为审批中,任务分配回调发送待办消息
  848 + Task task1 = getRunningTask(processInstanceId);
  849 + assertEquals(BpmTaskStatusEnum.RUNNING.getStatus(), getTaskStatus(task1.getId()));
  850 + verify(messageService).sendMessageWhenTaskAssigned(argThat(reqDTO ->
  851 + task1.getId().equals(reqDTO.getTaskId()) && reqDTO.getAssigneeUserId().equals(1L)
  852 + && reqDTO.getStartUserId().equals(1L) && "用户1".equals(reqDTO.getStartUserNickname())));
  853 +
  854 + // 调用:task1、task2 依次审批通过
  855 + approveRunningTask(processInstanceId);
  856 + approveRunningTask(processInstanceId);
  857 + // 断言:流程完成回调将状态更新为审批通过,并发送消息、状态事件
  858 + assertEquals(BpmProcessInstanceStatusEnum.APPROVE.getStatus(), getHistoricProcessVariable(processInstanceId,
  859 + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS));
  860 + verify(messageService, times(2)).sendMessageWhenTaskAssigned(any());
  861 + verify(messageService).sendMessageWhenProcessInstanceApprove(argThat(reqDTO ->
  862 + processInstanceId.equals(reqDTO.getProcessInstanceId()) && reqDTO.getStartUserId().equals(1L)));
  863 + BpmProcessInstanceStatusEvent event = getProcessInstanceStatusEvent(processInstanceId);
  864 + assertEquals(BpmProcessInstanceStatusEnum.APPROVE.getStatus(), event.getStatus());
  865 + assertEquals("twoStepApprove", event.getProcessDefinitionKey());
  866 + assertEquals("business-1", event.getBusinessKey());
  867 + }
  868 +
  869 + @Test
  870 + public void testProcessCallback_reject() {
  871 + // 准备参数
  872 + String processInstanceId = startProcessInstance();
  873 + Task task1 = getRunningTask(processInstanceId);
  874 +
  875 + // 调用
  876 + bpmTaskService.rejectTask(1L, new BpmTaskRejectReqVO().setId(task1.getId()).setReason("不同意"));
  877 + // 断言:流程完成回调发送不通过消息、状态事件,且不会误发通过消息
  878 + verify(messageService).sendMessageWhenProcessInstanceReject(argThat(reqDTO ->
  879 + processInstanceId.equals(reqDTO.getProcessInstanceId()) && "审批不通过任务,原因:不同意".equals(reqDTO.getReason())));
  880 + verify(messageService, never()).sendMessageWhenProcessInstanceApprove(any());
  881 + BpmProcessInstanceStatusEvent event = getProcessInstanceStatusEvent(processInstanceId);
  882 + assertEquals(BpmProcessInstanceStatusEnum.REJECT.getStatus(), event.getStatus());
  883 + assertEquals("审批不通过任务,原因:不同意", event.getReason());
  884 + }
  885 +
  886 + @Test
  887 + public void testProcessCallback_cancel() {
  888 + // 准备参数
  889 + String processInstanceId = startProcessInstance();
  890 +
  891 + // 调用
  892 + processInstanceService.cancelProcessInstanceByStartUser(1L, new BpmProcessInstanceCancelReqVO()
  893 + .setId(processInstanceId).setReason("不想请了"));
  894 + // 断言:取消时只发送状态事件,不发送通过、不通过消息
  895 + BpmProcessInstanceStatusEvent event = getProcessInstanceStatusEvent(processInstanceId);
  896 + assertEquals(BpmProcessInstanceStatusEnum.CANCEL.getStatus(), event.getStatus());
  897 + assertEquals(BpmReasonEnum.CANCEL_PROCESS_INSTANCE_BY_START_USER.format("不想请了"), event.getReason());
  898 + verify(messageService, never()).sendMessageWhenProcessInstanceApprove(any());
  899 + verify(messageService, never()).sendMessageWhenProcessInstanceReject(any());
  900 + }
  901 +
  902 + // ========== 自动审批相关(依赖 getSelf() 获取 Spring 代理) ==========
  903 +
  904 + @Test
  905 + public void testProcessTaskCreated_autoApprove() {
  906 + // 准备参数:task1 配置为【自动通过】
  907 + mockUserTaskExtensionElement("task1", BpmnModelConstants.USER_TASK_APPROVE_TYPE,
  908 + BpmUserTaskApproveTypeEnum.AUTO_APPROVE.getType());
  909 + mockTaskWithoutCandidate("task1");
  910 +
  911 + // 调用
  912 + String processInstanceId = startProcessInstance();
  913 + // 断言:task1 在事务结束后自动通过,流程流转到 task2
  914 + HistoricTaskInstance task1 = getHistoricTask(processInstanceId, "task1");
  915 + assertTaskStatus(task1.getId(), BpmTaskStatusEnum.APPROVE.getStatus(),
  916 + BpmReasonEnum.APPROVE_TYPE_AUTO_APPROVE.getReason());
  917 + assertEquals("task2", getRunningTask(processInstanceId).getTaskDefinitionKey());
  918 + }
  919 +
  920 + @Test
  921 + public void testProcessTaskCreated_autoReject() {
  922 + // 准备参数:task1 配置为【自动拒绝】
  923 + mockUserTaskExtensionElement("task1", BpmnModelConstants.USER_TASK_APPROVE_TYPE,
  924 + BpmUserTaskApproveTypeEnum.AUTO_REJECT.getType());
  925 + mockTaskWithoutCandidate("task1");
  926 +
  927 + // 调用
  928 + String processInstanceId = startProcessInstance();
  929 + // 断言:task1 自动拒绝,流程实例不通过并结束
  930 + HistoricTaskInstance task1 = getHistoricTask(processInstanceId, "task1");
  931 + assertTaskStatus(task1.getId(), BpmTaskStatusEnum.REJECT.getStatus(),
  932 + BpmReasonEnum.APPROVE_TYPE_AUTO_REJECT.getReason());
  933 + assertNull(processInstanceService.getProcessInstance(processInstanceId));
  934 + assertEquals(BpmProcessInstanceStatusEnum.REJECT.getStatus(), getHistoricProcessVariable(processInstanceId,
  935 + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS));
  936 + }
  937 +
  938 + @Test
  939 + public void testProcessTaskAssigned_autoApproveAll() {
  940 + // 准备参数:流程配置【仅审批一次,后续重复的审批节点均自动通过】
  941 + mockAutoApprovalType(BpmAutoApproveTypeEnum.APPROVE_ALL);
  942 + String processInstanceId = startProcessInstance();
  943 +
  944 + // 调用:用户 1 审批 task1
  945 + approveRunningTask(processInstanceId);
  946 + // 断言:task2 仍由用户 1 审批,自动通过,流程结束
  947 + HistoricTaskInstance task2 = getHistoricTask(processInstanceId, "task2");
  948 + assertTaskStatus(task2.getId(), BpmTaskStatusEnum.APPROVE.getStatus(), BpmAutoApproveTypeEnum.APPROVE_ALL.getName());
  949 + assertNull(processInstanceService.getProcessInstance(processInstanceId));
  950 + assertEquals(BpmProcessInstanceStatusEnum.APPROVE.getStatus(), getHistoricProcessVariable(processInstanceId,
  951 + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS));
  952 + }
  953 +
  954 + @Test
  955 + public void testProcessTaskAssigned_autoApproveSequent() {
  956 + // 准备参数:流程配置【仅针对连续审批的节点自动通过】
  957 + mockAutoApprovalType(BpmAutoApproveTypeEnum.APPROVE_SEQUENT);
  958 + String processInstanceId = startProcessInstance();
  959 +
  960 + // 调用:用户 1 审批 task1
  961 + approveRunningTask(processInstanceId);
  962 + // 断言:task2 的上一节点 task1 也由用户 1 审批,自动通过
  963 + HistoricTaskInstance task2 = getHistoricTask(processInstanceId, "task2");
  964 + assertTaskStatus(task2.getId(), BpmTaskStatusEnum.APPROVE.getStatus(),
  965 + BpmAutoApproveTypeEnum.APPROVE_SEQUENT.getName());
  966 + assertNull(processInstanceService.getProcessInstance(processInstanceId));
  967 + }
  968 +
  969 + // ========== 定时器超时相关(依赖 BpmTaskEventListener#timerFired) ==========
  970 +
  971 + @Test
  972 + public void testTimerFired_userTaskTimeoutApprove() {
  973 + // 准备参数
  974 + deploy("bpmn/timeout-approve.bpmn20.xml");
  975 + String processInstanceId = startProcessInstance("timeoutApprove");
  976 + Task task1 = getRunningTask(processInstanceId);
  977 +
  978 + // 调用:触发 task1 的超时定时器
  979 + executeTimerJob(processInstanceId);
  980 + // 断言:task1 超时自动通过,流程结束
  981 + assertTaskStatus(task1.getId(), BpmTaskStatusEnum.APPROVE.getStatus(), BpmReasonEnum.TIMEOUT_APPROVE.getReason());
  982 + assertNull(processInstanceService.getProcessInstance(processInstanceId));
  983 + assertEquals(BpmProcessInstanceStatusEnum.APPROVE.getStatus(), getHistoricProcessVariable(processInstanceId,
  984 + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS));
  985 + }
  986 +
  987 + @Test
  988 + public void testTimerFired_userTaskTimeoutReminder() {
  989 + // 准备参数:超时处理方式改为【自动提醒】
  990 + deploy("bpmn/timeout-approve.bpmn20.xml");
  991 + mockBoundaryEventTimeoutHandlerType("task1-timeout", BpmUserTaskTimeoutHandlerTypeEnum.REMINDER);
  992 + String processInstanceId = startProcessInstance("timeoutApprove");
  993 + Task task1 = getRunningTask(processInstanceId);
  994 +
  995 + // 调用
  996 + executeTimerJob(processInstanceId);
  997 + // 断言:发送超时提醒,任务保持审批中
  998 + verify(messageService).sendMessageWhenTaskTimeout(argThat(reqDTO ->
  999 + task1.getId().equals(reqDTO.getTaskId()) && reqDTO.getAssigneeUserId().equals(1L)
  1000 + && processInstanceId.equals(reqDTO.getProcessInstanceId())));
  1001 + assertEquals(task1.getId(), getRunningTask(processInstanceId).getId());
  1002 + assertEquals(BpmTaskStatusEnum.RUNNING.getStatus(), getTaskStatus(task1.getId()));
  1003 + }
  1004 +
  1005 + @Test
  1006 + public void testTimerFired_userTaskTimeoutReject() {
  1007 + // 准备参数:超时处理方式改为【自动拒绝】
  1008 + deploy("bpmn/timeout-approve.bpmn20.xml");
  1009 + mockBoundaryEventTimeoutHandlerType("task1-timeout", BpmUserTaskTimeoutHandlerTypeEnum.REJECT);
  1010 + String processInstanceId = startProcessInstance("timeoutApprove");
  1011 + Task task1 = getRunningTask(processInstanceId);
  1012 +
  1013 + // 调用
  1014 + executeTimerJob(processInstanceId);
  1015 + // 断言:task1 超时自动拒绝,流程实例不通过并结束
  1016 + assertTaskStatus(task1.getId(), BpmTaskStatusEnum.REJECT.getStatus(), BpmReasonEnum.REJECT_TASK.getReason());
  1017 + assertNull(processInstanceService.getProcessInstance(processInstanceId));
  1018 + assertEquals(BpmProcessInstanceStatusEnum.REJECT.getStatus(), getHistoricProcessVariable(processInstanceId,
  1019 + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS));
  1020 + }
  1021 +
  1022 + @Test
  1023 + public void testTimerFired_delayTimer() {
  1024 + // 准备参数
  1025 + deploy("bpmn/delay-timer.bpmn20.xml");
  1026 + String processInstanceId = startProcessInstance("delayTimer");
  1027 + assertNotNull(runtimeService.createExecutionQuery().processInstanceId(processInstanceId)
  1028 + .activityId("wait").singleResult());
  1029 +
  1030 + // 调用:延迟器到期
  1031 + executeTimerJob(processInstanceId);
  1032 + // 断言:receiveTask 被触发,流程结束
  1033 + assertNull(processInstanceService.getProcessInstance(processInstanceId));
  1034 + assertEquals(BpmProcessInstanceStatusEnum.APPROVE.getStatus(), getHistoricProcessVariable(processInstanceId,
  1035 + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS));
  1036 + }
  1037 +
  1038 + // ========== 子流程相关 ==========
  1039 +
  1040 + @Test
  1041 + public void testTimerFired_childProcessTimeout() {
  1042 + // 准备参数:主流程发起 twoStepApprove 子流程
  1043 + deploy("bpmn/call-activity.bpmn20.xml");
  1044 + String parentId = startProcessInstance("callActivityParent");
  1045 + ProcessInstance child = getChildProcessInstance(parentId);
  1046 + Task childTask = getRunningTask(child.getId());
  1047 +
  1048 + // 调用:触发子流程的超时定时器
  1049 + executeTimerJob(parentId);
  1050 + // 断言:子流程被结束、任务被取消,主流程继续流转并结束
  1051 + assertTaskStatus(childTask.getId(), BpmTaskStatusEnum.CANCEL.getStatus(), BpmReasonEnum.CANCEL_BY_SYSTEM.getReason());
  1052 + assertNull(processInstanceService.getProcessInstance(child.getId()));
  1053 + assertNull(processInstanceService.getProcessInstance(parentId));
  1054 + assertEquals(BpmProcessInstanceStatusEnum.APPROVE.getStatus(), getHistoricProcessVariable(parentId,
  1055 + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS));
  1056 + }
  1057 +
  1058 + @Test
  1059 + public void testRejectTask_childProcessRejectParent() {
  1060 + // 准备参数
  1061 + deploy("bpmn/call-activity.bpmn20.xml");
  1062 + String parentId = startProcessInstance("callActivityParent");
  1063 + ProcessInstance child = getChildProcessInstance(parentId);
  1064 + Task childTask = getRunningTask(child.getId());
  1065 +
  1066 + // 调用:子流程审批不通过
  1067 + bpmTaskService.rejectTask(1L, new BpmTaskRejectReqVO().setId(childTask.getId()).setReason("不同意"));
  1068 + // 断言:子流程不通过时,主流程同样标记为不通过并结束
  1069 + assertNull(processInstanceService.getProcessInstance(child.getId()));
  1070 + assertNull(processInstanceService.getProcessInstance(parentId));
  1071 + assertEquals(BpmProcessInstanceStatusEnum.REJECT.getStatus(), getHistoricProcessVariable(parentId,
  1072 + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS));
  1073 + assertEquals(BpmReasonEnum.REJECT_TASK.format(BpmReasonEnum.REJECT_CHILD_PROCESS.getReason()),
  1074 + getHistoricProcessVariable(parentId, BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_REASON));
  1075 + assertEquals(BpmProcessInstanceStatusEnum.REJECT.getStatus(), getProcessInstanceStatusEvent(parentId).getStatus());
  1076 + }
  1077 +
  1078 + @Test
  1079 + public void testCancelProcessInstanceByStartUser_cancelChildProcess() {
  1080 + // 准备参数
  1081 + deploy("bpmn/call-activity.bpmn20.xml");
  1082 + String parentId = startProcessInstance("callActivityParent");
  1083 + ProcessInstance child = getChildProcessInstance(parentId);
  1084 + Task childTask = getRunningTask(child.getId());
  1085 +
  1086 + // 调用:取消主流程
  1087 + processInstanceService.cancelProcessInstanceByStartUser(1L, new BpmProcessInstanceCancelReqVO()
  1088 + .setId(parentId).setReason("不想请了"));
  1089 + // 断言:主流程、子流程都被取消
  1090 + assertNull(processInstanceService.getProcessInstance(parentId));
  1091 + assertNull(processInstanceService.getProcessInstance(child.getId()));
  1092 + assertEquals(BpmProcessInstanceStatusEnum.CANCEL.getStatus(), getHistoricProcessVariable(parentId,
  1093 + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS));
  1094 + assertEquals(BpmProcessInstanceStatusEnum.CANCEL.getStatus(), getHistoricProcessVariable(child.getId(),
  1095 + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS));
  1096 + assertEquals(BpmReasonEnum.CANCEL_CHILD_PROCESS_INSTANCE_BY_MAIN_PROCESS.getReason(),
  1097 + getHistoricProcessVariable(child.getId(), BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_REASON));
  1098 + assertTaskStatus(childTask.getId(), BpmTaskStatusEnum.CANCEL.getStatus(), BpmReasonEnum.CANCEL_BY_SYSTEM.getReason());
  1099 + }
  1100 +
  1101 + @Test
  1102 + public void testCancelProcessInstanceByStartUser_childNotAllow() {
  1103 + // 准备参数
  1104 + deploy("bpmn/call-activity.bpmn20.xml");
  1105 + String parentId = startProcessInstance("callActivityParent");
  1106 + ProcessInstance child = getChildProcessInstance(parentId);
  1107 +
  1108 + // 调用,并断言异常:子流程不允许单独取消
  1109 + assertServiceException(() -> processInstanceService.cancelProcessInstanceByStartUser(1L,
  1110 + new BpmProcessInstanceCancelReqVO().setId(child.getId()).setReason("取消")),
  1111 + PROCESS_INSTANCE_CANCEL_CHILD_FAIL_NOT_ALLOW);
  1112 + }
  1113 +
  1114 + // ========== 私有方法 ==========
  1115 +
  1116 + private String startProcessInstance() {
  1117 + return startProcessInstance("twoStepApprove");
  1118 + }
  1119 +
  1120 + private String startProcessInstance(String processDefinitionKey) {
  1121 + return processInstanceService.createProcessInstance(1L, new BpmProcessInstanceCreateReqDTO()
  1122 + .setProcessDefinitionKey(processDefinitionKey).setBusinessKey("business-1"));
  1123 + }
  1124 +
  1125 + private void deploy(String resource) {
  1126 + extraDeploymentIds.add(repositoryService.createDeployment().addClasspathResource(resource).deploy().getId());
  1127 + }
  1128 +
  1129 + private ProcessInstance getChildProcessInstance(String parentId) {
  1130 + ProcessInstance child = runtimeService.createProcessInstanceQuery().superProcessInstanceId(parentId).singleResult();
  1131 + assertNotNull(child);
  1132 + return child;
  1133 + }
  1134 +
  1135 + private HistoricTaskInstance getHistoricTask(String processInstanceId, String taskDefinitionKey) {
  1136 + return historyService.createHistoricTaskInstanceQuery().processInstanceId(processInstanceId)
  1137 + .taskDefinitionKey(taskDefinitionKey).singleResult();
  1138 + }
  1139 +
  1140 + /**
  1141 + * 将流程实例上唯一的定时器转为可执行任务并立即执行,模拟定时器到期
  1142 + */
  1143 + private void executeTimerJob(String processInstanceId) {
  1144 + Job timerJob = managementService.createTimerJobQuery().processInstanceId(processInstanceId).singleResult();
  1145 + assertNotNull(timerJob);
  1146 + Job job = managementService.moveTimerToExecutableJob(timerJob.getId());
  1147 + managementService.executeJob(job.getId());
  1148 + }
  1149 +
  1150 + private BpmProcessInstanceStatusEvent getProcessInstanceStatusEvent(String processInstanceId) {
  1151 + List<BpmProcessInstanceStatusEvent> events = applicationEvents.stream(BpmProcessInstanceStatusEvent.class)
  1152 + .filter(event -> processInstanceId.equals(event.getId())).toList();
  1153 + assertEquals(1, events.size());
  1154 + return events.get(0);
  1155 + }
  1156 +
  1157 + private void mockUserTaskExtensionElement(String taskDefinitionKey, String name, Object value) {
  1158 + doAnswer(invocation -> {
  1159 + BpmnModel bpmnModel = repositoryService.getBpmnModel(invocation.getArgument(0));
  1160 + FlowElement element = bpmnModel.getFlowElement(taskDefinitionKey);
  1161 + if (element != null && !element.getExtensionElements().containsKey(name)) {
  1162 + BpmnModelUtils.addExtensionElement(element, name, String.valueOf(value));
  1163 + }
  1164 + return bpmnModel;
  1165 + }).when(modelService).getBpmnModelByDefinitionId(anyString());
  1166 + }
  1167 +
  1168 + private void mockBoundaryEventTimeoutHandlerType(String boundaryEventId, BpmUserTaskTimeoutHandlerTypeEnum handlerType) {
  1169 + doAnswer(invocation -> {
  1170 + BpmnModel bpmnModel = repositoryService.getBpmnModel(invocation.getArgument(0));
  1171 + FlowElement element = bpmnModel.getFlowElement(boundaryEventId);
  1172 + if (element != null) {
  1173 + element.getExtensionElements().get(BpmnModelConstants.USER_TASK_TIMEOUT_HANDLER_TYPE).get(0)
  1174 + .setElementText(String.valueOf(handlerType.getType()));
  1175 + }
  1176 + return bpmnModel;
  1177 + }).when(modelService).getBpmnModelByDefinitionId(anyString());
  1178 + }
  1179 +
  1180 + /**
  1181 + * 与 {@link cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker#calculateUsersByTask} 一致:非人工审核(自动通过、不通过)的节点不计算审批人
  1182 + */
  1183 + private void mockTaskWithoutCandidate(String taskDefinitionKey) {
  1184 + when(taskCandidateInvoker.calculateUsersByTask(any())).thenAnswer(invocation ->
  1185 + taskDefinitionKey.equals(((DelegateExecution) invocation.getArgument(0)).getCurrentActivityId())
  1186 + ? Set.of() : Set.of(1L));
  1187 + }
  1188 +
  1189 + private void mockAutoApprovalType(BpmAutoApproveTypeEnum autoApproveType) {
  1190 + doAnswer(invocation -> new BpmProcessDefinitionInfoDO().setProcessDefinitionId(invocation.getArgument(0))
  1191 + .setModelType(BpmModelTypeEnum.BPMN.getType()).setAutoApprovalType(autoApproveType.getType()))
  1192 + .when(processDefinitionService).getProcessDefinitionInfo(anyString());
  1193 + }
  1194 +
  1195 + private Task getRunningTask(String processInstanceId) {
  1196 + List<Task> tasks = bpmTaskService.getRunningTaskListByProcessInstanceId(processInstanceId, null, null);
  1197 + assertEquals(1, tasks.size());
  1198 + return tasks.get(0);
  1199 + }
  1200 +
  1201 + private void approveRunningTask(String processInstanceId) {
  1202 + Task task = getRunningTask(processInstanceId);
  1203 + bpmTaskService.approveTask(1L, new BpmTaskApproveReqVO().setId(task.getId()).setReason("同意"));
  1204 + }
  1205 +
  1206 + private Integer getTaskStatus(String taskId) {
  1207 + return (Integer) bpmTaskService.getHistoricTask(taskId).getTaskLocalVariables()
  1208 + .get(BpmnVariableConstants.TASK_VARIABLE_STATUS);
  1209 + }
  1210 +
  1211 + private void assertTaskStatus(String taskId, Integer status, String reason) {
  1212 + HistoricTaskInstance task = bpmTaskService.getHistoricTask(taskId);
  1213 + assertEquals(status, task.getTaskLocalVariables().get(BpmnVariableConstants.TASK_VARIABLE_STATUS));
  1214 + assertEquals(reason, task.getTaskLocalVariables().get(BpmnVariableConstants.TASK_VARIABLE_REASON));
  1215 + }
  1216 +
  1217 + private Object getHistoricProcessVariable(String processInstanceId, String name) {
  1218 + return processInstanceService.getHistoricProcessInstance(processInstanceId).getProcessVariables().get(name);
  1219 + }
  1220 +
  1221 +}
... ...
yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/BpmUserTaskActivityBehaviorFlowableTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.bpm.framework.flowable;
  2 +
  3 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior.BpmUserTaskActivityBehavior;
  4 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
  5 +import jakarta.annotation.Resource;
  6 +import org.flowable.engine.RepositoryService;
  7 +import org.flowable.engine.RuntimeService;
  8 +import org.flowable.engine.TaskService;
  9 +import org.flowable.engine.runtime.ProcessInstance;
  10 +import org.flowable.task.api.Task;
  11 +import org.junit.jupiter.api.AfterEach;
  12 +import org.junit.jupiter.api.BeforeEach;
  13 +import org.junit.jupiter.api.Test;
  14 +
  15 +import java.util.Map;
  16 +import java.util.Set;
  17 +
  18 +import static org.junit.jupiter.api.Assertions.*;
  19 +import static org.mockito.ArgumentMatchers.any;
  20 +import static org.mockito.Mockito.when;
  21 +
  22 +/**
  23 + * {@link BpmUserTaskActivityBehavior} 的 Flowable 集成测试
  24 + *
  25 + * @author HUIHUI
  26 + */
  27 +public class BpmUserTaskActivityBehaviorFlowableTest extends BaseFlowableUnitTest {
  28 +
  29 + @Resource
  30 + private RepositoryService repositoryService;
  31 + @Resource
  32 + private RuntimeService runtimeService;
  33 + @Resource
  34 + private TaskService taskService;
  35 +
  36 + private String deploymentId;
  37 +
  38 + @BeforeEach
  39 + public void setUp() {
  40 + // 简单审批流程:BPMN 中的 assignee 表达式为 ${startUserId}
  41 + deploymentId = repositoryService.createDeployment()
  42 + .addClasspathResource("bpmn/simple-approve.bpmn20.xml")
  43 + .deploy().getId();
  44 + }
  45 +
  46 + @AfterEach
  47 + public void tearDown() {
  48 + repositoryService.deleteDeployment(deploymentId, true);
  49 + }
  50 +
  51 + @Test
  52 + public void testHandleAssignments_success() {
  53 + // 准备参数:发起人为用户 1,审批人计算结果为用户 2
  54 + when(taskCandidateInvoker.calculateUsersByTask(any())).thenReturn(Set.of(2L));
  55 +
  56 + // 调用
  57 + String processInstanceId = startProcessInstance();
  58 + // 断言:审批人以 BpmTaskCandidateInvoker 的计算结果为准,而不是 BPMN 中的 assignee 表达式
  59 + Task task = taskService.createTaskQuery().processInstanceId(processInstanceId).singleResult();
  60 + assertEquals("审批", task.getName());
  61 + assertEquals("2", task.getAssignee());
  62 + }
  63 +
  64 + @Test
  65 + public void testHandleAssignments_candidateEmpty() {
  66 + // 准备参数:计算不到审批人
  67 + when(taskCandidateInvoker.calculateUsersByTask(any())).thenReturn(Set.of());
  68 +
  69 + // 调用
  70 + String processInstanceId = startProcessInstance();
  71 + // 断言:任务仍然创建,但没有审批人(后续由审批人为空的策略处理)
  72 + Task task = taskService.createTaskQuery().processInstanceId(processInstanceId).singleResult();
  73 + assertNotNull(task);
  74 + assertNull(task.getAssignee());
  75 + }
  76 +
  77 + private String startProcessInstance() {
  78 + // 模拟 FlowableWebFilter 设置发起人,与生产发起流程的上下文一致
  79 + ProcessInstance processInstance = FlowableUtils.executeAuthenticatedUserId(1L,
  80 + () -> runtimeService.startProcessInstanceByKey("simpleApprove", "business-1",
  81 + Map.of("startUserId", 1L)));
  82 + return processInstance.getId();
  83 + }
  84 +
  85 +}
... ...
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 {
49 49  
50 50 @Mock
51 51 private AdminUserApi adminUserApi;
52   -
53 52 @Mock
54 53 private BpmProcessInstanceService processInstanceService;
55 54  
56 55 @Spy
57 56 private BpmTaskCandidateStrategy userStrategy;
  57 +
58 58 @Mock
59 59 private BpmTaskCandidateAssignEmptyStrategy emptyStrategy;
60 60  
... ...
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 {
30 30 private AdminUserApi adminUserApi;
31 31 @Mock
32 32 private DeptApi deptApi;
33   -
34 33 @Mock
35 34 private BpmProcessInstanceService processInstanceService;
36 35  
... ...
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
33 33  
34 34 @Mock
35 35 private BpmProcessInstanceService processInstanceService;
36   -
37 36 @Mock
38 37 private AdminUserApi adminUserApi;
39 38 @Mock
... ...
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
33 33  
34 34 @Mock
35 35 private BpmProcessInstanceService processInstanceService;
36   -
37 36 @Mock
38 37 private AdminUserApi adminUserApi;
39 38 @Mock
... ...
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
3 3 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
4 4 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
5 5 import org.flowable.engine.delegate.DelegateExecution;
6   -import org.junit.jupiter.api.Disabled;
7 6 import org.junit.jupiter.api.Test;
8 7 import org.mockito.InjectMocks;
9 8 import org.mockito.MockedStatic;
... ... @@ -17,7 +16,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
17 16 import static org.mockito.ArgumentMatchers.eq;
18 17 import static org.mockito.Mockito.*;
19 18  
20   -@Disabled // TODO 芋艿:临时注释
21 19 public class BpmTaskCandidateExpressionStrategyTest extends BaseMockitoUnitTest {
22 20  
23 21 @InjectMocks
... ...
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
3 3 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
4 4 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmUserGroupDO;
5 5 import cn.iocoder.yudao.module.bpm.service.definition.BpmUserGroupService;
6   -import org.junit.jupiter.api.Disabled;
7 6 import org.junit.jupiter.api.Test;
8 7 import org.mockito.InjectMocks;
9 8 import org.mockito.Mock;
... ... @@ -17,7 +16,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
17 16 import static org.mockito.ArgumentMatchers.eq;
18 17 import static org.mockito.Mockito.when;
19 18  
20   -@Disabled // TODO 芋艿:临时注释
21 19 public class BpmTaskCandidateGroupStrategyTest extends BaseMockitoUnitTest {
22 20  
23 21 @InjectMocks
... ...
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;
4 4 import cn.iocoder.yudao.module.system.api.dept.PostApi;
5 5 import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
6 6 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
7   -import org.junit.jupiter.api.Disabled;
8 7 import org.junit.jupiter.api.Test;
9 8 import org.mockito.InjectMocks;
10 9 import org.mockito.Mock;
... ... @@ -18,7 +17,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
18 17 import static org.mockito.ArgumentMatchers.eq;
19 18 import static org.mockito.Mockito.when;
20 19  
21   -@Disabled // TODO 芋艿:临时注释
22 20 public class BpmTaskCandidatePostStrategyTest extends BaseMockitoUnitTest {
23 21  
24 22 @InjectMocks
... ...
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
3 3 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
4 4 import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
5 5 import cn.iocoder.yudao.module.system.api.permission.RoleApi;
6   -import org.junit.jupiter.api.Disabled;
7 6 import org.junit.jupiter.api.Test;
8 7 import org.mockito.InjectMocks;
9 8 import org.mockito.Mock;
... ... @@ -15,7 +14,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
15 14 import static org.mockito.ArgumentMatchers.eq;
16 15 import static org.mockito.Mockito.when;
17 16  
18   -@Disabled // TODO 芋艿:临时注释
19 17 public class BpmTaskCandidateRoleStrategyTest extends BaseMockitoUnitTest {
20 18  
21 19 @InjectMocks
... ...
yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/user/BpmTaskCandidateUserStrategyTest.java
1 1 package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user;
2 2  
3 3 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
4   -import org.junit.jupiter.api.Disabled;
5 4 import org.junit.jupiter.api.Test;
6 5 import org.mockito.InjectMocks;
7 6  
... ... @@ -10,7 +9,6 @@ import java.util.Set;
10 9 import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet;
11 10 import static org.junit.jupiter.api.Assertions.assertEquals;
12 11  
13   -@Disabled // TODO 芋艿:临时注释
14 12 public class BpmTaskCandidateUserStrategyTest extends BaseMockitoUnitTest {
15 13  
16 14 @InjectMocks
... ... @@ -27,5 +25,4 @@ public class BpmTaskCandidateUserStrategyTest extends BaseMockitoUnitTest {
27 25 assertEquals(asSet(1L, 2L), userIds);
28 26 }
29 27  
30   -
31 28 }
... ...
yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListenerTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.bpm.framework.flowable.core.listener;
  2 +
  3 +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
  4 +import cn.iocoder.yudao.module.bpm.enums.definition.BpmBoundaryEventTypeEnum;
  5 +import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskTimeoutHandlerTypeEnum;
  6 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants;
  7 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
  8 +import cn.iocoder.yudao.module.bpm.service.definition.BpmModelService;
  9 +import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService;
  10 +import org.flowable.bpmn.model.BoundaryEvent;
  11 +import org.flowable.bpmn.model.BpmnModel;
  12 +import org.flowable.bpmn.model.Process;
  13 +import org.flowable.common.engine.api.delegate.event.FlowableEngineEntityEvent;
  14 +import org.flowable.job.api.Job;
  15 +import org.junit.jupiter.api.Test;
  16 +import org.mockito.InjectMocks;
  17 +import org.mockito.Mock;
  18 +
  19 +import static org.mockito.Mockito.*;
  20 +
  21 +/**
  22 + * {@link BpmTaskEventListener} 的单元测试
  23 + *
  24 + * @author 芋道源码
  25 + */
  26 +public class BpmTaskEventListenerTest extends BaseMockitoUnitTest {
  27 +
  28 + @InjectMocks
  29 + private BpmTaskEventListener taskEventListener;
  30 +
  31 + @Mock
  32 + private BpmModelService modelService;
  33 + @Mock
  34 + private BpmTaskService taskService;
  35 +
  36 + @Test
  37 + public void testTimerFired_elementIdFromJobHandlerConfiguration() {
  38 + // mock 流程模型:task 上挂载【超时提醒】的边界事件
  39 + BoundaryEvent boundaryEvent = new BoundaryEvent();
  40 + boundaryEvent.setId("timeout-boundary");
  41 + boundaryEvent.setAttachedToRefId("task");
  42 + BpmnModelUtils.addExtensionElement(boundaryEvent, BpmnModelConstants.BOUNDARY_EVENT_TYPE,
  43 + BpmBoundaryEventTypeEnum.USER_TASK_TIMEOUT.getType());
  44 + BpmnModelUtils.addExtensionElement(boundaryEvent, BpmnModelConstants.USER_TASK_TIMEOUT_HANDLER_TYPE,
  45 + BpmUserTaskTimeoutHandlerTypeEnum.REMINDER.getType());
  46 + Process process = new Process();
  47 + process.addFlowElement(boundaryEvent);
  48 + BpmnModel bpmnModel = new BpmnModel();
  49 + bpmnModel.addProcess(process);
  50 + when(modelService.getBpmnModelByDefinitionId("definition-1")).thenReturn(bpmnModel);
  51 + // 准备参数:部分 Flowable Job 的 elementId 为空,需要从 handler configuration 解析 activityId
  52 + Job job = mock(Job.class);
  53 + when(job.getJobHandlerConfiguration()).thenReturn("{\"activityId\":\"timeout-boundary\"}");
  54 + FlowableEngineEntityEvent event = mock(FlowableEngineEntityEvent.class);
  55 + when(event.getProcessDefinitionId()).thenReturn("definition-1");
  56 + when(event.getProcessInstanceId()).thenReturn("process-instance-1");
  57 + when(event.getEntity()).thenReturn(job);
  58 +
  59 + // 调用
  60 + taskEventListener.timerFired(event);
  61 +
  62 + // 断言:使用解析出的 activityId 查找边界事件,并触发用户任务超时处理
  63 + verify(taskService).processTaskTimeout("process-instance-1", "task",
  64 + BpmUserTaskTimeoutHandlerTypeEnum.REMINDER.getType());
  65 + }
  66 +
  67 +}
... ...
yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmHttpRequestUtilsTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.bpm.framework.flowable.core.util;
  2 +
  3 +import cn.hutool.extra.spring.SpringUtil;
  4 +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
  5 +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
  6 +import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent;
  7 +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
  8 +import org.flowable.engine.runtime.ProcessInstance;
  9 +import org.junit.jupiter.api.AfterEach;
  10 +import org.junit.jupiter.api.Test;
  11 +import org.mockito.ArgumentCaptor;
  12 +import org.mockito.Mock;
  13 +import org.mockito.MockedStatic;
  14 +import org.springframework.http.HttpEntity;
  15 +import org.springframework.http.ResponseEntity;
  16 +import org.springframework.web.client.RestTemplate;
  17 +
  18 +import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
  19 +import static org.junit.jupiter.api.Assertions.assertEquals;
  20 +import static org.mockito.ArgumentMatchers.*;
  21 +import static org.mockito.Mockito.*;
  22 +
  23 +/**
  24 + * {@link BpmHttpRequestUtils} 的单元测试
  25 + *
  26 + * @author HUIHUI
  27 + */
  28 +public class BpmHttpRequestUtilsTest extends BaseMockitoUnitTest {
  29 +
  30 + @Mock
  31 + private BpmProcessInstanceService processInstanceService;
  32 + @Mock
  33 + private ProcessInstance processInstance;
  34 + @Mock
  35 + private RestTemplate restTemplate;
  36 +
  37 + @AfterEach
  38 + public void tearDown() {
  39 + TenantContextHolder.clear();
  40 + }
  41 +
  42 + @Test
  43 + public void testExecuteBpmHttpRequest_statusEventTenantFromProcessInstance() {
  44 + // 准备参数:事务完成后的回调可能已经没有租户上下文,但流程实例仍带有租户编号
  45 + BpmProcessInstanceStatusEvent event = new BpmProcessInstanceStatusEvent(this);
  46 + event.setId("process-instance");
  47 + when(processInstanceService.getProcessInstance("process-instance")).thenReturn(processInstance);
  48 + when(processInstance.getTenantId()).thenReturn("tenant-1");
  49 + when(restTemplate.exchange(anyString(), any(), any(), eq(String.class))).thenReturn(ResponseEntity.ok("{}"));
  50 +
  51 + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) {
  52 + springUtilMockedStatic.when(() -> SpringUtil.getBean(BpmProcessInstanceService.class))
  53 + .thenReturn(processInstanceService);
  54 + springUtilMockedStatic.when(() -> SpringUtil.getBean(RestTemplate.class)).thenReturn(restTemplate);
  55 +
  56 + // 调用
  57 + BpmHttpRequestUtils.executeBpmHttpRequest(event, "http://callback");
  58 + }
  59 + // 断言:请求头使用流程实例租户,而不是已清空的上下文租户
  60 + ArgumentCaptor<HttpEntity<Object>> requestCaptor = ArgumentCaptor.forClass(HttpEntity.class);
  61 + verify(restTemplate).exchange(eq("http://callback"), any(), requestCaptor.capture(), eq(String.class));
  62 + assertEquals("tenant-1", requestCaptor.getValue().getHeaders().getFirst(HEADER_TENANT_ID));
  63 + }
  64 +
  65 +}
... ...
yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtilsTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.bpm.framework.flowable.core.util;
  2 +
  3 +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
  4 +import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskRejectHandlerTypeEnum;
  5 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
  6 +import org.flowable.bpmn.model.UserTask;
  7 +import org.junit.jupiter.api.Test;
  8 +
  9 +import java.util.Collections;
  10 +
  11 +import static org.junit.jupiter.api.Assertions.*;
  12 +
  13 +/**
  14 + * {@link BpmnModelUtils} 的单元测试
  15 + *
  16 + * @author HUIHUI
  17 + */
  18 +public class BpmnModelUtilsTest {
  19 +
  20 + @Test
  21 + public void testCandidateElements_success() {
  22 + // 准备参数
  23 + UserTask userTask = new UserTask();
  24 +
  25 + // 调用
  26 + BpmnModelUtils.addCandidateElements(BpmTaskCandidateStrategyEnum.USER.getStrategy(), "1,2", userTask);
  27 + // 断言
  28 + assertEquals(BpmTaskCandidateStrategyEnum.USER.getStrategy(),
  29 + BpmnModelUtils.parseCandidateStrategy(userTask));
  30 + assertEquals("1,2", BpmnModelUtils.parseCandidateParam(userTask));
  31 + }
  32 +
  33 + @Test
  34 + public void testCandidateElements_empty() {
  35 + // 准备参数
  36 + UserTask userTask = new UserTask();
  37 +
  38 + // 调用
  39 + BpmnModelUtils.addCandidateElements(null, null, userTask);
  40 + // 断言
  41 + assertNull(BpmnModelUtils.parseCandidateStrategy(userTask));
  42 + assertNull(BpmnModelUtils.parseCandidateParam(userTask));
  43 + }
  44 +
  45 + @Test
  46 + public void testRejectElements_success() {
  47 + // 准备参数
  48 + UserTask userTask = new UserTask();
  49 + BpmSimpleModelNodeVO.RejectHandler rejectHandler = new BpmSimpleModelNodeVO.RejectHandler()
  50 + .setType(BpmUserTaskRejectHandlerTypeEnum.RETURN_USER_TASK.getType())
  51 + .setReturnNodeId("approve");
  52 +
  53 + // 调用
  54 + BpmnModelUtils.addTaskRejectElements(rejectHandler, userTask);
  55 + // 断言
  56 + assertEquals(BpmUserTaskRejectHandlerTypeEnum.RETURN_USER_TASK,
  57 + BpmnModelUtils.parseRejectHandlerType(userTask));
  58 + assertEquals("approve", BpmnModelUtils.parseReturnTaskId(userTask));
  59 + }
  60 +
  61 + @Test
  62 + public void testExtensionElement_emptyValue_notAdd() {
  63 + // 准备参数
  64 + UserTask userTask = new UserTask();
  65 +
  66 + // 调用
  67 + BpmnModelUtils.addExtensionElement(userTask, "remark", (String) null);
  68 + BpmnModelUtils.addExtensionElementJson(userTask, "config", null);
  69 + BpmnModelUtils.addExtensionElement(userTask, "attributes", Collections.emptyMap());
  70 + // 断言
  71 + assertNull(BpmnModelUtils.parseExtensionElement(userTask, "remark"));
  72 + assertNull(BpmnModelUtils.parseExtensionElement(userTask, "config"));
  73 + }
  74 +
  75 +}
... ...
yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtilsTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.bpm.framework.flowable.core.util;
  2 +
  3 +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
  4 +import cn.iocoder.yudao.module.bpm.enums.definition.BpmChildProcessStartUserEmptyTypeEnum;
  5 +import cn.iocoder.yudao.module.bpm.enums.definition.BpmChildProcessStartUserTypeEnum;
  6 +import cn.iocoder.yudao.module.bpm.enums.definition.BpmBoundaryEventTypeEnum;
  7 +import cn.iocoder.yudao.module.bpm.enums.definition.BpmConditionOpCodeEnum;
  8 +import cn.iocoder.yudao.module.bpm.enums.definition.BpmDelayTimerTypeEnum;
  9 +import cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModeConditionTypeEnum;
  10 +import cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeTypeEnum;
  11 +import cn.iocoder.yudao.module.bpm.enums.definition.BpmTriggerTypeEnum;
  12 +import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskApproveMethodEnum;
  13 +import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskApproveTypeEnum;
  14 +import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskTimeoutHandlerTypeEnum;
  15 +import cn.iocoder.yudao.module.bpm.framework.flowable.BaseFlowableUnitTest;
  16 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
  17 +import org.flowable.bpmn.model.BoundaryEvent;
  18 +import org.flowable.bpmn.model.BpmnModel;
  19 +import org.flowable.bpmn.model.CallActivity;
  20 +import org.flowable.bpmn.model.EndEvent;
  21 +import org.flowable.bpmn.model.ExclusiveGateway;
  22 +import org.flowable.bpmn.model.FlowElement;
  23 +import org.flowable.bpmn.model.Process;
  24 +import org.flowable.bpmn.model.ReceiveTask;
  25 +import org.flowable.bpmn.model.SequenceFlow;
  26 +import org.flowable.bpmn.model.ServiceTask;
  27 +import org.flowable.bpmn.model.TimerEventDefinition;
  28 +import org.flowable.bpmn.model.UserTask;
  29 +import org.junit.jupiter.api.Test;
  30 +
  31 +import java.util.Arrays;
  32 +import java.util.Collections;
  33 +import java.util.HashMap;
  34 +import java.util.List;
  35 +import java.util.Map;
  36 +
  37 +import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.BOUNDARY_EVENT_TYPE;
  38 +import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.START_EVENT_NODE_ID;
  39 +import static org.junit.jupiter.api.Assertions.*;
  40 +
  41 +/**
  42 + * {@link SimpleModelUtils} 的单元测试
  43 + *
  44 + * @author HUIHUI
  45 + */
  46 +public class SimpleModelUtilsTest extends BaseFlowableUnitTest {
  47 +
  48 + @Test
  49 + public void testBuildBpmnModel_startUserApproveEnd() {
  50 + // 准备参数
  51 + BpmSimpleModelNodeVO approveNode = node("approve", BpmSimpleModelNodeTypeEnum.APPROVE_NODE)
  52 + .setName("审批").setApproveType(BpmUserTaskApproveTypeEnum.USER.getType())
  53 + .setApproveMethod(BpmUserTaskApproveMethodEnum.RANDOM.getMethod())
  54 + .setCandidateStrategy(BpmTaskCandidateStrategyEnum.USER.getStrategy()).setCandidateParam("1,2");
  55 + BpmSimpleModelNodeVO startUserNode = node("start-user", BpmSimpleModelNodeTypeEnum.START_USER_NODE)
  56 + .setName("发起人").setChildNode(approveNode);
  57 + approveNode.setChildNode(node("end", BpmSimpleModelNodeTypeEnum.END_NODE).setName("结束"));
  58 +
  59 + // 调用
  60 + BpmnModel bpmnModel = SimpleModelUtils.buildBpmnModel("process", "测试流程", startUserNode);
  61 + Process process = bpmnModel.getMainProcess();
  62 + // 断言
  63 + assertEquals("process", process.getId());
  64 + assertEquals("测试流程", process.getName());
  65 + assertNotNull(process.getFlowElement(START_EVENT_NODE_ID));
  66 + assertInstanceOf(UserTask.class, process.getFlowElement("start-user"));
  67 + UserTask approveTask = (UserTask) process.getFlowElement("approve");
  68 + assertEquals(BpmTaskCandidateStrategyEnum.USER.getStrategy(), BpmnModelUtils.parseCandidateStrategy(approveTask));
  69 + assertEquals("1,2", BpmnModelUtils.parseCandidateParam(approveTask));
  70 + assertInstanceOf(EndEvent.class, process.getFlowElement("end"));
  71 + assertTrue(process.getFlowElements().stream().anyMatch(item -> item instanceof SequenceFlow
  72 + && "approve".equals(((SequenceFlow) item).getSourceRef())
  73 + && "end".equals(((SequenceFlow) item).getTargetRef())));
  74 + }
  75 +
  76 + @Test
  77 + public void testBuildBpmnModel_approveConfig() {
  78 + // 准备参数
  79 + BpmSimpleModelNodeVO approveNode = node("approve", BpmSimpleModelNodeTypeEnum.APPROVE_NODE)
  80 + .setApproveType(BpmUserTaskApproveTypeEnum.USER.getType())
  81 + .setApproveMethod(BpmUserTaskApproveMethodEnum.RATIO.getMethod()).setApproveRatio(60)
  82 + .setCandidateStrategy(BpmTaskCandidateStrategyEnum.USER.getStrategy()).setCandidateParam("1")
  83 + .setTimeoutHandler(new BpmSimpleModelNodeVO.TimeoutHandler()
  84 + .setEnable(true).setType(BpmUserTaskTimeoutHandlerTypeEnum.REMINDER.getType())
  85 + .setTimeDuration("PT1H").setMaxRemindCount(3));
  86 +
  87 + // 调用
  88 + List<FlowElement> flowElements = new ApproveNodeFlowBuilder().convert(approveNode);
  89 + // 断言
  90 + UserTask userTask = (UserTask) flowElements.get(0);
  91 + assertEquals("${ nrOfCompletedInstances/nrOfInstances >= 0.60}",
  92 + userTask.getLoopCharacteristics().getCompletionCondition());
  93 + assertEquals(BpmTaskCandidateStrategyEnum.USER.getStrategy(), BpmnModelUtils.parseCandidateStrategy(userTask));
  94 + BoundaryEvent boundaryEvent = (BoundaryEvent) flowElements.get(1);
  95 + TimerEventDefinition timer = (TimerEventDefinition) boundaryEvent.getEventDefinitions().get(0);
  96 + assertEquals("PT1H", timer.getTimeDuration());
  97 + assertEquals("R3/PT1H", timer.getTimeCycle());
  98 + }
  99 +
  100 + @Test
  101 + public void testDelayTimerNodeConvert_durationAndDate() {
  102 + // 准备参数
  103 + BpmSimpleModelNodeVO durationNode = node("delay-duration", BpmSimpleModelNodeTypeEnum.DELAY_TIMER_NODE)
  104 + .setDelaySetting(new BpmSimpleModelNodeVO.DelaySetting()
  105 + .setDelayType(BpmDelayTimerTypeEnum.FIXED_TIME_DURATION.getType()).setDelayTime("PT2H"));
  106 + BpmSimpleModelNodeVO dateNode = node("delay-date", BpmSimpleModelNodeTypeEnum.DELAY_TIMER_NODE)
  107 + .setDelaySetting(new BpmSimpleModelNodeVO.DelaySetting()
  108 + .setDelayType(BpmDelayTimerTypeEnum.FIXED_DATE_TIME.getType())
  109 + .setDelayTime("2026-10-06T12:00:00"));
  110 +
  111 + // 调用
  112 + BoundaryEvent durationEvent = (BoundaryEvent) new SimpleModelUtils.DelayTimerNodeConvert()
  113 + .convertList(durationNode).get(1);
  114 + BoundaryEvent dateEvent = (BoundaryEvent) new SimpleModelUtils.DelayTimerNodeConvert()
  115 + .convertList(dateNode).get(1);
  116 + // 断言
  117 + TimerEventDefinition durationTimer = (TimerEventDefinition) durationEvent.getEventDefinitions().get(0);
  118 + assertEquals("PT2H", durationTimer.getTimeDuration());
  119 + assertNull(durationTimer.getTimeDate());
  120 + TimerEventDefinition dateTimer = (TimerEventDefinition) dateEvent.getEventDefinitions().get(0);
  121 + assertEquals("2026-10-06T12:00:00", dateTimer.getTimeDate());
  122 + assertNull(dateTimer.getTimeDuration());
  123 + }
  124 +
  125 + @Test
  126 + public void testBuildBpmnModel_childProcessTimeout() {
  127 + // 准备参数
  128 + BpmSimpleModelNodeVO.ChildProcessSetting setting = new BpmSimpleModelNodeVO.ChildProcessSetting()
  129 + .setCalledProcessDefinitionKey("child-process").setCalledProcessDefinitionName("子流程")
  130 + .setAsync(false).setSkipStartUserNode(true)
  131 + .setStartUserSetting(new BpmSimpleModelNodeVO.ChildProcessSetting.StartUserSetting()
  132 + .setType(BpmChildProcessStartUserTypeEnum.MAIN_PROCESS_START_USER.getType())
  133 + .setEmptyType(BpmChildProcessStartUserEmptyTypeEnum.MAIN_PROCESS_START_USER.getType()))
  134 + .setTimeoutSetting(new BpmSimpleModelNodeVO.ChildProcessSetting.TimeoutSetting()
  135 + .setEnable(true).setType(BpmDelayTimerTypeEnum.FIXED_DATE_TIME.getType())
  136 + .setTimeExpression("2026-10-06T12:00:00"));
  137 + BpmSimpleModelNodeVO childNode = node("child", BpmSimpleModelNodeTypeEnum.CHILD_PROCESS)
  138 + .setChildProcessSetting(setting).setChildNode(node("end", BpmSimpleModelNodeTypeEnum.END_NODE));
  139 +
  140 + // 调用
  141 + BpmnModel bpmnModel = SimpleModelUtils.buildBpmnModel("process", "测试流程", childNode);
  142 + Process process = bpmnModel.getMainProcess();
  143 + // 断言
  144 + assertInstanceOf(CallActivity.class, process.getFlowElement("child"));
  145 + BoundaryEvent boundaryEvent = process.getFlowElements().stream().filter(item -> item instanceof BoundaryEvent)
  146 + .map(item -> (BoundaryEvent) item).findFirst().orElseThrow();
  147 + TimerEventDefinition timer = (TimerEventDefinition) boundaryEvent.getEventDefinitions().get(0);
  148 + assertEquals("2026-10-06T12:00:00", timer.getTimeDate());
  149 + assertNull(timer.getTimeDuration());
  150 + assertEquals(String.valueOf(BpmBoundaryEventTypeEnum.CHILD_PROCESS_TIMEOUT.getType()),
  151 + BpmnModelUtils.parseBoundaryEventExtensionElement(boundaryEvent, BOUNDARY_EVENT_TYPE));
  152 + }
  153 +
  154 + @Test
  155 + public void testBuildBpmnModel_childProcessTimeoutDuration() {
  156 + // 准备参数
  157 + BpmSimpleModelNodeVO.ChildProcessSetting setting = new BpmSimpleModelNodeVO.ChildProcessSetting()
  158 + .setCalledProcessDefinitionKey("child-process").setCalledProcessDefinitionName("子流程")
  159 + .setAsync(false).setSkipStartUserNode(true)
  160 + .setStartUserSetting(new BpmSimpleModelNodeVO.ChildProcessSetting.StartUserSetting()
  161 + .setType(BpmChildProcessStartUserTypeEnum.MAIN_PROCESS_START_USER.getType())
  162 + .setEmptyType(BpmChildProcessStartUserEmptyTypeEnum.MAIN_PROCESS_START_USER.getType()))
  163 + .setTimeoutSetting(new BpmSimpleModelNodeVO.ChildProcessSetting.TimeoutSetting()
  164 + .setEnable(true).setType(BpmDelayTimerTypeEnum.FIXED_TIME_DURATION.getType())
  165 + .setTimeExpression("PT30M"));
  166 + BpmSimpleModelNodeVO childNode = node("child", BpmSimpleModelNodeTypeEnum.CHILD_PROCESS)
  167 + .setChildProcessSetting(setting).setChildNode(node("end", BpmSimpleModelNodeTypeEnum.END_NODE));
  168 +
  169 + // 调用
  170 + BpmnModel bpmnModel = SimpleModelUtils.buildBpmnModel("process", "测试流程", childNode);
  171 + Process process = bpmnModel.getMainProcess();
  172 + // 断言:固定时长使用 timeDuration,且不会误设置 timeDate、timeCycle
  173 + BoundaryEvent boundaryEvent = process.getFlowElements().stream().filter(item -> item instanceof BoundaryEvent)
  174 + .map(item -> (BoundaryEvent) item).findFirst().orElseThrow();
  175 + assertEquals("child", boundaryEvent.getAttachedToRef().getId());
  176 + assertFalse(boundaryEvent.isCancelActivity());
  177 + TimerEventDefinition timer = (TimerEventDefinition) boundaryEvent.getEventDefinitions().get(0);
  178 + assertEquals("PT30M", timer.getTimeDuration());
  179 + assertNull(timer.getTimeDate());
  180 + assertNull(timer.getTimeCycle());
  181 + // 断言:边界事件类型为子流程超时,而不是延迟器超时
  182 + assertEquals(String.valueOf(BpmBoundaryEventTypeEnum.CHILD_PROCESS_TIMEOUT.getType()),
  183 + BpmnModelUtils.parseBoundaryEventExtensionElement(boundaryEvent, BOUNDARY_EVENT_TYPE));
  184 + }
  185 +
  186 + @Test
  187 + public void testBuildBpmnModel_conditionBranch() {
  188 + // 准备参数
  189 + BpmSimpleModelNodeVO branch = node("branch", BpmSimpleModelNodeTypeEnum.CONDITION_BRANCH_NODE);
  190 + BpmSimpleModelNodeVO condition = node("condition", BpmSimpleModelNodeTypeEnum.CONDITION_NODE)
  191 + .setName("金额大于 10").setConditionSetting(new BpmSimpleModelNodeVO.ConditionSetting()
  192 + .setConditionType(BpmSimpleModeConditionTypeEnum.EXPRESSION.getType())
  193 + .setConditionExpression("${amount > 10}").setDefaultFlow(false))
  194 + .setChildNode(node("approve", BpmSimpleModelNodeTypeEnum.APPROVE_NODE)
  195 + .setApproveType(BpmUserTaskApproveTypeEnum.AUTO_APPROVE.getType()));
  196 + BpmSimpleModelNodeVO defaultCondition = node("default-condition", BpmSimpleModelNodeTypeEnum.CONDITION_NODE)
  197 + .setName("默认").setConditionSetting(new BpmSimpleModelNodeVO.ConditionSetting()
  198 + .setConditionType(BpmSimpleModeConditionTypeEnum.EXPRESSION.getType())
  199 + .setDefaultFlow(true))
  200 + .setChildNode(node("reject", BpmSimpleModelNodeTypeEnum.APPROVE_NODE)
  201 + .setApproveType(BpmUserTaskApproveTypeEnum.AUTO_REJECT.getType()));
  202 + branch.setConditionNodes(Arrays.asList(condition, defaultCondition))
  203 + .setChildNode(node("end", BpmSimpleModelNodeTypeEnum.END_NODE));
  204 +
  205 + // 调用
  206 + Process process = SimpleModelUtils.buildBpmnModel("process", "测试流程", branch).getMainProcess();
  207 + // 断言
  208 + ExclusiveGateway gateway = (ExclusiveGateway) process.getFlowElement("branch");
  209 + assertEquals("default-condition", gateway.getDefaultFlow());
  210 + SequenceFlow conditionFlow = (SequenceFlow) process.getFlowElement("condition");
  211 + assertEquals("${amount > 10}", conditionFlow.getConditionExpression());
  212 + assertEquals("approve", conditionFlow.getTargetRef());
  213 + }
  214 +
  215 + @Test
  216 + public void testTriggerNodeConvert_httpCallback() {
  217 + // 准备参数
  218 + BpmSimpleModelNodeVO.TriggerSetting.HttpRequestTriggerSetting httpSetting =
  219 + new BpmSimpleModelNodeVO.TriggerSetting.HttpRequestTriggerSetting().setUrl("https://example.com/callback");
  220 + BpmSimpleModelNodeVO node = node("trigger", BpmSimpleModelNodeTypeEnum.TRIGGER_NODE)
  221 + .setTriggerSetting(new BpmSimpleModelNodeVO.TriggerSetting()
  222 + .setType(BpmTriggerTypeEnum.HTTP_CALLBACK.getType())
  223 + .setHttpRequestSetting(httpSetting));
  224 +
  225 + // 调用
  226 + List<? extends FlowElement> flowElements = new SimpleModelUtils.TriggerNodeConvert().convertList(node);
  227 + // 断言
  228 + assertEquals(2, flowElements.size());
  229 + assertInstanceOf(ReceiveTask.class, flowElements.get(0));
  230 + assertInstanceOf(ServiceTask.class, flowElements.get(1));
  231 + assertNotNull(httpSetting.getCallbackTaskDefineKey());
  232 + assertEquals(httpSetting.getCallbackTaskDefineKey(), flowElements.get(0).getId());
  233 + }
  234 +
  235 + @Test
  236 + public void testBuildConditionExpression_rule() {
  237 + // 准备参数
  238 + BpmSimpleModelNodeVO.ConditionRule numberRule = new BpmSimpleModelNodeVO.ConditionRule()
  239 + .setOpCode(BpmConditionOpCodeEnum.GT.getCode()).setLeftSide("amount").setRightSide("10");
  240 + BpmSimpleModelNodeVO.ConditionRule stringRule = new BpmSimpleModelNodeVO.ConditionRule()
  241 + .setOpCode(BpmConditionOpCodeEnum.EQ.getCode()).setLeftSide("status").setRightSide("PASS");
  242 + BpmSimpleModelNodeVO.Condition condition = new BpmSimpleModelNodeVO.Condition()
  243 + .setAnd(true).setRules(Arrays.asList(numberRule, stringRule));
  244 + BpmSimpleModelNodeVO.ConditionGroups groups = new BpmSimpleModelNodeVO.ConditionGroups()
  245 + .setAnd(false).setConditions(Collections.singletonList(condition));
  246 + BpmSimpleModelNodeVO.ConditionSetting setting = new BpmSimpleModelNodeVO.ConditionSetting()
  247 + .setConditionType(BpmSimpleModeConditionTypeEnum.RULE.getType()).setConditionGroups(groups);
  248 +
  249 + // 调用
  250 + String expression = SimpleModelUtils.buildConditionExpression(setting);
  251 + // 断言
  252 + assertEquals("${( var:getOrDefault(amount, null) > 10 && var:getOrDefault(status, null) == \"PASS\" )}",
  253 + expression);
  254 + }
  255 +
  256 + @Test
  257 + public void testBuildConditionExpression_expressionAndEmptyRule() {
  258 + // 准备参数
  259 + BpmSimpleModelNodeVO.ConditionSetting expressionSetting = new BpmSimpleModelNodeVO.ConditionSetting()
  260 + .setConditionType(BpmSimpleModeConditionTypeEnum.EXPRESSION.getType())
  261 + .setConditionExpression("${amount > 1}");
  262 + BpmSimpleModelNodeVO.ConditionSetting emptyRuleSetting = new BpmSimpleModelNodeVO.ConditionSetting()
  263 + .setConditionType(BpmSimpleModeConditionTypeEnum.RULE.getType())
  264 + .setConditionGroups(new BpmSimpleModelNodeVO.ConditionGroups().setAnd(true)
  265 + .setConditions(Collections.emptyList()));
  266 +
  267 + // 调用,并断言
  268 + assertEquals("${amount > 1}", SimpleModelUtils.buildConditionExpression(expressionSetting));
  269 + assertNull(SimpleModelUtils.buildConditionExpression(emptyRuleSetting));
  270 + }
  271 +
  272 + @Test
  273 + public void testSimulateProcess_conditionBranches() {
  274 + // 准备参数
  275 + BpmSimpleModelNodeVO branch = node("branch", BpmSimpleModelNodeTypeEnum.CONDITION_BRANCH_NODE);
  276 + BpmSimpleModelNodeVO matched = node("matched", BpmSimpleModelNodeTypeEnum.APPROVE_NODE)
  277 + .setApproveType(BpmUserTaskApproveTypeEnum.AUTO_APPROVE.getType())
  278 + .setConditionSetting(new BpmSimpleModelNodeVO.ConditionSetting()
  279 + .setConditionType(BpmSimpleModeConditionTypeEnum.EXPRESSION.getType())
  280 + .setConditionExpression("${amount > 10}").setDefaultFlow(false));
  281 + BpmSimpleModelNodeVO defaultNode = node("default", BpmSimpleModelNodeTypeEnum.APPROVE_NODE)
  282 + .setApproveType(BpmUserTaskApproveTypeEnum.AUTO_REJECT.getType())
  283 + .setConditionSetting(new BpmSimpleModelNodeVO.ConditionSetting()
  284 + .setConditionType(BpmSimpleModeConditionTypeEnum.EXPRESSION.getType())
  285 + .setDefaultFlow(true));
  286 + matched.setChildNode(node("matched-end", BpmSimpleModelNodeTypeEnum.END_NODE));
  287 + defaultNode.setChildNode(node("default-end", BpmSimpleModelNodeTypeEnum.END_NODE));
  288 + branch.setConditionNodes(Arrays.asList(
  289 + node("condition-1", BpmSimpleModelNodeTypeEnum.CONDITION_NODE).setConditionSetting(matched.getConditionSetting())
  290 + .setChildNode(matched),
  291 + node("condition-2", BpmSimpleModelNodeTypeEnum.CONDITION_NODE).setConditionSetting(defaultNode.getConditionSetting())
  292 + .setChildNode(defaultNode)));
  293 +
  294 + // 调用
  295 + List<BpmSimpleModelNodeVO> result = SimpleModelUtils.simulateProcess(branch,
  296 + Collections.singletonMap("amount", 20));
  297 + // 断言
  298 + assertEquals(Arrays.asList("matched", "matched-end"), result.stream()
  299 + .map(BpmSimpleModelNodeVO::getId).toList());
  300 + }
  301 +
  302 + @Test
  303 + public void testSimulateProcess_inclusiveAndParallelBranches() {
  304 + // 准备参数
  305 + BpmSimpleModelNodeVO inclusive = node("inclusive", BpmSimpleModelNodeTypeEnum.INCLUSIVE_BRANCH_NODE);
  306 + BpmSimpleModelNodeVO first = node("first", BpmSimpleModelNodeTypeEnum.APPROVE_NODE)
  307 + .setApproveType(BpmUserTaskApproveTypeEnum.AUTO_APPROVE.getType());
  308 + BpmSimpleModelNodeVO second = node("second", BpmSimpleModelNodeTypeEnum.COPY_NODE);
  309 + first.setChildNode(node("first-end", BpmSimpleModelNodeTypeEnum.END_NODE));
  310 + second.setChildNode(node("second-end", BpmSimpleModelNodeTypeEnum.END_NODE));
  311 + inclusive.setConditionNodes(Arrays.asList(
  312 + node("condition-1", BpmSimpleModelNodeTypeEnum.CONDITION_NODE)
  313 + .setConditionSetting(new BpmSimpleModelNodeVO.ConditionSetting()
  314 + .setConditionType(BpmSimpleModeConditionTypeEnum.EXPRESSION.getType())
  315 + .setConditionExpression("${left}").setDefaultFlow(false)).setChildNode(first),
  316 + node("condition-2", BpmSimpleModelNodeTypeEnum.CONDITION_NODE)
  317 + .setConditionSetting(new BpmSimpleModelNodeVO.ConditionSetting()
  318 + .setConditionType(BpmSimpleModeConditionTypeEnum.EXPRESSION.getType())
  319 + .setConditionExpression("${right}").setDefaultFlow(false)).setChildNode(second)));
  320 + Map<String, Object> variables = new HashMap<>();
  321 + variables.put("left", true);
  322 + variables.put("right", true);
  323 +
  324 + // 调用
  325 + List<BpmSimpleModelNodeVO> result = SimpleModelUtils.simulateProcess(inclusive, variables);
  326 + // 断言
  327 + assertEquals(Arrays.asList("first", "first-end", "second", "second-end"), result.stream()
  328 + .map(BpmSimpleModelNodeVO::getId).toList());
  329 + }
  330 +
  331 + @Test
  332 + public void testNodeHelpers() {
  333 + // 准备参数
  334 + BpmSimpleModelNodeVO sequential = node("approve", BpmSimpleModelNodeTypeEnum.APPROVE_NODE)
  335 + .setApproveMethod(BpmUserTaskApproveMethodEnum.SEQUENTIAL.getMethod());
  336 + BpmSimpleModelNodeVO skipNode = node("skip", BpmSimpleModelNodeTypeEnum.APPROVE_NODE)
  337 + .setSkipExpression("${skip}");
  338 +
  339 + // 调用,并断言
  340 + assertTrue(SimpleModelUtils.isValidNode(sequential));
  341 + assertFalse(SimpleModelUtils.isValidNode(new BpmSimpleModelNodeVO()));
  342 + assertTrue(SimpleModelUtils.isSequentialApproveNode(sequential));
  343 + assertFalse(SimpleModelUtils.isSequentialApproveNode(node("other", BpmSimpleModelNodeTypeEnum.COPY_NODE)
  344 + .setApproveMethod(BpmUserTaskApproveMethodEnum.SEQUENTIAL.getMethod())));
  345 + assertTrue(SimpleModelUtils.isSkipNode(skipNode, Collections.singletonMap("skip", true)));
  346 + assertFalse(SimpleModelUtils.isSkipNode(node("without-skip", BpmSimpleModelNodeTypeEnum.APPROVE_NODE),
  347 + Collections.emptyMap()));
  348 + }
  349 +
  350 + private static BpmSimpleModelNodeVO node(String id, BpmSimpleModelNodeTypeEnum type) {
  351 + return new BpmSimpleModelNodeVO().setId(id).setType(type.getType());
  352 + }
  353 +
  354 + /**
  355 + * 通过受保护转换器验证审批节点配置,不暴露生产实现细节。
  356 + */
  357 + private static class ApproveNodeFlowBuilder {
  358 + List<FlowElement> convert(BpmSimpleModelNodeVO node) {
  359 + BpmnModel model = SimpleModelUtils.buildBpmnModel("test", "test",
  360 + node.setChildNode(node("end", BpmSimpleModelNodeTypeEnum.END_NODE)));
  361 + Process process = model.getMainProcess();
  362 + return Arrays.asList(process.getFlowElement("approve"), process.getFlowElements().stream()
  363 + .filter(item -> item instanceof BoundaryEvent).map(item -> (BoundaryEvent) item)
  364 + .findFirst().orElseThrow());
  365 + }
  366 + }
  367 +}
... ...
yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/category/BpmCategoryServiceImplTest.java
... ... @@ -138,4 +138,4 @@ public class BpmCategoryServiceImplTest extends BaseDbUnitTest {
138 138 assertPojoEquals(dbCategory, pageResult.getList().get(0));
139 139 }
140 140  
141   -}
142 141 \ No newline at end of file
  142 +}
... ...
yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/comment/BpmCommentServiceImplTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.bpm.service.comment;
  2 +
  3 +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
  4 +import cn.iocoder.yudao.module.bpm.controller.admin.comment.vo.BpmCommentCreateReqVO;
  5 +import cn.iocoder.yudao.module.bpm.enums.task.BpmCommentTypeEnum;
  6 +import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService;
  7 +import org.flowable.engine.TaskService;
  8 +import org.flowable.task.api.Task;
  9 +import org.junit.jupiter.api.Test;
  10 +import org.mockito.InjectMocks;
  11 +import org.mockito.Mock;
  12 +
  13 +import static org.mockito.ArgumentMatchers.eq;
  14 +import static org.mockito.Mockito.verify;
  15 +import static org.mockito.Mockito.when;
  16 +
  17 +/**
  18 + * {@link BpmCommentServiceImpl} 的单元测试
  19 + *
  20 + * @author HUIHUI
  21 + */
  22 +public class BpmCommentServiceImplTest extends BaseMockitoUnitTest {
  23 +
  24 + @InjectMocks
  25 + private BpmCommentServiceImpl commentService;
  26 +
  27 + @Mock
  28 + private TaskService taskService;
  29 + @Mock
  30 + private BpmTaskService bpmTaskService;
  31 + @Mock
  32 + private Task task;
  33 +
  34 + @Test
  35 + public void testGetCommentListByProcessInstanceId_success() {
  36 + // 准备参数
  37 + String processInstanceId = "process-instance-1";
  38 +
  39 + // 调用
  40 + commentService.getCommentListByProcessInstanceId(processInstanceId);
  41 + // 断言
  42 + verify(taskService).getProcessInstanceComments(processInstanceId);
  43 + }
  44 +
  45 + @Test
  46 + public void testCreateComment_success() {
  47 + // 准备参数
  48 + BpmCommentCreateReqVO reqVO = new BpmCommentCreateReqVO()
  49 + .setTaskId("task-1").setMessage("请补充附件");
  50 + when(bpmTaskService.validateTaskExists(reqVO.getTaskId())).thenReturn(task);
  51 + when(task.getId()).thenReturn(reqVO.getTaskId());
  52 + when(task.getProcessInstanceId()).thenReturn("process-instance-1");
  53 +
  54 + // 调用
  55 + commentService.createComment(reqVO);
  56 + // 断言
  57 + verify(taskService).addComment(eq("task-1"), eq("process-instance-1"),
  58 + eq(BpmCommentTypeEnum.COMMENT.getType()), eq("请补充附件"));
  59 + }
  60 +
  61 + @Test
  62 + public void testCreateComment_withType_success() {
  63 + // 准备参数
  64 + String taskId = "task-1";
  65 + String processInstanceId = "process-instance-1";
  66 +
  67 + // 调用
  68 + commentService.createComment(taskId, processInstanceId, BpmCommentTypeEnum.APPROVE, "同意");
  69 + // 断言
  70 + verify(taskService).addComment(taskId, processInstanceId, BpmCommentTypeEnum.APPROVE.getType(), "审批通过,原因是:同意");
  71 + }
  72 +
  73 +}
... ...
yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/definition/BpmFormServiceTest.java
... ... @@ -35,7 +35,6 @@ public class BpmFormServiceTest extends BaseDbUnitTest {
35 35  
36 36 @Resource
37 37 private BpmFormServiceImpl formService;
38   -
39 38 @Resource
40 39 private BpmFormMapper formMapper;
41 40  
... ...
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;
7 7 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
8 8 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelSaveReqVO;
9 9 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
  10 +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelUpdateReqVO;
10 11 import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum;
11 12 import cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeTypeEnum;
12 13 import org.flowable.engine.RepositoryService;
... ... @@ -22,8 +23,11 @@ import org.mockito.Mock;
22 23 import java.util.Arrays;
23 24 import java.util.Collections;
24 25 import java.util.Date;
  26 +import java.util.List;
25 27  
26 28 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
  29 +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.MODEL_KEY_EXISTS;
  30 +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.MODEL_KEY_VALID;
27 31 import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.MODEL_NOT_EXISTS;
28 32 import static org.junit.jupiter.api.Assertions.*;
29 33 import static org.mockito.AdditionalMatchers.aryEq;
... ... @@ -47,6 +51,8 @@ public class BpmModelServiceImplTest extends BaseMockitoUnitTest {
47 51 private RepositoryService repositoryService;
48 52 @Mock
49 53 private ModelQuery modelQuery;
  54 + @Mock
  55 + private BpmProcessDefinitionService processDefinitionService;
50 56  
51 57 @BeforeEach
52 58 public void setUp() {
... ... @@ -153,6 +159,163 @@ public class BpmModelServiceImplTest extends BaseMockitoUnitTest {
153 159 assertEquals(Collections.singletonList(40L), metaInfo.getManagerUserIds());
154 160 }
155 161  
  162 + @Test
  163 + public void testCreateModel_simple() {
  164 + // 准备参数
  165 + BpmSimpleModelNodeVO simpleModel = simpleModel();
  166 + BpmModelSaveReqVO reqVO = new BpmModelSaveReqVO();
  167 + reqVO.setKey("simple_process");
  168 + reqVO.setName("简单流程");
  169 + reqVO.setCategory("OA");
  170 + reqVO.setType(BpmModelTypeEnum.SIMPLE.getType());
  171 + reqVO.setSimpleModel(simpleModel);
  172 + Model model = mock(Model.class);
  173 + when(model.getId()).thenReturn(MODEL_ID);
  174 + when(model.getKey()).thenReturn(reqVO.getKey());
  175 + when(model.getName()).thenReturn(reqVO.getName());
  176 + mockModelKeyNotExists(reqVO.getKey());
  177 + when(repositoryService.newModel()).thenReturn(model);
  178 +
  179 + // 调用
  180 + String result = modelService.createModel(reqVO);
  181 +
  182 + // 断言
  183 + assertEquals(MODEL_ID, result);
  184 + verify(repositoryService).saveModel(same(model));
  185 + verify(repositoryService).addModelEditorSource(eq(MODEL_ID), any(byte[].class));
  186 + verify(repositoryService).addModelEditorSourceExtra(eq(MODEL_ID), aryEq(JsonUtils.toJsonByte(simpleModel)));
  187 + }
  188 +
  189 + @Test
  190 + public void testCreateModel_invalidKey() {
  191 + // 准备参数
  192 + BpmModelSaveReqVO reqVO = new BpmModelSaveReqVO().setKey("bad key");
  193 +
  194 + // 调用,并断言异常
  195 + assertServiceException(() -> modelService.createModel(reqVO), MODEL_KEY_VALID);
  196 + verifyNoInteractions(repositoryService);
  197 + }
  198 +
  199 + @Test
  200 + public void testCreateModel_duplicateKey() {
  201 + // 准备参数
  202 + BpmModelSaveReqVO reqVO = new BpmModelSaveReqVO().setKey("duplicate_process");
  203 + mockModelKeyExists(reqVO.getKey());
  204 +
  205 + // 调用,并断言异常
  206 + assertServiceException(() -> modelService.createModel(reqVO), MODEL_KEY_EXISTS, reqVO.getKey());
  207 + verify(repositoryService, never()).newModel();
  208 + }
  209 +
  210 + @Test
  211 + public void testGetModelList_nameFilter() {
  212 + // 准备参数
  213 + Model model = mock(Model.class);
  214 + when(repositoryService.createModelQuery()).thenReturn(modelQuery);
  215 + when(modelQuery.modelNameLike(eq("%请假%"))).thenReturn(modelQuery);
  216 + when(modelQuery.modelTenantId(eq(TENANT_ID.toString()))).thenReturn(modelQuery);
  217 + when(modelQuery.list()).thenReturn(Collections.singletonList(model));
  218 +
  219 + // 调用
  220 + List<Model> result = modelService.getModelList("请假");
  221 +
  222 + // 断言
  223 + assertEquals(Collections.singletonList(model), result);
  224 + verify(modelQuery).modelNameLike(eq("%请假%"));
  225 + }
  226 +
  227 + @Test
  228 + public void testGetSimpleModel_success() {
  229 + // 准备参数
  230 + Model model = mock(Model.class);
  231 + BpmSimpleModelNodeVO simpleModel = simpleModel();
  232 + when(model.getId()).thenReturn(MODEL_ID);
  233 + mockGetModel(model);
  234 + when(repositoryService.getModelEditorSourceExtra(eq(MODEL_ID))).thenReturn(JsonUtils.toJsonByte(simpleModel));
  235 +
  236 + // 调用
  237 + BpmSimpleModelNodeVO result = modelService.getSimpleModel(MODEL_ID);
  238 +
  239 + // 断言
  240 + assertEquals(simpleModel.getId(), result.getId());
  241 + assertEquals(simpleModel.getType(), result.getType());
  242 + }
  243 +
  244 + @Test
  245 + public void testUpdateSimpleModel_success() {
  246 + // 准备参数
  247 + Model model = mock(Model.class);
  248 + when(model.getId()).thenReturn(MODEL_ID);
  249 + when(model.getKey()).thenReturn("simple_process");
  250 + when(model.getName()).thenReturn("简单流程");
  251 + when(model.getCreateTime()).thenReturn(new Date(1_000L));
  252 + BpmModelMetaInfoVO metaInfo = new BpmModelMetaInfoVO();
  253 + metaInfo.setType(BpmModelTypeEnum.SIMPLE.getType());
  254 + metaInfo.setManagerUserIds(Collections.singletonList(10L));
  255 + when(model.getMetaInfo()).thenReturn(JsonUtils.toJsonString(metaInfo));
  256 + BpmSimpleModelNodeVO simpleModel = simpleModel();
  257 + BpmSimpleModelUpdateReqVO reqVO = new BpmSimpleModelUpdateReqVO().setId(MODEL_ID).setSimpleModel(simpleModel);
  258 + mockGetModel(model);
  259 +
  260 + // 调用
  261 + modelService.updateSimpleModel(10L, reqVO);
  262 +
  263 + // 断言
  264 + verify(repositoryService).addModelEditorSource(eq(MODEL_ID), any(byte[].class));
  265 + verify(repositoryService).addModelEditorSourceExtra(eq(MODEL_ID), aryEq(JsonUtils.toJsonByte(simpleModel)));
  266 + }
  267 +
  268 + @Test
  269 + public void testUpdateModelSortBatch_success() {
  270 + // 准备参数
  271 + List<String> ids = Arrays.asList("model-1", "model-2", "model-3");
  272 + Model model1 = mockSortableModel("model-1");
  273 + Model model2 = mockSortableModel("model-2");
  274 + Model model3 = mockSortableModel("model-3");
  275 + ModelQuery listQuery = mock(ModelQuery.class);
  276 + ModelQuery managerQuery = mock(ModelQuery.class);
  277 + when(repositoryService.createModelQuery()).thenReturn(listQuery, managerQuery, managerQuery, managerQuery);
  278 + when(listQuery.modelTenantId(eq(TENANT_ID.toString()))).thenReturn(listQuery);
  279 + when(listQuery.list()).thenReturn(Arrays.asList(model1, model2, model3));
  280 + when(managerQuery.modelId(anyString())).thenReturn(managerQuery);
  281 + when(managerQuery.modelTenantId(eq(TENANT_ID.toString()))).thenReturn(managerQuery);
  282 + when(managerQuery.singleResult()).thenReturn(model1, model2, model3);
  283 +
  284 + // 调用
  285 + modelService.updateModelSortBatch(1L, ids);
  286 +
  287 + // 断言:所有模型(包括第一个)都更新排序,且按 ids 顺序递增(列表按 sort 升序展示)
  288 + verify(repositoryService, times(3)).saveModel(any(Model.class));
  289 + ArgumentCaptor<String> modelIdCaptor = ArgumentCaptor.forClass(String.class);
  290 + ArgumentCaptor<Long> sortCaptor = ArgumentCaptor.forClass(Long.class);
  291 + verify(processDefinitionService, times(3))
  292 + .updateProcessDefinitionSortByModelId(modelIdCaptor.capture(), sortCaptor.capture());
  293 + assertEquals(Arrays.asList("model-3", "model-2", "model-1"), modelIdCaptor.getAllValues());
  294 + List<Long> sorts = sortCaptor.getAllValues();
  295 + assertTrue(sorts.get(0) > sorts.get(1) && sorts.get(1) > sorts.get(2));
  296 + }
  297 +
  298 + private void mockModelKeyNotExists(String key) {
  299 + when(repositoryService.createModelQuery()).thenReturn(modelQuery);
  300 + when(modelQuery.modelTenantId(eq(TENANT_ID.toString()))).thenReturn(modelQuery);
  301 + when(modelQuery.modelKey(eq(key))).thenReturn(modelQuery);
  302 + when(modelQuery.singleResult()).thenReturn(null);
  303 + }
  304 +
  305 + private void mockModelKeyExists(String key) {
  306 + when(repositoryService.createModelQuery()).thenReturn(modelQuery);
  307 + when(modelQuery.modelTenantId(eq(TENANT_ID.toString()))).thenReturn(modelQuery);
  308 + when(modelQuery.modelKey(eq(key))).thenReturn(modelQuery);
  309 + when(modelQuery.singleResult()).thenReturn(mock(Model.class));
  310 + }
  311 +
  312 + private BpmSimpleModelNodeVO simpleModel() {
  313 + return new BpmSimpleModelNodeVO().setId("start-user")
  314 + .setType(BpmSimpleModelNodeTypeEnum.START_USER_NODE.getType())
  315 + .setChildNode(new BpmSimpleModelNodeVO().setId("end")
  316 + .setType(BpmSimpleModelNodeTypeEnum.END_NODE.getType()));
  317 + }
  318 +
156 319 private Model mockModel(Integer type) {
157 320 Model model = mock(Model.class);
158 321 when(model.getKey()).thenReturn("test_process");
... ... @@ -165,6 +328,16 @@ public class BpmModelServiceImplTest extends BaseMockitoUnitTest {
165 328 return model;
166 329 }
167 330  
  331 + private Model mockSortableModel(String id) {
  332 + Model model = mock(Model.class);
  333 + when(model.getId()).thenReturn(id);
  334 + when(model.getCreateTime()).thenReturn(new Date(1_000L));
  335 + BpmModelMetaInfoVO metaInfo = new BpmModelMetaInfoVO();
  336 + metaInfo.setManagerUserIds(Collections.singletonList(1L));
  337 + when(model.getMetaInfo()).thenReturn(JsonUtils.toJsonString(metaInfo));
  338 + return model;
  339 + }
  340 +
168 341 private void mockGetModel(Model model) {
169 342 when(repositoryService.createModelQuery()).thenReturn(modelQuery);
170 343 when(modelQuery.modelId(eq(MODEL_ID))).thenReturn(modelQuery);
... ...
yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImplTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.bpm.service.definition;
  2 +
  3 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  4 +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
  5 +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionPageReqVO;
  6 +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
  7 +import cn.iocoder.yudao.module.bpm.dal.mysql.definition.BpmProcessDefinitionInfoMapper;
  8 +import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
  9 +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
  10 +import org.flowable.common.engine.impl.db.SuspensionState;
  11 +import org.flowable.engine.RepositoryService;
  12 +import org.flowable.engine.repository.Deployment;
  13 +import org.flowable.engine.repository.DeploymentQuery;
  14 +import org.flowable.engine.repository.ProcessDefinition;
  15 +import org.flowable.engine.repository.ProcessDefinitionQuery;
  16 +import org.junit.jupiter.api.Test;
  17 +import org.mockito.InjectMocks;
  18 +import org.mockito.Mock;
  19 +
  20 +import java.util.Collections;
  21 +import java.util.List;
  22 +import java.util.Set;
  23 +
  24 +import static org.junit.jupiter.api.Assertions.*;
  25 +import static org.mockito.ArgumentMatchers.anyString;
  26 +import static org.mockito.Mockito.*;
  27 +
  28 +/**
  29 + * {@link BpmProcessDefinitionServiceImpl} 的单元测试
  30 + *
  31 + * @author HUIHUI
  32 + */
  33 +public class BpmProcessDefinitionServiceImplTest extends BaseMockitoUnitTest {
  34 +
  35 + @InjectMocks
  36 + private BpmProcessDefinitionServiceImpl processDefinitionService;
  37 +
  38 + @Mock
  39 + private RepositoryService repositoryService;
  40 + @Mock
  41 + private BpmProcessDefinitionInfoMapper processDefinitionMapper;
  42 + @Mock
  43 + private AdminUserApi adminUserApi;
  44 + @Mock
  45 + private ProcessDefinitionQuery processDefinitionQuery;
  46 + @Mock
  47 + private DeploymentQuery deploymentQuery;
  48 +
  49 + @Test
  50 + public void testCanUserStartProcessDefinition_null() {
  51 + // 调用,并断言
  52 + assertFalse(processDefinitionService.canUserStartProcessDefinition(null, 1L));
  53 + }
  54 +
  55 + @Test
  56 + public void testCanUserStartProcessDefinition_startUserIds() {
  57 + // 准备参数
  58 + BpmProcessDefinitionInfoDO definition = new BpmProcessDefinitionInfoDO().setStartUserIds(List.of(1L, 2L));
  59 +
  60 + // 调用,并断言
  61 + assertTrue(processDefinitionService.canUserStartProcessDefinition(definition, 2L));
  62 + assertFalse(processDefinitionService.canUserStartProcessDefinition(definition, 3L));
  63 + }
  64 +
  65 + @Test
  66 + public void testCanUserStartProcessDefinition_startDeptIdsMatch() {
  67 + // 准备参数
  68 + BpmProcessDefinitionInfoDO definition = new BpmProcessDefinitionInfoDO().setStartDeptIds(List.of(10L));
  69 + when(adminUserApi.getUser(1L)).thenReturn(new AdminUserRespDTO().setDeptId(10L));
  70 +
  71 + // 调用,并断言
  72 + assertTrue(processDefinitionService.canUserStartProcessDefinition(definition, 1L));
  73 + }
  74 +
  75 + @Test
  76 + public void testCanUserStartProcessDefinition_startDeptIdsNotMatch() {
  77 + // 准备参数
  78 + BpmProcessDefinitionInfoDO definition = new BpmProcessDefinitionInfoDO().setStartDeptIds(List.of(10L));
  79 + when(adminUserApi.getUser(1L)).thenReturn(new AdminUserRespDTO().setDeptId(20L));
  80 +
  81 + // 调用,并断言
  82 + assertFalse(processDefinitionService.canUserStartProcessDefinition(definition, 1L));
  83 + }
  84 +
  85 + @Test
  86 + public void testCanUserStartProcessDefinition_noLimit() {
  87 + // 调用,并断言
  88 + assertTrue(processDefinitionService.canUserStartProcessDefinition(new BpmProcessDefinitionInfoDO(), 1L));
  89 + }
  90 +
  91 + @Test
  92 + public void testGetProcessDefinitionByDeploymentId_empty() {
  93 + // 调用,并断言
  94 + assertNull(processDefinitionService.getProcessDefinitionByDeploymentId(null));
  95 + assertNull(processDefinitionService.getProcessDefinitionByDeploymentId(""));
  96 + }
  97 +
  98 + @Test
  99 + public void testGetProcessDefinitionListByDeploymentIds_empty() {
  100 + // 调用,并断言
  101 + assertTrue(processDefinitionService.getProcessDefinitionListByDeploymentIds(Collections.emptySet()).isEmpty());
  102 + }
  103 +
  104 + @Test
  105 + public void testGetActiveProcessDefinition_success() {
  106 + // 准备参数
  107 + ProcessDefinition definition = mock(ProcessDefinition.class);
  108 + when(repositoryService.createProcessDefinitionQuery()).thenReturn(processDefinitionQuery);
  109 + when(processDefinitionQuery.processDefinitionTenantId(anyString())).thenReturn(processDefinitionQuery);
  110 + when(processDefinitionQuery.processDefinitionKey("leave")).thenReturn(processDefinitionQuery);
  111 + when(processDefinitionQuery.active()).thenReturn(processDefinitionQuery);
  112 + when(processDefinitionQuery.singleResult()).thenReturn(definition);
  113 +
  114 + // 调用,并断言
  115 + assertSame(definition, processDefinitionService.getActiveProcessDefinition("leave"));
  116 + }
  117 +
  118 + @Test
  119 + public void testGetDeploymentList_filterNotExists() {
  120 + // 准备参数
  121 + Deployment deployment = mock(Deployment.class);
  122 + when(repositoryService.createDeploymentQuery()).thenReturn(deploymentQuery);
  123 + when(deploymentQuery.deploymentId(anyString())).thenReturn(deploymentQuery);
  124 + when(deploymentQuery.singleResult()).thenReturn(deployment, null);
  125 +
  126 + // 调用,并断言
  127 + assertEquals(Collections.singletonList(deployment),
  128 + processDefinitionService.getDeploymentList(Set.of("deployment-1", "deployment-2")));
  129 + }
  130 +
  131 + @Test
  132 + public void testUpdateProcessDefinitionState_activeAndSuspended() {
  133 + // 准备参数
  134 + ProcessDefinition definition = mock(ProcessDefinition.class);
  135 + when(repositoryService.getProcessDefinition("definition-1")).thenReturn(definition);
  136 +
  137 + // 调用:挂起状态恢复为激活
  138 + when(definition.isSuspended()).thenReturn(true);
  139 + processDefinitionService.updateProcessDefinitionState("definition-1", SuspensionState.ACTIVE.getStateCode());
  140 + verify(repositoryService).activateProcessDefinitionById("definition-1", false, null);
  141 +
  142 + // 调用:激活状态挂起
  143 + when(definition.isSuspended()).thenReturn(false);
  144 + processDefinitionService.updateProcessDefinitionState("definition-1", SuspensionState.SUSPENDED.getStateCode());
  145 + verify(repositoryService).suspendProcessDefinitionById("definition-1", false, null);
  146 + }
  147 +
  148 + @Test
  149 + public void testGetProcessDefinitionPage_empty() {
  150 + // 准备参数
  151 + BpmProcessDefinitionPageReqVO pageVO = new BpmProcessDefinitionPageReqVO();
  152 + when(repositoryService.createProcessDefinitionQuery()).thenReturn(processDefinitionQuery);
  153 + when(processDefinitionQuery.processDefinitionTenantId(anyString())).thenReturn(processDefinitionQuery);
  154 + when(processDefinitionQuery.count()).thenReturn(0L);
  155 +
  156 + // 调用,并断言
  157 + PageResult<ProcessDefinition> result = processDefinitionService.getProcessDefinitionPage(pageVO);
  158 + assertEquals(0L, result.getTotal());
  159 + assertTrue(result.getList().isEmpty());
  160 + }
  161 +
  162 +}
... ...
yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessExpressionServiceImplTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.bpm.service.definition;
  2 +
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  4 +import cn.iocoder.yudao.framework.common.pojo.PageResult;
  5 +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
  6 +import cn.iocoder.yudao.framework.test.core.util.AssertUtils;
  7 +import cn.iocoder.yudao.framework.test.core.util.RandomUtils;
  8 +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.expression.BpmProcessExpressionPageReqVO;
  9 +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.expression.BpmProcessExpressionSaveReqVO;
  10 +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessExpressionDO;
  11 +import cn.iocoder.yudao.module.bpm.dal.mysql.definition.BpmProcessExpressionMapper;
  12 +import jakarta.annotation.Resource;
  13 +import org.junit.jupiter.api.Assertions;
  14 +import org.junit.jupiter.api.Test;
  15 +import org.springframework.context.annotation.Import;
  16 +
  17 +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime;
  18 +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildTime;
  19 +import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
  20 +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_EXPRESSION_NOT_EXISTS;
  21 +
  22 +/**
  23 + * {@link BpmProcessExpressionServiceImpl} 的单元测试类
  24 + *
  25 + * @author HUIHUI
  26 + */
  27 +@Import(BpmProcessExpressionServiceImpl.class)
  28 +public class BpmProcessExpressionServiceImplTest extends BaseDbUnitTest {
  29 +
  30 + @Resource
  31 + private BpmProcessExpressionServiceImpl processExpressionService;
  32 + @Resource
  33 + private BpmProcessExpressionMapper processExpressionMapper;
  34 +
  35 + @Test
  36 + public void testCreateProcessExpression_success() {
  37 + // 准备参数
  38 + BpmProcessExpressionSaveReqVO reqVO = RandomUtils.randomPojo(BpmProcessExpressionSaveReqVO.class)
  39 + .setId(null);
  40 +
  41 + // 调用
  42 + Long expressionId = processExpressionService.createProcessExpression(reqVO);
  43 + // 断言
  44 + Assertions.assertNotNull(expressionId);
  45 + AssertUtils.assertPojoEquals(reqVO, processExpressionMapper.selectById(expressionId), "id");
  46 + }
  47 +
  48 + @Test
  49 + public void testUpdateProcessExpression_success() {
  50 + // 准备参数
  51 + BpmProcessExpressionDO dbExpression = RandomUtils.randomPojo(BpmProcessExpressionDO.class);
  52 + processExpressionMapper.insert(dbExpression);
  53 + BpmProcessExpressionSaveReqVO reqVO = RandomUtils.randomPojo(BpmProcessExpressionSaveReqVO.class)
  54 + .setId(dbExpression.getId());
  55 +
  56 + // 调用
  57 + processExpressionService.updateProcessExpression(reqVO);
  58 + // 断言
  59 + AssertUtils.assertPojoEquals(reqVO, processExpressionMapper.selectById(dbExpression.getId()));
  60 + }
  61 +
  62 + @Test
  63 + public void testUpdateProcessExpression_notExists() {
  64 + // 准备参数
  65 + BpmProcessExpressionSaveReqVO reqVO = RandomUtils.randomPojo(BpmProcessExpressionSaveReqVO.class);
  66 +
  67 + // 调用,并断言异常
  68 + AssertUtils.assertServiceException(() -> processExpressionService.updateProcessExpression(reqVO),
  69 + PROCESS_EXPRESSION_NOT_EXISTS);
  70 + }
  71 +
  72 + @Test
  73 + public void testDeleteProcessExpression_success() {
  74 + // 准备参数
  75 + BpmProcessExpressionDO dbExpression = RandomUtils.randomPojo(BpmProcessExpressionDO.class);
  76 + processExpressionMapper.insert(dbExpression);
  77 +
  78 + // 调用
  79 + processExpressionService.deleteProcessExpression(dbExpression.getId());
  80 + // 断言
  81 + Assertions.assertNull(processExpressionMapper.selectById(dbExpression.getId()));
  82 + }
  83 +
  84 + @Test
  85 + public void testDeleteProcessExpression_notExists() {
  86 + // 准备参数
  87 + Long id = RandomUtils.randomLongId();
  88 +
  89 + // 调用,并断言异常
  90 + AssertUtils.assertServiceException(() -> processExpressionService.deleteProcessExpression(id),
  91 + PROCESS_EXPRESSION_NOT_EXISTS);
  92 + }
  93 +
  94 + @Test
  95 + public void testGetProcessExpressionPage() {
  96 + // 准备参数
  97 + BpmProcessExpressionDO dbExpression = RandomUtils.randomPojo(BpmProcessExpressionDO.class, o -> {
  98 + o.setName("请假审批");
  99 + o.setStatus(CommonStatusEnum.ENABLE.getStatus());
  100 + o.setCreateTime(buildTime(2023, 2, 2));
  101 + });
  102 + processExpressionMapper.insert(dbExpression);
  103 + processExpressionMapper.insert(cloneIgnoreId(dbExpression, o -> o.setName("报销审批")));
  104 + processExpressionMapper.insert(cloneIgnoreId(dbExpression, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus())));
  105 + processExpressionMapper.insert(cloneIgnoreId(dbExpression, o -> o.setCreateTime(buildTime(2024, 2, 2))));
  106 + BpmProcessExpressionPageReqVO reqVO = new BpmProcessExpressionPageReqVO();
  107 + reqVO.setName("请假");
  108 + reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
  109 + reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
  110 +
  111 + // 调用
  112 + PageResult<BpmProcessExpressionDO> pageResult = processExpressionService.getProcessExpressionPage(reqVO);
  113 + // 断言
  114 + Assertions.assertEquals(1, pageResult.getTotal());
  115 + AssertUtils.assertPojoEquals(dbExpression, pageResult.getList().get(0));
  116 + }
  117 +
  118 +}
... ...
yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessListenerServiceImplTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.bpm.service.definition;
  2 +
  3 +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
  4 +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
  5 +import cn.iocoder.yudao.framework.test.core.util.AssertUtils;
  6 +import cn.iocoder.yudao.framework.test.core.util.RandomUtils;
  7 +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.listener.BpmProcessListenerSaveReqVO;
  8 +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessListenerDO;
  9 +import cn.iocoder.yudao.module.bpm.dal.mysql.definition.BpmProcessListenerMapper;
  10 +import cn.iocoder.yudao.module.bpm.enums.definition.BpmProcessListenerTypeEnum;
  11 +import cn.iocoder.yudao.module.bpm.enums.definition.BpmProcessListenerValueTypeEnum;
  12 +import jakarta.annotation.Resource;
  13 +import org.junit.jupiter.api.Assertions;
  14 +import org.junit.jupiter.api.Test;
  15 +import org.springframework.context.annotation.Import;
  16 +
  17 +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_LISTENER_CLASS_IMPLEMENTS_ERROR;
  18 +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_LISTENER_CLASS_NOT_FOUND;
  19 +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_LISTENER_EXPRESSION_INVALID;
  20 +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_LISTENER_NOT_EXISTS;
  21 +
  22 +/**
  23 + * {@link BpmProcessListenerServiceImpl} 的单元测试类
  24 + *
  25 + * @author HUIHUI
  26 + */
  27 +@Import(BpmProcessListenerServiceImpl.class)
  28 +public class BpmProcessListenerServiceImplTest extends BaseDbUnitTest {
  29 +
  30 + @Resource
  31 + private BpmProcessListenerServiceImpl processListenerService;
  32 + @Resource
  33 + private BpmProcessListenerMapper processListenerMapper;
  34 +
  35 + @Test
  36 + public void testCreateProcessListener_class_success() {
  37 + // 准备参数
  38 + BpmProcessListenerSaveReqVO reqVO = randomRequest()
  39 + .setType(BpmProcessListenerTypeEnum.EXECUTION.getType())
  40 + .setValueType(BpmProcessListenerValueTypeEnum.CLASS.getType())
  41 + .setValue("cn.iocoder.yudao.module.bpm.framework.flowable.core.listener.demo.exection.DemoDelegateClassExecutionListener");
  42 +
  43 + // 调用
  44 + Long listenerId = processListenerService.createProcessListener(reqVO);
  45 + // 断言
  46 + Assertions.assertNotNull(listenerId);
  47 + AssertUtils.assertPojoEquals(reqVO, processListenerMapper.selectById(listenerId));
  48 + }
  49 +
  50 + @Test
  51 + public void testCreateProcessListener_expression_success() {
  52 + // 准备参数
  53 + BpmProcessListenerSaveReqVO reqVO = randomRequest()
  54 + .setValueType(BpmProcessListenerValueTypeEnum.EXPRESSION.getType()).setValue("${listener.handle}");
  55 +
  56 + // 调用
  57 + Long listenerId = processListenerService.createProcessListener(reqVO);
  58 + // 断言
  59 + Assertions.assertNotNull(listenerId);
  60 + }
  61 +
  62 + @Test
  63 + public void testCreateProcessListener_classNotFound() {
  64 + // 准备参数
  65 + BpmProcessListenerSaveReqVO reqVO = randomRequest().setValueType(BpmProcessListenerValueTypeEnum.CLASS.getType())
  66 + .setValue("cn.iocoder.yudao.module.bpm.NotExistsListener");
  67 +
  68 + // 调用,并断言异常
  69 + AssertUtils.assertServiceException(() -> processListenerService.createProcessListener(reqVO),
  70 + PROCESS_LISTENER_CLASS_NOT_FOUND, reqVO.getValue());
  71 + }
  72 +
  73 + @Test
  74 + public void testCreateProcessListener_classNotImplements() {
  75 + // 准备参数
  76 + BpmProcessListenerSaveReqVO reqVO = randomRequest().setValueType(BpmProcessListenerValueTypeEnum.CLASS.getType())
  77 + .setValue(String.class.getName());
  78 +
  79 + // 调用,并断言异常
  80 + AssertUtils.assertServiceException(() -> processListenerService.createProcessListener(reqVO),
  81 + PROCESS_LISTENER_CLASS_IMPLEMENTS_ERROR, reqVO.getValue(),
  82 + org.flowable.engine.delegate.TaskListener.class.getName());
  83 + }
  84 +
  85 + @Test
  86 + public void testCreateProcessListener_expressionInvalid() {
  87 + // 准备参数
  88 + BpmProcessListenerSaveReqVO reqVO = randomRequest().setValueType(BpmProcessListenerValueTypeEnum.EXPRESSION.getType())
  89 + .setValue("listener.handle");
  90 +
  91 + // 调用,并断言异常
  92 + AssertUtils.assertServiceException(() -> processListenerService.createProcessListener(reqVO),
  93 + PROCESS_LISTENER_EXPRESSION_INVALID, reqVO.getValue());
  94 + }
  95 +
  96 + @Test
  97 + public void testDeleteProcessListener_notExists() {
  98 + // 准备参数
  99 + Long id = RandomUtils.randomLongId();
  100 +
  101 + // 调用,并断言异常
  102 + AssertUtils.assertServiceException(() -> processListenerService.deleteProcessListener(id),
  103 + PROCESS_LISTENER_NOT_EXISTS);
  104 + }
  105 +
  106 + private static BpmProcessListenerSaveReqVO randomRequest() {
  107 + return RandomUtils.randomPojo(BpmProcessListenerSaveReqVO.class, o -> {
  108 + o.setStatus(CommonStatusEnum.ENABLE.getStatus());
  109 + o.setType(BpmProcessListenerTypeEnum.TASK.getType());
  110 + o.setEvent("create");
  111 + });
  112 + }
  113 +
  114 +}
... ...
yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/definition/BpmUserGroupServiceTest.java
... ... @@ -31,7 +31,6 @@ public class BpmUserGroupServiceTest extends BaseDbUnitTest {
31 31  
32 32 @Resource
33 33 private BpmUserGroupServiceImpl userGroupService;
34   -
35 34 @Resource
36 35 private BpmUserGroupMapper userGroupMapper;
37 36  
... ...
yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/message/BpmMessageServiceImplTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.bpm.service.message;
  2 +
  3 +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
  4 +import cn.iocoder.yudao.framework.web.config.WebProperties;
  5 +import cn.iocoder.yudao.module.bpm.enums.message.BpmMessageEnum;
  6 +import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceApproveReqDTO;
  7 +import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceRejectReqDTO;
  8 +import cn.iocoder.yudao.module.system.api.sms.SmsSendApi;
  9 +import cn.iocoder.yudao.module.system.api.sms.dto.send.SmsSendSingleToUserReqDTO;
  10 +import org.junit.jupiter.api.BeforeEach;
  11 +import org.junit.jupiter.api.Test;
  12 +import org.mockito.ArgumentCaptor;
  13 +import org.mockito.InjectMocks;
  14 +import org.mockito.Mock;
  15 +
  16 +import static org.junit.jupiter.api.Assertions.assertEquals;
  17 +import static org.mockito.Mockito.verify;
  18 +
  19 +/**
  20 + * {@link BpmMessageServiceImpl} 的单元测试
  21 + *
  22 + * @author HUIHUI
  23 + */
  24 +public class BpmMessageServiceImplTest extends BaseMockitoUnitTest {
  25 +
  26 + @InjectMocks
  27 + private BpmMessageServiceImpl messageService;
  28 +
  29 + @Mock
  30 + private SmsSendApi smsSendApi;
  31 + @Mock
  32 + private WebProperties webProperties;
  33 + @Mock
  34 + private WebProperties.Ui adminUi;
  35 +
  36 + @BeforeEach
  37 + public void setUp() {
  38 + org.mockito.Mockito.when(webProperties.getAdminUi()).thenReturn(adminUi);
  39 + org.mockito.Mockito.when(adminUi.getUrl()).thenReturn("http://localhost:48080");
  40 + }
  41 +
  42 + @Test
  43 + public void testSendMessageWhenProcessInstanceApprove_success() {
  44 + // 准备参数
  45 + BpmMessageSendWhenProcessInstanceApproveReqDTO reqDTO = new BpmMessageSendWhenProcessInstanceApproveReqDTO()
  46 + .setProcessInstanceId("process-instance-1").setProcessInstanceName("请假申请").setStartUserId(1L);
  47 +
  48 + // 调用
  49 + messageService.sendMessageWhenProcessInstanceApprove(reqDTO);
  50 + // 断言
  51 + SmsSendSingleToUserReqDTO smsReqDTO = captureSmsRequest();
  52 + assertEquals(1L, smsReqDTO.getUserId());
  53 + assertEquals(BpmMessageEnum.PROCESS_INSTANCE_APPROVE.getSmsTemplateCode(), smsReqDTO.getTemplateCode());
  54 + assertEquals("请假申请", smsReqDTO.getTemplateParams().get("processInstanceName"));
  55 + assertEquals("http://localhost:48080/bpm/process-instance/detail?id=process-instance-1",
  56 + smsReqDTO.getTemplateParams().get("detailUrl"));
  57 + }
  58 +
  59 + @Test
  60 + public void testSendMessageWhenProcessInstanceReject_success() {
  61 + // 准备参数
  62 + BpmMessageSendWhenProcessInstanceRejectReqDTO reqDTO = new BpmMessageSendWhenProcessInstanceRejectReqDTO()
  63 + .setProcessInstanceId("process-instance-1").setProcessInstanceName("请假申请")
  64 + .setStartUserId(1L).setReason("材料不完整");
  65 +
  66 + // 调用
  67 + messageService.sendMessageWhenProcessInstanceReject(reqDTO);
  68 + // 断言
  69 + SmsSendSingleToUserReqDTO smsReqDTO = captureSmsRequest();
  70 + assertEquals(1L, smsReqDTO.getUserId());
  71 + assertEquals(BpmMessageEnum.PROCESS_INSTANCE_REJECT.getSmsTemplateCode(), smsReqDTO.getTemplateCode());
  72 + assertEquals("材料不完整", smsReqDTO.getTemplateParams().get("reason"));
  73 + }
  74 +
  75 + private SmsSendSingleToUserReqDTO captureSmsRequest() {
  76 + ArgumentCaptor<SmsSendSingleToUserReqDTO> captor = ArgumentCaptor.forClass(SmsSendSingleToUserReqDTO.class);
  77 + verify(smsSendApi).sendSingleSmsToAdmin(captor.capture());
  78 + return captor.getValue();
  79 + }
  80 +
  81 +}
... ...
yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/oa/BpmOALeaveServiceImplTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.bpm.service.oa;
  2 +
  3 +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
  4 +import cn.iocoder.yudao.module.bpm.api.task.BpmProcessInstanceApi;
  5 +import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
  6 +import cn.iocoder.yudao.module.bpm.controller.admin.oa.vo.BpmOALeaveCreateReqVO;
  7 +import cn.iocoder.yudao.module.bpm.dal.dataobject.oa.BpmOALeaveDO;
  8 +import cn.iocoder.yudao.module.bpm.dal.mysql.oa.BpmOALeaveMapper;
  9 +import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
  10 +import jakarta.annotation.Resource;
  11 +import org.junit.jupiter.api.Test;
  12 +import org.mockito.ArgumentCaptor;
  13 +import org.springframework.context.annotation.Import;
  14 +import org.springframework.test.context.bean.override.mockito.MockitoBean;
  15 +
  16 +import java.util.List;
  17 +import java.util.Map;
  18 +
  19 +import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildTime;
  20 +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
  21 +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
  22 +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.OA_LEAVE_NOT_EXISTS;
  23 +import static org.junit.jupiter.api.Assertions.*;
  24 +import static org.mockito.ArgumentMatchers.*;
  25 +import static org.mockito.Mockito.*;
  26 +
  27 +/**
  28 + * {@link BpmOALeaveServiceImpl} 的单元测试类
  29 + *
  30 + * @author HUIHUI
  31 + */
  32 +@Import(BpmOALeaveServiceImpl.class)
  33 +public class BpmOALeaveServiceImplTest extends BaseDbUnitTest {
  34 +
  35 + @Resource
  36 + private BpmOALeaveServiceImpl leaveService;
  37 + @Resource
  38 + private BpmOALeaveMapper leaveMapper;
  39 +
  40 + @MockitoBean
  41 + private BpmProcessInstanceApi processInstanceApi;
  42 +
  43 + @Test
  44 + public void testCreateLeave_success() {
  45 + // 准备参数
  46 + BpmOALeaveCreateReqVO reqVO = new BpmOALeaveCreateReqVO().setType(1).setReason("休假")
  47 + .setStartTime(buildTime(2026, 10, 1)).setEndTime(buildTime(2026, 10, 3))
  48 + .setStartUserSelectAssignees(Map.of("approve", List.of(2L)));
  49 + when(processInstanceApi.createProcessInstance(eq(1L), any())).thenReturn("process-instance-1");
  50 +
  51 + // 调用
  52 + Long leaveId = leaveService.createLeave(1L, reqVO);
  53 + // 断言
  54 + BpmOALeaveDO leave = leaveMapper.selectById(leaveId);
  55 + assertEquals(1L, leave.getUserId());
  56 + assertEquals(2L, leave.getDay());
  57 + assertEquals(BpmTaskStatusEnum.RUNNING.getStatus(), leave.getStatus());
  58 + assertEquals("process-instance-1", leave.getProcessInstanceId());
  59 + ArgumentCaptor<BpmProcessInstanceCreateReqDTO> captor = ArgumentCaptor.forClass(BpmProcessInstanceCreateReqDTO.class);
  60 + verify(processInstanceApi).createProcessInstance(eq(1L), captor.capture());
  61 + assertEquals("oa_leave", captor.getValue().getProcessDefinitionKey());
  62 + assertEquals(String.valueOf(leaveId), captor.getValue().getBusinessKey());
  63 + assertEquals(2L, captor.getValue().getVariables().get("day"));
  64 + assertEquals(reqVO.getStartUserSelectAssignees(), captor.getValue().getStartUserSelectAssignees());
  65 + }
  66 +
  67 + @Test
  68 + public void testCreateLeave_processCreateFails_rollback() {
  69 + // 准备参数
  70 + BpmOALeaveCreateReqVO reqVO = new BpmOALeaveCreateReqVO().setType(1).setReason("休假")
  71 + .setStartTime(buildTime(2026, 10, 1)).setEndTime(buildTime(2026, 10, 3));
  72 + when(processInstanceApi.createProcessInstance(eq(1L), any())).thenThrow(new IllegalStateException("流程启动失败"));
  73 +
  74 + // 调用,并断言异常
  75 + assertThrows(IllegalStateException.class, () -> leaveService.createLeave(1L, reqVO));
  76 +
  77 + // 断言
  78 + assertEquals(0, leaveMapper.selectCount());
  79 + }
  80 +
  81 + @Test
  82 + public void testUpdateLeaveStatus_success() {
  83 + // 准备参数
  84 + BpmOALeaveDO leave = randomPojo(BpmOALeaveDO.class);
  85 + leaveMapper.insert(leave);
  86 +
  87 + // 调用
  88 + leaveService.updateLeaveStatus(leave.getId(), BpmTaskStatusEnum.APPROVE.getStatus());
  89 + // 断言
  90 + assertEquals(BpmTaskStatusEnum.APPROVE.getStatus(), leaveMapper.selectById(leave.getId()).getStatus());
  91 + }
  92 +
  93 + @Test
  94 + public void testUpdateLeaveStatus_notExists() {
  95 + // 调用,并断言异常
  96 + assertServiceException(() -> leaveService.updateLeaveStatus(1L, BpmTaskStatusEnum.APPROVE.getStatus()),
  97 + OA_LEAVE_NOT_EXISTS);
  98 + }
  99 +
  100 +}
... ...
yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/oa/listener/BpmOALeaveStatusListenerTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.bpm.service.oa.listener;
  2 +
  3 +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
  4 +import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent;
  5 +import cn.iocoder.yudao.module.bpm.service.oa.BpmOALeaveService;
  6 +import org.junit.jupiter.api.Test;
  7 +import org.mockito.InjectMocks;
  8 +import org.mockito.Mock;
  9 +
  10 +import static org.mockito.Mockito.verify;
  11 +
  12 +/**
  13 + * {@link BpmOALeaveStatusListener} 的单元测试
  14 + *
  15 + * @author HUIHUI
  16 + */
  17 +public class BpmOALeaveStatusListenerTest extends BaseMockitoUnitTest {
  18 +
  19 + @InjectMocks
  20 + private BpmOALeaveStatusListener leaveStatusListener;
  21 +
  22 + @Mock
  23 + private BpmOALeaveService leaveService;
  24 +
  25 + @Test
  26 + public void testOnApplicationEvent_sameProcessDefinitionKey_updateStatus() {
  27 + // 准备参数
  28 + BpmProcessInstanceStatusEvent event = new BpmProcessInstanceStatusEvent(this);
  29 + event.setProcessDefinitionKey("oa_leave").setBusinessKey("1").setStatus(2);
  30 +
  31 + // 调用
  32 + leaveStatusListener.onApplicationEvent(event);
  33 + // 断言
  34 + verify(leaveService).updateLeaveStatus(1L, 2);
  35 + }
  36 +
  37 + @Test
  38 + public void testOnApplicationEvent_differentProcessDefinitionKey_ignore() {
  39 + // 准备参数
  40 + BpmProcessInstanceStatusEvent event = new BpmProcessInstanceStatusEvent(this);
  41 + event.setProcessDefinitionKey("oa_reimbursement").setBusinessKey("1").setStatus(2);
  42 +
  43 + // 调用
  44 + leaveStatusListener.onApplicationEvent(event);
  45 + // 断言
  46 + org.mockito.Mockito.verifyNoInteractions(leaveService);
  47 + }
  48 +
  49 +}
... ...
yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceCopyServiceImplTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.bpm.service.task;
  2 +
  3 +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
  4 +import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmProcessInstanceCopyDO;
  5 +import cn.iocoder.yudao.module.bpm.dal.mysql.task.BpmProcessInstanceCopyMapper;
  6 +import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
  7 +import org.flowable.engine.repository.ProcessDefinition;
  8 +import org.flowable.engine.runtime.ProcessInstance;
  9 +import org.flowable.task.api.Task;
  10 +import org.junit.jupiter.api.Test;
  11 +import org.mockito.ArgumentCaptor;
  12 +import org.mockito.InjectMocks;
  13 +import org.mockito.Mock;
  14 +
  15 +import java.util.List;
  16 +
  17 +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
  18 +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_DEFINITION_NOT_EXISTS;
  19 +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_INSTANCE_NOT_EXISTS;
  20 +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.TASK_NOT_EXISTS;
  21 +import static org.junit.jupiter.api.Assertions.assertEquals;
  22 +import static org.mockito.Mockito.verify;
  23 +import static org.mockito.Mockito.when;
  24 +
  25 +/**
  26 + * {@link BpmProcessInstanceCopyServiceImpl} 的单元测试
  27 + *
  28 + * @author HUIHUI
  29 + */
  30 +public class BpmProcessInstanceCopyServiceImplTest extends BaseMockitoUnitTest {
  31 +
  32 + @InjectMocks
  33 + private BpmProcessInstanceCopyServiceImpl copyService;
  34 +
  35 + @Mock
  36 + private BpmProcessInstanceCopyMapper copyMapper;
  37 + @Mock
  38 + private BpmTaskService taskService;
  39 + @Mock
  40 + private BpmProcessInstanceService processInstanceService;
  41 + @Mock
  42 + private BpmProcessDefinitionService processDefinitionService;
  43 + @Mock
  44 + private Task task;
  45 + @Mock
  46 + private ProcessInstance processInstance;
  47 + @Mock
  48 + private ProcessDefinition processDefinition;
  49 +
  50 + @Test
  51 + public void testCreateProcessInstanceCopy_task_success() {
  52 + // 准备参数
  53 + when(taskService.getTask("task-1")).thenReturn(task);
  54 + when(task.getProcessInstanceId()).thenReturn("process-instance-1");
  55 + when(task.getTaskDefinitionKey()).thenReturn("approve");
  56 + when(task.getName()).thenReturn("审批");
  57 + when(task.getId()).thenReturn("task-1");
  58 + prepareProcessInstance();
  59 +
  60 + // 调用
  61 + copyService.createProcessInstanceCopy(List.of(2L, 3L), "请知悉", "task-1");
  62 + // 断言
  63 + ArgumentCaptor<List<BpmProcessInstanceCopyDO>> captor = ArgumentCaptor.forClass(List.class);
  64 + verify(copyMapper).insertBatch(captor.capture());
  65 + assertEquals(List.of(2L, 3L), captor.getValue().stream().map(BpmProcessInstanceCopyDO::getUserId).toList());
  66 + assertEquals("process-instance-1", captor.getValue().get(0).getProcessInstanceId());
  67 + assertEquals(1L, captor.getValue().get(0).getStartUserId());
  68 + }
  69 +
  70 + @Test
  71 + public void testCreateProcessInstanceCopy_taskNotExists() {
  72 + // 准备参数
  73 + when(taskService.getTask("task-1")).thenReturn(null);
  74 +
  75 + // 调用,并断言异常
  76 + assertServiceException(() -> copyService.createProcessInstanceCopy(List.of(2L), "请知悉", "task-1"),
  77 + TASK_NOT_EXISTS);
  78 + }
  79 +
  80 + @Test
  81 + public void testCreateProcessInstanceCopy_processInstanceNotExists() {
  82 + // 准备参数
  83 + when(processInstanceService.getProcessInstance("process-instance-1")).thenReturn(null);
  84 +
  85 + // 调用,并断言异常
  86 + assertServiceException(() -> copyService.createProcessInstanceCopy(List.of(2L), "请知悉",
  87 + "process-instance-1", "approve", "审批", "task-1"), PROCESS_INSTANCE_NOT_EXISTS);
  88 + }
  89 +
  90 + @Test
  91 + public void testCreateProcessInstanceCopy_processDefinitionNotExists() {
  92 + // 准备参数
  93 + when(processInstanceService.getProcessInstance("process-instance-1")).thenReturn(processInstance);
  94 + when(processInstance.getProcessDefinitionId()).thenReturn("definition-1");
  95 + when(processDefinitionService.getProcessDefinition("definition-1")).thenReturn(null);
  96 +
  97 + // 调用,并断言异常
  98 + assertServiceException(() -> copyService.createProcessInstanceCopy(List.of(2L), "请知悉",
  99 + "process-instance-1", "approve", "审批", "task-1"), PROCESS_DEFINITION_NOT_EXISTS);
  100 + }
  101 +
  102 + private void prepareProcessInstance() {
  103 + when(processInstanceService.getProcessInstance("process-instance-1")).thenReturn(processInstance);
  104 + when(processInstance.getStartUserId()).thenReturn("1");
  105 + when(processInstance.getName()).thenReturn("请假申请");
  106 + when(processInstance.getProcessDefinitionId()).thenReturn("definition-1");
  107 + when(processDefinitionService.getProcessDefinition("definition-1")).thenReturn(processDefinition);
  108 + when(processDefinition.getCategory()).thenReturn("oa");
  109 + }
  110 +
  111 +}
... ...
yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImplTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.bpm.service.task;
  2 +
  3 +import cn.iocoder.yudao.framework.common.exception.ServiceException;
  4 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  5 +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
  6 +import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent;
  7 +import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
  8 +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
  9 +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailRespVO;
  10 +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCancelReqVO;
  11 +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCreateReqVO;
  12 +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
  13 +import cn.iocoder.yudao.module.bpm.dal.redis.BpmProcessIdRedisDAO;
  14 +import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
  15 +import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum;
  16 +import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum;
  17 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
  18 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants;
  19 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.event.BpmProcessInstanceEventPublisher;
  20 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmHttpRequestUtils;
  21 +import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
  22 +import cn.iocoder.yudao.module.bpm.service.message.BpmMessageService;
  23 +import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceApproveReqDTO;
  24 +import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
  25 +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
  26 +import org.flowable.common.engine.impl.identity.Authentication;
  27 +import org.flowable.engine.HistoryService;
  28 +import org.flowable.engine.RuntimeService;
  29 +import org.flowable.engine.repository.ProcessDefinition;
  30 +import org.flowable.engine.runtime.Execution;
  31 +import org.flowable.engine.runtime.ExecutionQuery;
  32 +import org.flowable.engine.runtime.ProcessInstance;
  33 +import org.flowable.engine.runtime.ProcessInstanceBuilder;
  34 +import org.junit.jupiter.api.Test;
  35 +import org.mockito.ArgumentCaptor;
  36 +import org.mockito.InjectMocks;
  37 +import org.mockito.Mock;
  38 +import org.mockito.MockedStatic;
  39 +import org.springframework.transaction.support.TransactionSynchronization;
  40 +import org.springframework.transaction.support.TransactionSynchronizationManager;
  41 +
  42 +import java.util.ArrayList;
  43 +import java.util.HashMap;
  44 +import java.util.List;
  45 +import java.util.Map;
  46 +
  47 +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
  48 +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*;
  49 +import static org.junit.jupiter.api.Assertions.*;
  50 +import static org.mockito.ArgumentMatchers.*;
  51 +import static org.mockito.Mockito.*;
  52 +
  53 +/**
  54 + * {@link BpmProcessInstanceServiceImpl} 的单元测试
  55 + *
  56 + * @author HUIHUI
  57 + */
  58 +public class BpmProcessInstanceServiceImplTest extends BaseMockitoUnitTest {
  59 +
  60 + @InjectMocks
  61 + private BpmProcessInstanceServiceImpl processInstanceService;
  62 +
  63 + @Mock
  64 + private RuntimeService runtimeService;
  65 + @Mock
  66 + private HistoryService historyService;
  67 + @Mock
  68 + private BpmProcessDefinitionService processDefinitionService;
  69 + @Mock
  70 + private BpmTaskService taskService;
  71 + @Mock
  72 + private BpmMessageService messageService;
  73 + @Mock
  74 + private AdminUserApi adminUserApi;
  75 + @Mock
  76 + private BpmProcessInstanceEventPublisher processInstanceEventPublisher;
  77 + @Mock
  78 + private BpmProcessIdRedisDAO processIdRedisDAO;
  79 + @Mock
  80 + private ProcessInstance processInstance;
  81 + @Mock
  82 + private ProcessDefinition processDefinition;
  83 +
  84 + // ========== createProcessInstance 相关 ==========
  85 +
  86 + @Test
  87 + public void testCreateProcessInstance_success() {
  88 + // 准备参数:动态表单必填字段已填写,开启流程编号规则、标题设置,并配置发起人自选审批人
  89 + BpmProcessInstanceServiceImpl spyService = spy(processInstanceService);
  90 + BpmModelMetaInfoVO.ProcessIdRule processIdRule = new BpmModelMetaInfoVO.ProcessIdRule().setEnable(true)
  91 + .setPrefix("QJ");
  92 + mockCreateProcessInstance(buildProcessDefinitionInfo()
  93 + .setFormType(BpmModelFormTypeEnum.NORMAL.getType())
  94 + .setFormFields(List.of(buildFormField("day", "请假天数", List.of("required"))))
  95 + .setProcessIdRule(processIdRule)
  96 + .setTitleSetting(new BpmModelMetaInfoVO.TitleSetting().setEnable(true)
  97 + .setTitle("{" + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_ID + "}的"
  98 + + "{" + BpmnVariableConstants.PROCESS_DEFINITION_NAME + "},请假{day}天")));
  99 + mockApprovalDetail(spyService, buildActivityNode("task1", "一级审批", BpmTaskCandidateStrategyEnum.USER),
  100 + buildActivityNode("task2", "二级审批", BpmTaskCandidateStrategyEnum.START_USER_SELECT));
  101 + when(adminUserApi.getUserMap(List.of(2L))).thenReturn(Map.of(2L, new AdminUserRespDTO().setId(2L)));
  102 + when(adminUserApi.getUser(1L)).thenReturn(new AdminUserRespDTO().setId(1L).setNickname("张三"));
  103 + when(processIdRedisDAO.generate(processIdRule)).thenReturn("QJ-001");
  104 + ProcessInstanceBuilder processInstanceBuilder = mockProcessInstanceBuilder();
  105 + Map<String, List<Long>> startUserSelectAssignees = Map.of("task2", List.of(2L));
  106 + BpmProcessInstanceCreateReqVO reqVO = new BpmProcessInstanceCreateReqVO().setProcessDefinitionId("definition-1")
  107 + .setVariables(new HashMap<>(Map.of("day", 3)))
  108 + .setStartUserSelectAssignees(startUserSelectAssignees);
  109 +
  110 + // 调用
  111 + String processInstanceId = spyService.createProcessInstance(1L, reqVO);
  112 + // 断言
  113 + assertEquals("process-instance-1", processInstanceId);
  114 + verify(processInstanceBuilder).processDefinitionId("definition-1");
  115 + verify(processInstanceBuilder).businessKey(null);
  116 + verify(processInstanceBuilder).predefineProcessInstanceId("QJ-001");
  117 + verify(processInstanceBuilder).name("张三的请假,请假3天");
  118 + Map<String, Object> variables = captureVariables(processInstanceBuilder);
  119 + assertEquals(3, variables.get("day"));
  120 + assertEquals(1L, variables.get(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_ID));
  121 + assertEquals(BpmProcessInstanceStatusEnum.RUNNING.getStatus(),
  122 + variables.get(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS));
  123 + assertEquals(true, variables.get(BpmnVariableConstants.PROCESS_INSTANCE_SKIP_EXPRESSION_ENABLED));
  124 + assertEquals(startUserSelectAssignees,
  125 + variables.get(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES));
  126 + }
  127 +
  128 + @Test
  129 + public void testCreateProcessInstance_dto() {
  130 + // 准备参数:未开启流程编号规则、标题设置
  131 + BpmProcessInstanceServiceImpl spyService = spy(processInstanceService);
  132 + when(processDefinitionService.getActiveProcessDefinition("leave")).thenReturn(processDefinition);
  133 + when(processDefinition.getId()).thenReturn("definition-1");
  134 + when(processDefinition.getName()).thenReturn("请假");
  135 + BpmProcessDefinitionInfoDO processDefinitionInfo = buildProcessDefinitionInfo();
  136 + when(processDefinitionService.getProcessDefinitionInfo("definition-1")).thenReturn(processDefinitionInfo);
  137 + when(processDefinitionService.canUserStartProcessDefinition(processDefinitionInfo, 1L)).thenReturn(true);
  138 + mockApprovalDetail(spyService);
  139 + ProcessInstanceBuilder processInstanceBuilder = mockProcessInstanceBuilder();
  140 + // 发起时,需要设置 Flowable 的认证用户,作为流程发起人
  141 + List<String> authenticatedUserIds = new ArrayList<>();
  142 + when(processInstanceBuilder.start()).thenAnswer(invocation -> {
  143 + authenticatedUserIds.add(Authentication.getAuthenticatedUserId());
  144 + return processInstance;
  145 + });
  146 +
  147 + // 调用
  148 + String processInstanceId = spyService.createProcessInstance(1L, new BpmProcessInstanceCreateReqDTO()
  149 + .setProcessDefinitionKey("leave").setBusinessKey("business-1"));
  150 + // 断言
  151 + assertEquals("process-instance-1", processInstanceId);
  152 + assertEquals(List.of("1"), authenticatedUserIds);
  153 + assertNull(Authentication.getAuthenticatedUserId());
  154 + verify(processInstanceBuilder).businessKey("business-1");
  155 + verify(processInstanceBuilder).name("请假");
  156 + verify(processInstanceBuilder, never()).predefineProcessInstanceId(anyString());
  157 + Map<String, Object> variables = captureVariables(processInstanceBuilder);
  158 + assertEquals(1L, variables.get(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_ID));
  159 + assertFalse(variables.containsKey(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES));
  160 + verifyNoInteractions(processIdRedisDAO);
  161 + }
  162 +
  163 + @Test
  164 + public void testCreateProcessInstance_definitionNotExists() {
  165 + // 准备参数
  166 + BpmProcessInstanceCreateReqVO reqVO = new BpmProcessInstanceCreateReqVO().setProcessDefinitionId("definition-1");
  167 +
  168 + // 调用,并断言异常
  169 + assertServiceException(() -> processInstanceService.createProcessInstance(1L, reqVO),
  170 + PROCESS_DEFINITION_NOT_EXISTS);
  171 + verifyNoInteractions(runtimeService);
  172 + }
  173 +
  174 + @Test
  175 + public void testCreateProcessInstance_dtoDefinitionNotExists() {
  176 + // 调用
  177 + RuntimeException exception = assertThrows(RuntimeException.class, () -> processInstanceService
  178 + .createProcessInstance(1L, new BpmProcessInstanceCreateReqDTO().setProcessDefinitionKey("leave")));
  179 + // 断言:ServiceException 被 FlowableUtils 包装,由全局异常处理器解包;认证用户需要被清理
  180 + ServiceException serviceException = assertInstanceOf(ServiceException.class, exception.getCause());
  181 + assertEquals(PROCESS_DEFINITION_NOT_EXISTS.getCode(), serviceException.getCode());
  182 + assertNull(Authentication.getAuthenticatedUserId());
  183 + }
  184 +
  185 + @Test
  186 + public void testCreateProcessInstance_definitionSuspended() {
  187 + // 准备参数
  188 + when(processDefinitionService.getProcessDefinition("definition-1")).thenReturn(processDefinition);
  189 + when(processDefinition.getId()).thenReturn("definition-1");
  190 + when(processDefinition.isSuspended()).thenReturn(true);
  191 +
  192 + // 调用,并断言异常
  193 + assertServiceException(() -> processInstanceService.createProcessInstance(1L,
  194 + new BpmProcessInstanceCreateReqVO().setProcessDefinitionId("definition-1")), PROCESS_DEFINITION_IS_SUSPENDED);
  195 + }
  196 +
  197 + @Test
  198 + public void testCreateProcessInstance_definitionInfoNotExists() {
  199 + // 准备参数
  200 + when(processDefinitionService.getProcessDefinition("definition-1")).thenReturn(processDefinition);
  201 + when(processDefinition.getId()).thenReturn("definition-1");
  202 +
  203 + // 调用,并断言异常
  204 + assertServiceException(() -> processInstanceService.createProcessInstance(1L,
  205 + new BpmProcessInstanceCreateReqVO().setProcessDefinitionId("definition-1")), PROCESS_DEFINITION_NOT_EXISTS);
  206 + }
  207 +
  208 + @Test
  209 + public void testCreateProcessInstance_userCannotStart() {
  210 + // 准备参数
  211 + when(processDefinitionService.getProcessDefinition("definition-1")).thenReturn(processDefinition);
  212 + when(processDefinition.getId()).thenReturn("definition-1");
  213 + when(processDefinitionService.getProcessDefinitionInfo("definition-1")).thenReturn(buildProcessDefinitionInfo());
  214 +
  215 + // 调用,并断言异常
  216 + assertServiceException(() -> processInstanceService.createProcessInstance(1L,
  217 + new BpmProcessInstanceCreateReqVO().setProcessDefinitionId("definition-1")),
  218 + PROCESS_INSTANCE_START_USER_CAN_START);
  219 + verifyNoInteractions(runtimeService);
  220 + }
  221 +
  222 + @Test
  223 + public void testCreateProcessInstance_formFieldRequired() {
  224 + // 准备参数:必填标记为数组 ["required"],字段未填写
  225 + when(processDefinitionService.getProcessDefinition("definition-1")).thenReturn(processDefinition);
  226 + when(processDefinition.getId()).thenReturn("definition-1");
  227 + when(processDefinitionService.getProcessDefinitionInfo("definition-1")).thenReturn(buildProcessDefinitionInfo()
  228 + .setFormType(BpmModelFormTypeEnum.NORMAL.getType())
  229 + .setFormFields(List.of(buildFormField("day", "请假天数", List.of("required")))));
  230 +
  231 + // 调用,并断言异常
  232 + assertServiceException(() -> processInstanceService.createProcessInstance(1L,
  233 + new BpmProcessInstanceCreateReqVO().setProcessDefinitionId("definition-1")),
  234 + PROCESS_INSTANCE_START_FORM_FIELD_REQUIRED, "请假天数");
  235 + verifyNoInteractions(runtimeService);
  236 + }
  237 +
  238 + @Test
  239 + public void testCreateProcessInstance_formFieldRequiredBlank() {
  240 + // 准备参数:字段标识为 key,必填标记为对象 {"required": true},字段值为空白字符串
  241 + when(processDefinitionService.getProcessDefinition("definition-1")).thenReturn(processDefinition);
  242 + when(processDefinition.getId()).thenReturn("definition-1");
  243 + String formField = JsonUtils.toJsonString(Map.of("key", "reason", "validate", Map.of("required", true)));
  244 + when(processDefinitionService.getProcessDefinitionInfo("definition-1")).thenReturn(buildProcessDefinitionInfo()
  245 + .setFormType(BpmModelFormTypeEnum.NORMAL.getType()).setFormFields(List.of(formField)));
  246 +
  247 + // 调用,并断言异常:没有 title 时,使用字段标识提示
  248 + assertServiceException(() -> processInstanceService.createProcessInstance(1L,
  249 + new BpmProcessInstanceCreateReqVO().setProcessDefinitionId("definition-1")
  250 + .setVariables(new HashMap<>(Map.of("reason", " ")))),
  251 + PROCESS_INSTANCE_START_FORM_FIELD_REQUIRED, "reason");
  252 + }
  253 +
  254 + @Test
  255 + public void testCreateProcessInstance_startUserSelectAssigneesNotConfig() {
  256 + // 准备参数:预测到发起人自选节点,但未选择审批人
  257 + BpmProcessInstanceServiceImpl spyService = spy(processInstanceService);
  258 + mockCreateProcessInstance(buildProcessDefinitionInfo());
  259 + mockApprovalDetail(spyService,
  260 + buildActivityNode("task2", "二级审批", BpmTaskCandidateStrategyEnum.START_USER_SELECT));
  261 +
  262 + // 调用,并断言异常
  263 + assertServiceException(() -> spyService.createProcessInstance(1L,
  264 + new BpmProcessInstanceCreateReqVO().setProcessDefinitionId("definition-1")),
  265 + PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_CONFIG, "二级审批");
  266 + verifyNoInteractions(runtimeService);
  267 + }
  268 +
  269 + @Test
  270 + public void testCreateProcessInstance_startUserSelectAssigneesNotExists() {
  271 + // 准备参数:选择的审批人不存在
  272 + BpmProcessInstanceServiceImpl spyService = spy(processInstanceService);
  273 + mockCreateProcessInstance(buildProcessDefinitionInfo());
  274 + mockApprovalDetail(spyService,
  275 + buildActivityNode("task2", "二级审批", BpmTaskCandidateStrategyEnum.START_USER_SELECT));
  276 + when(adminUserApi.getUserMap(List.of(2L))).thenReturn(Map.of());
  277 +
  278 + // 调用,并断言异常
  279 + assertServiceException(() -> spyService.createProcessInstance(1L,
  280 + new BpmProcessInstanceCreateReqVO().setProcessDefinitionId("definition-1")
  281 + .setStartUserSelectAssignees(Map.of("task2", List.of(2L)))),
  282 + PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_EXISTS, "二级审批", 2L);
  283 + }
  284 +
  285 + // ========== cancelProcessInstance 相关 ==========
  286 +
  287 + @Test
  288 + public void testCancelProcessInstanceByAdmin_notExists() {
  289 + // 准备参数
  290 + BpmProcessInstanceServiceImpl spyService = spy(processInstanceService);
  291 + doReturn(null).when(spyService).getProcessInstance("process-instance-1");
  292 +
  293 + // 调用,并断言异常
  294 + assertServiceException(() -> spyService.cancelProcessInstanceByAdmin(1L, buildCancelReq()),
  295 + PROCESS_INSTANCE_CANCEL_FAIL_NOT_EXISTS);
  296 + }
  297 +
  298 + @Test
  299 + public void testCancelProcessInstanceByStartUser_notExists() {
  300 + // 准备参数
  301 + BpmProcessInstanceServiceImpl spyService = spy(processInstanceService);
  302 + doReturn(null).when(spyService).getProcessInstance("process-instance-1");
  303 + BpmProcessInstanceCancelReqVO reqVO = buildCancelReq();
  304 +
  305 + // 调用,并断言异常
  306 + assertServiceException(() -> spyService.cancelProcessInstanceByStartUser(1L, reqVO),
  307 + PROCESS_INSTANCE_CANCEL_FAIL_NOT_EXISTS);
  308 + }
  309 +
  310 + @Test
  311 + public void testCancelProcessInstanceByStartUser_notAllow() {
  312 + // 准备参数
  313 + BpmProcessInstanceServiceImpl spyService = spy(processInstanceService);
  314 + doReturn(processInstance).when(spyService).getProcessInstance("process-instance-1");
  315 + when(processInstance.getStartUserId()).thenReturn("1");
  316 + when(processInstance.getProcessDefinitionId()).thenReturn("definition-1");
  317 + when(processDefinitionService.getProcessDefinitionInfo("definition-1"))
  318 + .thenReturn(new BpmProcessDefinitionInfoDO().setAllowCancelRunningProcess(false));
  319 + BpmProcessInstanceCancelReqVO reqVO = buildCancelReq();
  320 +
  321 + // 调用,并断言异常
  322 + assertServiceException(() -> spyService.cancelProcessInstanceByStartUser(1L, reqVO),
  323 + PROCESS_INSTANCE_CANCEL_FAIL_NOT_ALLOW);
  324 + }
  325 +
  326 + // ========== Event 事件相关方法 ==========
  327 +
  328 + @Test
  329 + public void testProcessProcessInstanceCompleted_approve() {
  330 + // 准备参数:流程结束时仍为审批中,说明审批通过;配置了流程后置通知
  331 + mockCompletedProcessInstance(BpmProcessInstanceStatusEnum.RUNNING.getStatus(), null);
  332 + BpmModelMetaInfoVO.HttpRequestSetting setting = new BpmModelMetaInfoVO.HttpRequestSetting()
  333 + .setUrl("http://127.0.0.1/after");
  334 + when(processDefinitionService.getProcessDefinitionInfo("definition-1"))
  335 + .thenReturn(new BpmProcessDefinitionInfoDO().setProcessAfterTriggerSetting(setting));
  336 +
  337 + // 调用
  338 + try (MockedStatic<BpmHttpRequestUtils> httpRequestUtilsMockedStatic = mockStatic(BpmHttpRequestUtils.class)) {
  339 + processInstanceService.processProcessInstanceCompleted(processInstance);
  340 + // 断言:后置通知
  341 + httpRequestUtilsMockedStatic.verify(() -> BpmHttpRequestUtils.executeBpmHttpRequest(processInstance,
  342 + "http://127.0.0.1/after", null, null, true, null));
  343 + }
  344 + // 断言:状态更新为审批通过
  345 + verify(runtimeService).setVariable("process-instance-1", BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS,
  346 + BpmProcessInstanceStatusEnum.APPROVE.getStatus());
  347 + // 断言:审批通过消息
  348 + ArgumentCaptor<BpmMessageSendWhenProcessInstanceApproveReqDTO> messageCaptor =
  349 + ArgumentCaptor.forClass(BpmMessageSendWhenProcessInstanceApproveReqDTO.class);
  350 + verify(messageService).sendMessageWhenProcessInstanceApprove(messageCaptor.capture());
  351 + assertEquals("process-instance-1", messageCaptor.getValue().getProcessInstanceId());
  352 + assertEquals("请假流程", messageCaptor.getValue().getProcessInstanceName());
  353 + assertEquals(1L, messageCaptor.getValue().getStartUserId());
  354 + // 断言:状态事件
  355 + BpmProcessInstanceStatusEvent event = captureStatusEvent();
  356 + assertEquals("process-instance-1", event.getId());
  357 + assertEquals(BpmProcessInstanceStatusEnum.APPROVE.getStatus(), event.getStatus());
  358 + assertEquals("leave", event.getProcessDefinitionKey());
  359 + assertEquals("business-1", event.getBusinessKey());
  360 + }
  361 +
  362 + @Test
  363 + public void testProcessProcessInstanceCompleted_childRejectRolledBack() {
  364 + // 准备参数
  365 + BpmProcessInstanceServiceImpl spyService = spy(processInstanceService);
  366 + ProcessInstance parentProcessInstance = mockChildRejectProcessInstance(spyService);
  367 +
  368 + // 调用:事务回滚
  369 + executeWithSynchronization(() -> spyService.processProcessInstanceCompleted(processInstance),
  370 + TransactionSynchronization.STATUS_ROLLED_BACK);
  371 + // 断言:回滚时,不结束父流程
  372 + verify(spyService).updateProcessInstanceReject(parentProcessInstance,
  373 + BpmReasonEnum.REJECT_CHILD_PROCESS.getReason());
  374 + verifyNoInteractions(taskService);
  375 + }
  376 +
  377 + @Test
  378 + public void testProcessProcessInstanceCreated_definitionNotExists() {
  379 + // 准备参数
  380 + when(processInstance.getProcessDefinitionId()).thenReturn("definition-1");
  381 +
  382 + // 调用
  383 + executeWithSynchronization(() -> processInstanceService.processProcessInstanceCreated(processInstance),
  384 + TransactionSynchronization.STATUS_COMMITTED);
  385 + // 断言
  386 + verifyNoInteractions(runtimeService, adminUserApi);
  387 + }
  388 +
  389 + @Test
  390 + public void testProcessProcessInstanceCreated_titleAndBeforeTrigger() {
  391 + // 准备参数:开启标题设置,并配置流程前置通知
  392 + BpmModelMetaInfoVO.HttpRequestSetting setting = new BpmModelMetaInfoVO.HttpRequestSetting()
  393 + .setUrl("http://127.0.0.1/before");
  394 + mockCreatedProcessInstance(buildProcessDefinitionInfo()
  395 + .setTitleSetting(new BpmModelMetaInfoVO.TitleSetting().setEnable(true)
  396 + .setTitle("{" + BpmnVariableConstants.PROCESS_DEFINITION_NAME + "}-{day}天"))
  397 + .setProcessBeforeTriggerSetting(setting));
  398 + when(processInstance.getName()).thenReturn("请假");
  399 +
  400 + // 调用
  401 + try (MockedStatic<BpmHttpRequestUtils> httpRequestUtilsMockedStatic = mockStatic(BpmHttpRequestUtils.class)) {
  402 + executeWithSynchronization(() -> processInstanceService.processProcessInstanceCreated(processInstance),
  403 + TransactionSynchronization.STATUS_COMMITTED);
  404 + // 断言
  405 + verify(runtimeService).setProcessInstanceName("process-instance-1", "请假-3天");
  406 + httpRequestUtilsMockedStatic.verify(() -> BpmHttpRequestUtils.executeBpmHttpRequest(processInstance,
  407 + "http://127.0.0.1/before", null, null, true, null));
  408 + }
  409 + }
  410 +
  411 + @Test
  412 + public void testProcessProcessInstanceCreated_nameNotChanged() {
  413 + // 准备参数:未开启标题设置,名称与流程定义一致
  414 + mockCreatedProcessInstance(buildProcessDefinitionInfo());
  415 + when(processInstance.getName()).thenReturn("请假");
  416 +
  417 + // 调用
  418 + try (MockedStatic<BpmHttpRequestUtils> httpRequestUtilsMockedStatic = mockStatic(BpmHttpRequestUtils.class)) {
  419 + executeWithSynchronization(() -> processInstanceService.processProcessInstanceCreated(processInstance),
  420 + TransactionSynchronization.STATUS_COMMITTED);
  421 + // 断言:名称未变化时,不更新;未配置前置通知时,不发起请求
  422 + verify(runtimeService, never()).setProcessInstanceName(anyString(), anyString());
  423 + httpRequestUtilsMockedStatic.verifyNoInteractions();
  424 + }
  425 + }
  426 +
  427 + // ========== 私有方法 ==========
  428 +
  429 + private static BpmProcessInstanceCancelReqVO buildCancelReq() {
  430 + return new BpmProcessInstanceCancelReqVO().setId("process-instance-1").setReason("撤销申请");
  431 + }
  432 +
  433 + private static BpmProcessDefinitionInfoDO buildProcessDefinitionInfo() {
  434 + return new BpmProcessDefinitionInfoDO().setProcessDefinitionId("definition-1");
  435 + }
  436 +
  437 + private static String buildFormField(String field, String title, Object validate) {
  438 + return JsonUtils.toJsonString(Map.of("field", field, "title", title, "validate", validate));
  439 + }
  440 +
  441 + private static BpmApprovalDetailRespVO.ActivityNode buildActivityNode(String id, String name,
  442 + BpmTaskCandidateStrategyEnum strategy) {
  443 + return new BpmApprovalDetailRespVO.ActivityNode().setId(id).setName(name)
  444 + .setCandidateStrategy(strategy.getStrategy());
  445 + }
  446 +
  447 + private void mockCreateProcessInstance(BpmProcessDefinitionInfoDO processDefinitionInfo) {
  448 + when(processDefinitionService.getProcessDefinition("definition-1")).thenReturn(processDefinition);
  449 + when(processDefinition.getId()).thenReturn("definition-1");
  450 + lenient().when(processDefinition.getName()).thenReturn(" 请假 ");
  451 + when(processDefinitionService.getProcessDefinitionInfo("definition-1")).thenReturn(processDefinitionInfo);
  452 + when(processDefinitionService.canUserStartProcessDefinition(processDefinitionInfo, 1L)).thenReturn(true);
  453 + }
  454 +
  455 + /**
  456 + * 模拟发起流程时的审批节点预测结果
  457 + */
  458 + private void mockApprovalDetail(BpmProcessInstanceServiceImpl spyService,
  459 + BpmApprovalDetailRespVO.ActivityNode... activityNodes) {
  460 + doReturn(new BpmApprovalDetailRespVO().setActivityNodes(new ArrayList<>(List.of(activityNodes))))
  461 + .when(spyService).getApprovalDetail(eq(1L), any());
  462 + }
  463 +
  464 + private ProcessInstanceBuilder mockProcessInstanceBuilder() {
  465 + ProcessInstanceBuilder processInstanceBuilder = mock(ProcessInstanceBuilder.class, RETURNS_SELF);
  466 + when(runtimeService.createProcessInstanceBuilder()).thenReturn(processInstanceBuilder);
  467 + lenient().when(processInstanceBuilder.start()).thenReturn(processInstance);
  468 + when(processInstance.getId()).thenReturn("process-instance-1");
  469 + return processInstanceBuilder;
  470 + }
  471 +
  472 + @SuppressWarnings("unchecked")
  473 + private static Map<String, Object> captureVariables(ProcessInstanceBuilder processInstanceBuilder) {
  474 + ArgumentCaptor<Map<String, Object>> variablesCaptor = ArgumentCaptor.forClass(Map.class);
  475 + verify(processInstanceBuilder).variables(variablesCaptor.capture());
  476 + return variablesCaptor.getValue();
  477 + }
  478 +
  479 + private void mockCompletedProcessInstance(Integer status, String reason) {
  480 + Map<String, Object> variables = new HashMap<>();
  481 + variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS, status);
  482 + variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_REASON, reason);
  483 + when(processInstance.getProcessVariables()).thenReturn(variables);
  484 + lenient().when(processInstance.getId()).thenReturn("process-instance-1");
  485 + lenient().when(processInstance.getName()).thenReturn("请假流程");
  486 + lenient().when(processInstance.getStartUserId()).thenReturn("1");
  487 + lenient().when(processInstance.getProcessDefinitionId()).thenReturn("definition-1");
  488 + lenient().when(processInstance.getProcessDefinitionKey()).thenReturn("leave");
  489 + lenient().when(processInstance.getBusinessKey()).thenReturn("business-1");
  490 + }
  491 +
  492 + private ProcessInstance mockChildRejectProcessInstance(BpmProcessInstanceServiceImpl spyService) {
  493 + mockCompletedProcessInstance(BpmProcessInstanceStatusEnum.REJECT.getStatus(), "不同意");
  494 + when(processInstance.getSuperExecutionId()).thenReturn("super-execution-1");
  495 + ExecutionQuery executionQuery = mock(ExecutionQuery.class, RETURNS_SELF);
  496 + when(runtimeService.createExecutionQuery()).thenReturn(executionQuery);
  497 + Execution execution = mock(Execution.class);
  498 + when(execution.getProcessInstanceId()).thenReturn("parent-process-instance-1");
  499 + when(executionQuery.singleResult()).thenReturn(execution);
  500 + ProcessInstance parentProcessInstance = mock(ProcessInstance.class);
  501 + lenient().when(parentProcessInstance.getId()).thenReturn("parent-process-instance-1");
  502 + when(parentProcessInstance.getProcessInstanceId()).thenReturn("parent-process-instance-1");
  503 + doReturn(parentProcessInstance).when(spyService).getProcessInstance("parent-process-instance-1");
  504 + return parentProcessInstance;
  505 + }
  506 +
  507 + private void mockCreatedProcessInstance(BpmProcessDefinitionInfoDO processDefinitionInfo) {
  508 + when(processInstance.getProcessDefinitionId()).thenReturn("definition-1");
  509 + lenient().when(processInstance.getProcessInstanceId()).thenReturn("process-instance-1");
  510 + when(processInstance.getStartUserId()).thenReturn("1");
  511 + lenient().when(processInstance.getProcessVariables()).thenReturn(Map.of("day", 3));
  512 + when(processDefinitionService.getProcessDefinitionInfo("definition-1")).thenReturn(processDefinitionInfo);
  513 + when(processDefinitionService.getProcessDefinition("definition-1")).thenReturn(processDefinition);
  514 + when(processDefinition.getName()).thenReturn("请假");
  515 + lenient().when(adminUserApi.getUser(1L)).thenReturn(new AdminUserRespDTO().setId(1L).setNickname("张三"));
  516 + }
  517 +
  518 + private BpmProcessInstanceStatusEvent captureStatusEvent() {
  519 + ArgumentCaptor<BpmProcessInstanceStatusEvent> eventCaptor =
  520 + ArgumentCaptor.forClass(BpmProcessInstanceStatusEvent.class);
  521 + verify(processInstanceEventPublisher).sendProcessInstanceResultEvent(eventCaptor.capture());
  522 + return eventCaptor.getValue();
  523 + }
  524 +
  525 + /**
  526 + * 在事务同步上下文中执行,并模拟事务提交(或回滚)回调
  527 + */
  528 + private static void executeWithSynchronization(Runnable runnable, int transactionStatus) {
  529 + TransactionSynchronizationManager.initSynchronization();
  530 + try {
  531 + runnable.run();
  532 + List<TransactionSynchronization> synchronizations = TransactionSynchronizationManager.getSynchronizations();
  533 + if (transactionStatus == TransactionSynchronization.STATUS_COMMITTED) {
  534 + synchronizations.forEach(TransactionSynchronization::afterCommit);
  535 + }
  536 + synchronizations.forEach(synchronization -> synchronization.afterCompletion(transactionStatus));
  537 + } finally {
  538 + TransactionSynchronizationManager.clearSynchronization();
  539 + }
  540 + }
  541 +
  542 +}
... ...
yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImplTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.bpm.service.task;
  2 +
  3 +import cn.hutool.core.io.resource.ResourceUtil;
  4 +import cn.hutool.extra.spring.SpringUtil;
  5 +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
  6 +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
  7 +import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.*;
  8 +import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
  9 +import cn.iocoder.yudao.module.bpm.enums.definition.*;
  10 +import cn.iocoder.yudao.module.bpm.enums.task.BpmAttachmentTypeEnum;
  11 +import cn.iocoder.yudao.module.bpm.enums.task.BpmCommentTypeEnum;
  12 +import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum;
  13 +import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskSignTypeEnum;
  14 +import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
  15 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
  16 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants;
  17 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants;
  18 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmHttpRequestUtils;
  19 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
  20 +import cn.iocoder.yudao.module.bpm.service.comment.BpmCommentService;
  21 +import cn.iocoder.yudao.module.bpm.service.definition.BpmModelService;
  22 +import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
  23 +import cn.iocoder.yudao.module.bpm.service.message.BpmMessageService;
  24 +import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenTaskCreatedReqDTO;
  25 +import cn.iocoder.yudao.module.system.api.dept.DeptApi;
  26 +import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
  27 +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
  28 +import org.flowable.bpmn.model.BpmnModel;
  29 +import org.flowable.bpmn.model.FlowElement;
  30 +import org.flowable.bpmn.model.Process;
  31 +import org.flowable.bpmn.model.UserTask;
  32 +import org.flowable.engine.HistoryService;
  33 +import org.flowable.engine.RuntimeService;
  34 +import org.flowable.engine.TaskService;
  35 +import org.flowable.engine.impl.persistence.entity.AttachmentEntityImpl;
  36 +import org.flowable.engine.runtime.ProcessInstance;
  37 +import org.flowable.engine.task.Attachment;
  38 +import org.flowable.task.api.Task;
  39 +import org.flowable.task.api.TaskQuery;
  40 +import org.flowable.task.api.history.HistoricTaskInstance;
  41 +import org.flowable.task.api.history.HistoricTaskInstanceQuery;
  42 +import org.flowable.task.service.impl.persistence.entity.TaskEntityImpl;
  43 +import org.junit.jupiter.api.AfterEach;
  44 +import org.junit.jupiter.api.BeforeEach;
  45 +import org.junit.jupiter.api.Test;
  46 +import org.mockito.ArgumentCaptor;
  47 +import org.mockito.InjectMocks;
  48 +import org.mockito.Mock;
  49 +import org.mockito.MockedStatic;
  50 +import org.springframework.transaction.support.TransactionSynchronization;
  51 +import org.springframework.transaction.support.TransactionSynchronizationManager;
  52 +
  53 +import java.util.HashMap;
  54 +import java.util.HashSet;
  55 +import java.util.List;
  56 +import java.util.Map;
  57 +import java.util.Set;
  58 +
  59 +import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
  60 +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*;
  61 +import static org.junit.jupiter.api.Assertions.*;
  62 +import static org.mockito.ArgumentMatchers.*;
  63 +import static org.mockito.Mockito.*;
  64 +
  65 +/**
  66 + * {@link BpmTaskServiceImpl} 的单元测试
  67 + *
  68 + * @author HUIHUI
  69 + */
  70 +public class BpmTaskServiceImplTest extends BaseMockitoUnitTest {
  71 +
  72 + @InjectMocks
  73 + private BpmTaskServiceImpl taskService;
  74 +
  75 + @Mock
  76 + private TaskService flowableTaskService;
  77 + @Mock
  78 + private Task task;
  79 + @Mock
  80 + private HistoryService historyService;
  81 + @Mock
  82 + private RuntimeService runtimeService;
  83 + @Mock
  84 + private BpmProcessInstanceService processInstanceService;
  85 + @Mock
  86 + private BpmProcessDefinitionService processDefinitionService;
  87 + @Mock
  88 + private BpmProcessInstanceCopyService processInstanceCopyService;
  89 + @Mock
  90 + private BpmCommentService commentService;
  91 + @Mock
  92 + private BpmModelService modelService;
  93 + @Mock
  94 + private BpmMessageService messageService;
  95 + @Mock
  96 + private AdminUserApi adminUserApi;
  97 + @Mock
  98 + private DeptApi deptApi;
  99 + @Mock
  100 + private ProcessInstance processInstance;
  101 +
  102 + /**
  103 + * 自身代理对象,用于验证 getSelf() 发起的自动审批、自动拒绝、转办
  104 + */
  105 + @Mock
  106 + private BpmTaskServiceImpl selfService;
  107 +
  108 + private MockedStatic<SpringUtil> springUtilMockedStatic;
  109 +
  110 + @BeforeEach
  111 + public void setUp() {
  112 + springUtilMockedStatic = mockStatic(SpringUtil.class);
  113 + springUtilMockedStatic.when(() -> SpringUtil.getBean(any(Class.class))).thenReturn(selfService);
  114 + }
  115 +
  116 + @AfterEach
  117 + public void tearDown() {
  118 + springUtilMockedStatic.close();
  119 + }
  120 +
  121 + @Test
  122 + public void testValidateTask_success() {
  123 + // 准备参数:通过 spy 绕过 Flowable 查询,只验证负责人校验
  124 + BpmTaskServiceImpl spyService = spy(taskService);
  125 + doReturn(task).when(spyService).validateTaskExists("task-1");
  126 + when(task.getAssignee()).thenReturn("1");
  127 +
  128 + // 调用
  129 + Task result = spyService.validateTask(1L, "task-1");
  130 + // 断言
  131 + assertSame(task, result);
  132 + }
  133 +
  134 + @Test
  135 + public void testValidateTask_assigneeNotSelf() {
  136 + // 准备参数
  137 + BpmTaskServiceImpl spyService = spy(taskService);
  138 + doReturn(task).when(spyService).validateTaskExists("task-1");
  139 + when(task.getAssignee()).thenReturn("2");
  140 +
  141 + // 调用,并断言异常
  142 + assertServiceException(() -> spyService.validateTask(1L, "task-1"), TASK_OPERATE_FAIL_ASSIGN_NOT_SELF);
  143 + }
  144 +
  145 + @Test
  146 + public void testValidateTask_noAssignee_success() {
  147 + // 准备参数
  148 + BpmTaskServiceImpl spyService = spy(taskService);
  149 + doReturn(task).when(spyService).validateTaskExists("task-1");
  150 + when(task.getAssignee()).thenReturn(null);
  151 +
  152 + // 调用
  153 + Task result = spyService.validateTask(null, "task-1");
  154 + // 断言
  155 + assertSame(task, result);
  156 + }
  157 +
  158 + @Test
  159 + public void testValidateTaskExists_notExists() {
  160 + // 准备参数
  161 + BpmTaskServiceImpl spyService = spy(taskService);
  162 + doReturn(null).when(spyService).getTask("task-1");
  163 +
  164 + // 调用,并断言异常
  165 + assertServiceException(() -> spyService.validateTaskExists("task-1"), TASK_NOT_EXISTS);
  166 + }
  167 +
  168 + // ========== Query 查询相关方法 ==========
  169 +
  170 + @Test
  171 + public void testGetAllChildrenTaskListByParentTaskId_success() {
  172 + // 准备参数:parent -> child1 -> child2;other 属于其它父任务
  173 + TaskEntityImpl parent = buildTaskEntity("parent", null);
  174 + TaskEntityImpl child1 = buildTaskEntity("child1", "parent");
  175 + TaskEntityImpl child2 = buildTaskEntity("child2", "child1");
  176 + TaskEntityImpl other = buildTaskEntity("other", "unknown");
  177 +
  178 + // 调用
  179 + List<TaskEntityImpl> result = taskService.getAllChildrenTaskListByParentTaskId("parent",
  180 + List.of(parent, child1, child2, other));
  181 + // 断言
  182 + assertEquals(List.of("child1", "child2"), result.stream().map(TaskEntityImpl::getId).toList());
  183 + assertTrue(taskService.getAllChildrenTaskListByParentTaskId("parent", List.of(parent)).isEmpty());
  184 + assertTrue(taskService.getAllChildrenTaskListByParentTaskId("parent", List.<Task>of()).isEmpty());
  185 + }
  186 +
  187 + @Test
  188 + public void testGetAttachments_filter() {
  189 + // 准备参数
  190 + Attachment attachment1 = buildAttachment("task-1", BpmAttachmentTypeEnum.TASK_ATTACHMENT.getType());
  191 + Attachment attachment2 = buildAttachment("task-2", BpmAttachmentTypeEnum.TASK_ATTACHMENT.getType());
  192 + Attachment attachment3 = buildAttachment("task-1", "other");
  193 + when(flowableTaskService.getProcessInstanceAttachments("process-instance-1"))
  194 + .thenReturn(List.of(attachment1, attachment2, attachment3));
  195 +
  196 + // 调用
  197 + List<Attachment> result = taskService.getAttachments("process-instance-1", Set.of("task-1"),
  198 + BpmAttachmentTypeEnum.TASK_ATTACHMENT);
  199 + // 断言
  200 + assertEquals(List.of(attachment1), result);
  201 + assertEquals(3, taskService.getAttachments("process-instance-1", null, null).size());
  202 + }
  203 +
  204 + // ========== Update 写入相关方法 ==========
  205 +
  206 + @Test
  207 + public void testApproveTask_processInstanceNotExists() {
  208 + // 准备参数
  209 + BpmTaskServiceImpl spyService = spy(taskService);
  210 + doReturn(task).when(spyService).validateTask(1L, "task-1");
  211 + when(task.getProcessInstanceId()).thenReturn("process-instance-1");
  212 +
  213 + // 调用,并断言异常
  214 + assertServiceException(() -> spyService.approveTask(1L, new BpmTaskApproveReqVO().setId("task-1")),
  215 + PROCESS_INSTANCE_NOT_EXISTS);
  216 + }
  217 +
  218 + @Test
  219 + public void testApproveTask_signatureNotExists() {
  220 + // 准备参数
  221 + BpmTaskServiceImpl spyService = spy(taskService);
  222 + mockApproveTask(spyService, BpmnModelConstants.SIGN_ENABLE);
  223 +
  224 + // 调用,并断言异常
  225 + assertServiceException(() -> spyService.approveTask(1L, new BpmTaskApproveReqVO().setId("task-1")
  226 + .setReason("同意")), TASK_SIGNATURE_NOT_EXISTS);
  227 + }
  228 +
  229 + @Test
  230 + public void testApproveTask_reasonRequire() {
  231 + // 准备参数
  232 + BpmTaskServiceImpl spyService = spy(taskService);
  233 + mockApproveTask(spyService, BpmnModelConstants.REASON_REQUIRE);
  234 +
  235 + // 调用,并断言异常
  236 + assertServiceException(() -> spyService.approveTask(1L, new BpmTaskApproveReqVO().setId("task-1")),
  237 + TASK_REASON_REQUIRE);
  238 + }
  239 +
  240 + @Test
  241 + public void testApproveTask_success() {
  242 + // 准备参数:task1 开启签名,下一个节点 task2 为审批人自选;task1 曾被退回过
  243 + BpmTaskServiceImpl spyService = spy(taskService);
  244 + BpmnModel bpmnModel = mockApproveTaskSuccess(spyService, BpmTaskCandidateStrategyEnum.APPROVE_USER_SELECT,
  245 + Map.of("reason", "事假"));
  246 + BpmnModelUtils.addExtensionElement(bpmnModel.getFlowElement("task1"), BpmnModelConstants.SIGN_ENABLE, "true");
  247 + when(runtimeService.getVariable("process-instance-1",
  248 + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_TASK_IDS))
  249 + .thenReturn(new HashSet<>(Set.of("task1", "task2")));
  250 + String returnFlagKey = String.format(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, "task1");
  251 + when(runtimeService.hasVariable("process-instance-1", returnFlagKey)).thenReturn(true);
  252 + BpmTaskApproveReqVO reqVO = new BpmTaskApproveReqVO().setId("task-1").setReason("同意")
  253 + .setSignPicUrl("https://www.iocoder.cn/sign.png")
  254 + .setAttachments(List.of("https://www.iocoder.cn/files/a.pdf"))
  255 + .setVariables(Map.of("day", 3))
  256 + .setNextAssignees(Map.of("task2", List.of(2L)));
  257 +
  258 + // 调用
  259 + spyService.approveTask(1L, reqVO);
  260 + // 断言:任务状态、签名、评论、附件
  261 + verify(flowableTaskService).setVariableLocal("task-1", BpmnVariableConstants.TASK_VARIABLE_STATUS,
  262 + BpmTaskStatusEnum.APPROVE.getStatus());
  263 + verify(flowableTaskService).setVariableLocal("task-1", BpmnVariableConstants.TASK_VARIABLE_REASON, "同意");
  264 + verify(flowableTaskService).setVariableLocal("task-1", BpmnVariableConstants.TASK_SIGN_PIC_URL,
  265 + "https://www.iocoder.cn/sign.png");
  266 + verify(commentService).createComment("task-1", "process-instance-1", BpmCommentTypeEnum.APPROVE, "同意");
  267 + verify(flowableTaskService).createAttachment(BpmAttachmentTypeEnum.TASK_ATTACHMENT.getType(), "task-1",
  268 + "process-instance-1", "a.pdf", null, "https://www.iocoder.cn/files/a.pdf");
  269 + // 断言:流程变量合并历史变量与前端变量,并记录下一个节点的审批人
  270 + Map<String, Object> processVariables = captureProcessVariables();
  271 + assertEquals("事假", processVariables.get("reason"));
  272 + assertEquals(3, processVariables.get("day"));
  273 + assertEquals(Map.of("task2", List.of(2L)), processVariables.get(
  274 + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_APPROVE_USER_SELECT_ASSIGNEES));
  275 + // 断言:移除预测节点、清理退回标记,并完成任务
  276 + verify(runtimeService).setVariable("process-instance-1",
  277 + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_TASK_IDS, Set.of("task2"));
  278 + verify(runtimeService).removeVariable("process-instance-1", returnFlagKey);
  279 + verify(flowableTaskService).complete("task-1", Map.of("day", 3), true);
  280 + }
  281 +
  282 + @Test
  283 + public void testApproveTask_approveUserSelectNotConfig() {
  284 + // 准备参数:下一个节点 task2 为审批人自选,但未选择审批人
  285 + BpmTaskServiceImpl spyService = spy(taskService);
  286 + mockApproveTaskSuccess(spyService, BpmTaskCandidateStrategyEnum.APPROVE_USER_SELECT, Map.of());
  287 +
  288 + // 调用,并断言异常
  289 + assertServiceException(() -> spyService.approveTask(1L, new BpmTaskApproveReqVO().setId("task-1")
  290 + .setReason("同意")), PROCESS_INSTANCE_APPROVE_USER_SELECT_ASSIGNEES_NOT_CONFIG, "二级审批");
  291 + verify(flowableTaskService, never()).complete(anyString(), anyMap(), anyBoolean());
  292 + }
  293 +
  294 + @Test
  295 + public void testApproveTask_startUserSelectNotConfig() {
  296 + // 准备参数:下一个节点 task2 为发起人自选,但发起时、审批时都未选择审批人
  297 + BpmTaskServiceImpl spyService = spy(taskService);
  298 + mockApproveTaskSuccess(spyService, BpmTaskCandidateStrategyEnum.START_USER_SELECT, Map.of());
  299 +
  300 + // 调用,并断言异常
  301 + assertServiceException(() -> spyService.approveTask(1L, new BpmTaskApproveReqVO().setId("task-1")
  302 + .setReason("同意")), PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_CONFIG, "二级审批");
  303 + }
  304 +
  305 + @Test
  306 + public void testApproveTask_startUserSelectAlreadyConfig() {
  307 + // 准备参数:下一个节点 task2 为发起人自选,发起时已经选择审批人
  308 + BpmTaskServiceImpl spyService = spy(taskService);
  309 + Map<String, List<Long>> startUserSelectAssignees = Map.of("task2", List.of(2L));
  310 + mockApproveTaskSuccess(spyService, BpmTaskCandidateStrategyEnum.START_USER_SELECT, Map.of(
  311 + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES, startUserSelectAssignees));
  312 +
  313 + // 调用:审批时传入的审批人,不允许覆盖发起时选择的审批人
  314 + spyService.approveTask(1L, new BpmTaskApproveReqVO().setId("task-1").setReason("同意")
  315 + .setNextAssignees(Map.of("task2", List.of(3L))));
  316 + // 断言
  317 + assertEquals(startUserSelectAssignees, captureProcessVariables().get(
  318 + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES));
  319 + verify(flowableTaskService).complete("task-1", Map.of(), true);
  320 + verify(runtimeService, never()).removeVariable(anyString(), anyString());
  321 + }
  322 +
  323 + @Test
  324 + public void testMoveTaskToEnd_noRunningTask() {
  325 + // 准备参数
  326 + BpmTaskServiceImpl spyService = spy(taskService);
  327 + doReturn(List.of()).when(spyService).getRunningTaskListByProcessInstanceId("process-instance-1", null, null);
  328 +
  329 + // 调用
  330 + spyService.moveTaskToEnd("process-instance-1", "结束流程");
  331 + // 断言
  332 + verifyNoInteractions(runtimeService, modelService);
  333 + }
  334 +
  335 + @Test
  336 + public void testRejectTask_processInstanceNotExists() {
  337 + // 准备参数
  338 + BpmTaskServiceImpl spyService = spy(taskService);
  339 + doReturn(task).when(spyService).validateTask(1L, "task-1");
  340 + when(task.getProcessInstanceId()).thenReturn("process-instance-1");
  341 +
  342 + // 调用,并断言异常
  343 + assertServiceException(() -> spyService.rejectTask(1L, new BpmTaskRejectReqVO().setId("task-1")
  344 + .setReason("不同意")), PROCESS_INSTANCE_NOT_EXISTS);
  345 + }
  346 +
  347 + @Test
  348 + public void testReturnTask_pending() {
  349 + // 准备参数
  350 + BpmTaskServiceImpl spyService = spy(taskService);
  351 + doReturn(task).when(spyService).validateTask(1L, "task-1");
  352 + when(task.isSuspended()).thenReturn(true);
  353 +
  354 + // 调用,并断言异常
  355 + assertServiceException(() -> spyService.returnTask(1L, new BpmTaskReturnReqVO().setId("task-1")
  356 + .setTargetTaskDefinitionKey("task1").setReason("退回")), TASK_IS_PENDING);
  357 + }
  358 +
  359 + @Test
  360 + public void testDelegateTask_userNotExists() {
  361 + // 准备参数
  362 + BpmTaskServiceImpl spyService = spy(taskService);
  363 + doReturn(task).when(spyService).validateTask(1L, "task-1");
  364 + when(task.getAssignee()).thenReturn("1");
  365 +
  366 + // 调用,并断言异常
  367 + assertServiceException(() -> spyService.delegateTask(1L, new BpmTaskDelegateReqVO().setId("task-1")
  368 + .setDelegateUserId(2L).setReason("委派")), TASK_DELEGATE_FAIL_USER_NOT_EXISTS);
  369 + }
  370 +
  371 + @Test
  372 + public void testDelegateTask_emptyAssignee_userNotExists() {
  373 + // 准备参数:自动审批或审批人为空时,任务可能没有 assignee
  374 + BpmTaskServiceImpl spyService = spy(taskService);
  375 + doReturn(task).when(spyService).validateTask(1L, "task-1");
  376 + when(task.getAssignee()).thenReturn(null);
  377 +
  378 + // 调用,并断言:不应因 assignee 为空触发空指针
  379 + assertServiceException(() -> spyService.delegateTask(1L, new BpmTaskDelegateReqVO().setId("task-1")
  380 + .setDelegateUserId(2L).setReason("委派")), TASK_DELEGATE_FAIL_USER_NOT_EXISTS);
  381 + }
  382 +
  383 + @Test
  384 + public void testTransferTask_userRepeat() {
  385 + // 准备参数
  386 + BpmTaskServiceImpl spyService = spy(taskService);
  387 + doReturn(task).when(spyService).validateTask(1L, "task-1");
  388 + when(task.getAssignee()).thenReturn("1");
  389 +
  390 + // 调用,并断言异常
  391 + assertServiceException(() -> spyService.transferTask(1L, new BpmTaskTransferReqVO().setId("task-1")
  392 + .setAssigneeUserId(1L).setReason("转办")), TASK_TRANSFER_FAIL_USER_REPEAT);
  393 + }
  394 +
  395 + @Test
  396 + public void testTransferTask_userNotExists() {
  397 + // 准备参数
  398 + BpmTaskServiceImpl spyService = spy(taskService);
  399 + doReturn(task).when(spyService).validateTask(1L, "task-1");
  400 + when(task.getAssignee()).thenReturn("1");
  401 +
  402 + // 调用,并断言异常
  403 + assertServiceException(() -> spyService.transferTask(1L, new BpmTaskTransferReqVO().setId("task-1")
  404 + .setAssigneeUserId(2L).setReason("转办")), TASK_TRANSFER_FAIL_USER_NOT_EXISTS);
  405 + }
  406 +
  407 + @Test
  408 + public void testTransferTask_alreadyHasOwner() {
  409 + // 准备参数
  410 + BpmTaskServiceImpl spyService = spy(taskService);
  411 + doReturn(task).when(spyService).validateTask(1L, "task-1");
  412 + when(task.getAssignee()).thenReturn("1");
  413 + when(task.getOwner()).thenReturn("3");
  414 + when(task.getProcessInstanceId()).thenReturn("process-instance-1");
  415 + when(adminUserApi.getUser(2L)).thenReturn(new AdminUserRespDTO().setId(2L).setNickname("李四"));
  416 + when(adminUserApi.getUser(1L)).thenReturn(new AdminUserRespDTO().setId(1L).setNickname("张三"));
  417 +
  418 + // 调用
  419 + spyService.transferTask(1L, new BpmTaskTransferReqVO().setId("task-1").setAssigneeUserId(2L).setReason("转办"));
  420 + // 断言:已经存在 owner 时,不覆盖 owner
  421 + verify(flowableTaskService, never()).setOwner(anyString(), anyString());
  422 + verify(flowableTaskService).setAssignee("task-1", "2");
  423 + verify(commentService).createComment(eq("task-1"), eq("process-instance-1"),
  424 + eq(BpmCommentTypeEnum.TRANSFER), eq("张三"), eq("李四"), eq("转办"));
  425 + }
  426 +
  427 + @Test
  428 + public void testCreateSignTask_userNotExists() {
  429 + // 准备参数
  430 + BpmTaskServiceImpl spyService = spy(taskService);
  431 + TaskEntityImpl taskEntity = buildTaskEntity("task-1", null);
  432 + taskEntity.setProcessInstanceId("process-instance-1");
  433 + taskEntity.setTaskDefinitionKey("task1");
  434 + doReturn(taskEntity).when(spyService).validateTask(1L, "task-1");
  435 + TaskQuery taskQuery = mock(TaskQuery.class, RETURNS_SELF);
  436 + when(flowableTaskService.createTaskQuery()).thenReturn(taskQuery);
  437 + when(taskQuery.list()).thenReturn(List.of());
  438 +
  439 + // 调用,并断言异常
  440 + assertServiceException(() -> spyService.createSignTask(1L, new BpmTaskSignCreateReqVO().setId("task-1")
  441 + .setType(BpmTaskSignTypeEnum.BEFORE.getType()).setUserIds(Set.of(2L)).setReason("加签")),
  442 + TASK_SIGN_CREATE_USER_NOT_EXIST);
  443 + }
  444 +
  445 + @Test
  446 + public void testDeleteSignTask_parentNotExists() {
  447 + // 准备参数
  448 + BpmTaskServiceImpl spyService = spy(taskService);
  449 + doReturn(task).when(spyService).validateTaskExists("task-1");
  450 + when(task.getParentTaskId()).thenReturn("parent-1");
  451 + doReturn(null).when(spyService).getTask("parent-1");
  452 +
  453 + // 调用,并断言异常
  454 + assertServiceException(() -> spyService.deleteSignTask(1L, new BpmTaskSignDeleteReqVO().setId("task-1")
  455 + .setReason("减签")), TASK_SIGN_DELETE_NO_PARENT);
  456 + }
  457 +
  458 + @Test
  459 + public void testDeleteSignTask_parentNotSign() {
  460 + // 准备参数
  461 + BpmTaskServiceImpl spyService = spy(taskService);
  462 + doReturn(task).when(spyService).validateTaskExists("task-1");
  463 + when(task.getParentTaskId()).thenReturn("parent-1");
  464 + doReturn(buildTaskEntity("parent-1", null)).when(spyService).getTask("parent-1");
  465 +
  466 + // 调用,并断言异常
  467 + assertServiceException(() -> spyService.deleteSignTask(1L, new BpmTaskSignDeleteReqVO().setId("task-1")
  468 + .setReason("减签")), TASK_SIGN_DELETE_NO_PARENT);
  469 + }
  470 +
  471 + @Test
  472 + public void testCopyTask_success() {
  473 + // 准备参数
  474 + BpmTaskCopyReqVO reqVO = new BpmTaskCopyReqVO().setId("task-1").setCopyUserIds(Set.of(2L)).setReason("抄送");
  475 +
  476 + // 调用
  477 + taskService.copyTask(1L, reqVO);
  478 + // 断言
  479 + verify(processInstanceCopyService).createProcessInstanceCopy(Set.of(2L), "抄送", "task-1");
  480 + }
  481 +
  482 + @Test
  483 + public void testWithdrawTask_processNotRunning() {
  484 + // 准备参数
  485 + HistoricTaskInstance historicTask = mockWithdrawHistoricTask("task1");
  486 + when(historicTask.getProcessInstanceId()).thenReturn("process-instance-1");
  487 +
  488 + // 调用,并断言异常
  489 + assertServiceException(() -> taskService.withdrawTask(1L, "task-1"), TASK_WITHDRAW_FAIL_PROCESS_NOT_RUNNING);
  490 + }
  491 +
  492 + @Test
  493 + public void testWithdrawTask_noNextUserTask() {
  494 + // 准备参数:task2 已经是最后一个审批节点
  495 + HistoricTaskInstance historicTask = mockWithdrawHistoricTask("task2");
  496 + when(historicTask.getProcessInstanceId()).thenReturn("process-instance-1");
  497 + when(historicTask.getProcessDefinitionId()).thenReturn("definition-1");
  498 + when(processInstanceService.getProcessInstance("process-instance-1")).thenReturn(processInstance);
  499 + when(processInstance.getProcessDefinitionId()).thenReturn("definition-1");
  500 + when(processDefinitionService.getProcessDefinitionInfo("definition-1"))
  501 + .thenReturn(new BpmProcessDefinitionInfoDO().setAllowWithdrawTask(true));
  502 + when(modelService.getBpmnModelByDefinitionId("definition-1")).thenReturn(buildTwoStepBpmnModel());
  503 +
  504 + // 调用,并断言异常
  505 + assertServiceException(() -> taskService.withdrawTask(1L, "task-1"), TASK_WITHDRAW_FAIL_NEXT_TASK_NOT_ALLOW);
  506 + }
  507 +
  508 + // ========== Event 事件相关方法 ==========
  509 +
  510 + @Test
  511 + public void testProcessTaskCreated_statusExists() {
  512 + // 准备参数
  513 + when(task.getTaskLocalVariables()).thenReturn(Map.of(BpmnVariableConstants.TASK_VARIABLE_STATUS,
  514 + BpmTaskStatusEnum.RUNNING.getStatus()));
  515 +
  516 + // 调用
  517 + taskService.processTaskCreated(task);
  518 + // 断言
  519 + verify(flowableTaskService, never()).setVariableLocal(any(), any(), any());
  520 + verifyNoInteractions(processInstanceService);
  521 + }
  522 +
  523 + @Test
  524 + public void testProcessTaskCreated_processInstanceNotExists() {
  525 + // 准备参数
  526 + mockTask("1");
  527 +
  528 + // 调用
  529 + taskService.processTaskCreated(task);
  530 + // 断言:状态更新为审批中,但不再处理后续逻辑
  531 + verify(flowableTaskService).setVariableLocal("task-1", BpmnVariableConstants.TASK_VARIABLE_STATUS,
  532 + BpmTaskStatusEnum.RUNNING.getStatus());
  533 + verifyNoInteractions(processDefinitionService, modelService);
  534 + }
  535 +
  536 + @Test
  537 + public void testProcessTaskCreated_processDefinitionNotExists() {
  538 + // 准备参数
  539 + mockTask("1");
  540 + when(processInstanceService.getProcessInstance("process-instance-1")).thenReturn(processInstance);
  541 + when(processInstance.getProcessDefinitionId()).thenReturn("definition-1");
  542 +
  543 + // 调用
  544 + taskService.processTaskCreated(task);
  545 + // 断言
  546 + verify(processDefinitionService).getProcessDefinitionInfo("definition-1");
  547 + verifyNoInteractions(modelService);
  548 + }
  549 +
  550 + @Test
  551 + public void testProcessTaskCreated_userApproveHasAssignee() {
  552 + // 准备参数:人工审批且已有审批人,不自动处理
  553 + mockProcessTaskCreated("1", Map.of(BpmnModelConstants.USER_TASK_APPROVE_TYPE,
  554 + BpmUserTaskApproveTypeEnum.USER.getType(), BpmnModelConstants.USER_TASK_ASSIGN_EMPTY_HANDLER_TYPE,
  555 + BpmUserTaskAssignEmptyHandlerTypeEnum.APPROVE.getType()));
  556 +
  557 + // 调用
  558 + executeWithSynchronization(() -> taskService.processTaskCreated(task),
  559 + TransactionSynchronization.STATUS_COMMITTED);
  560 + // 断言
  561 + verifyNoInteractions(selfService);
  562 + }
  563 +
  564 + @Test
  565 + public void testProcessTaskCreated_rolledBack() {
  566 + // 准备参数
  567 + mockProcessTaskCreated("1", Map.of(BpmnModelConstants.USER_TASK_APPROVE_TYPE,
  568 + BpmUserTaskApproveTypeEnum.AUTO_APPROVE.getType()));
  569 +
  570 + // 调用
  571 + executeWithSynchronization(() -> taskService.processTaskCreated(task),
  572 + TransactionSynchronization.STATUS_ROLLED_BACK);
  573 + // 断言
  574 + verifyNoInteractions(selfService);
  575 + }
  576 +
  577 + @Test
  578 + public void testProcessTaskCreated_taskBeforeTrigger() {
  579 + // 准备参数:配置任务前置通知
  580 + BpmModelMetaInfoVO.HttpRequestSetting setting = new BpmModelMetaInfoVO.HttpRequestSetting()
  581 + .setUrl("http://127.0.0.1/task-before");
  582 + mockProcessTaskCreated("1", Map.of(BpmnModelConstants.USER_TASK_APPROVE_TYPE,
  583 + BpmUserTaskApproveTypeEnum.USER.getType()),
  584 + new BpmProcessDefinitionInfoDO().setTaskBeforeTriggerSetting(setting));
  585 +
  586 + // 调用
  587 + try (MockedStatic<BpmHttpRequestUtils> httpRequestUtilsMockedStatic = mockStatic(BpmHttpRequestUtils.class)) {
  588 + executeWithSynchronization(() -> taskService.processTaskCreated(task),
  589 + TransactionSynchronization.STATUS_COMMITTED);
  590 + // 断言
  591 + httpRequestUtilsMockedStatic.verify(() -> BpmHttpRequestUtils.executeBpmHttpRequest(processInstance,
  592 + "http://127.0.0.1/task-before", null, null, true, null));
  593 + }
  594 + verifyNoInteractions(selfService);
  595 + }
  596 +
  597 + @Test
  598 + public void testProcessTaskCreated_unknownStatusTaskNotExists() {
  599 + // 准备参数:事务状态未知,且任务已经不存在(例如说,已被自动审批)
  600 + BpmTaskServiceImpl spyService = spy(taskService);
  601 + mockProcessTaskCreated("1", Map.of(BpmnModelConstants.USER_TASK_APPROVE_TYPE,
  602 + BpmUserTaskApproveTypeEnum.AUTO_APPROVE.getType()));
  603 + doReturn(null).when(spyService).getTask("task-1");
  604 +
  605 + // 调用
  606 + executeWithSynchronization(() -> spyService.processTaskCreated(task),
  607 + TransactionSynchronization.STATUS_UNKNOWN);
  608 + // 断言
  609 + verifyNoInteractions(selfService);
  610 + }
  611 +
  612 + @Test
  613 + public void testProcessTaskCreated_unknownStatusTaskExists() {
  614 + // 准备参数:事务状态未知,但任务仍然存在
  615 + BpmTaskServiceImpl spyService = spy(taskService);
  616 + mockProcessTaskCreated("1", Map.of(BpmnModelConstants.USER_TASK_APPROVE_TYPE,
  617 + BpmUserTaskApproveTypeEnum.AUTO_APPROVE.getType()));
  618 + doReturn(task).when(spyService).getTask("task-1");
  619 +
  620 + // 调用
  621 + executeWithSynchronization(() -> spyService.processTaskCreated(task),
  622 + TransactionSynchronization.STATUS_UNKNOWN);
  623 + // 断言:仍然执行自动审批
  624 + assertEquals(BpmReasonEnum.APPROVE_TYPE_AUTO_APPROVE.getReason(), captureApproveTaskInternal(null).getReason());
  625 + }
  626 +
  627 + @Test
  628 + public void testProcessTaskCanceled_notExists() {
  629 + // 准备参数
  630 + BpmTaskServiceImpl spyService = spy(taskService);
  631 + doReturn(null).when(spyService).getTask("task-1");
  632 +
  633 + // 调用
  634 + spyService.processTaskCanceled("task-1");
  635 + // 断言
  636 + verify(flowableTaskService, never()).setVariableLocal(any(), any(), any());
  637 + }
  638 +
  639 + @Test
  640 + public void testProcessTaskCanceled_endStatus() {
  641 + // 准备参数
  642 + BpmTaskServiceImpl spyService = spy(taskService);
  643 + doReturn(task).when(spyService).getTask("task-1");
  644 + when(task.getTaskLocalVariables()).thenReturn(Map.of(BpmnVariableConstants.TASK_VARIABLE_STATUS,
  645 + BpmTaskStatusEnum.APPROVE.getStatus()));
  646 +
  647 + // 调用
  648 + spyService.processTaskCanceled("task-1");
  649 + // 断言
  650 + verify(flowableTaskService, never()).setVariableLocal(any(), any(), any());
  651 + }
  652 +
  653 + @Test
  654 + public void testProcessTaskCanceled_success() {
  655 + // 准备参数
  656 + BpmTaskServiceImpl spyService = spy(taskService);
  657 + doReturn(task).when(spyService).getTask("task-1");
  658 + when(task.getTaskLocalVariables()).thenReturn(Map.of(BpmnVariableConstants.TASK_VARIABLE_STATUS,
  659 + BpmTaskStatusEnum.RUNNING.getStatus()));
  660 +
  661 + // 调用
  662 + spyService.processTaskCanceled("task-1");
  663 + // 断言
  664 + verify(flowableTaskService).setVariableLocal("task-1", BpmnVariableConstants.TASK_VARIABLE_STATUS,
  665 + BpmTaskStatusEnum.CANCEL.getStatus());
  666 + verify(flowableTaskService).setVariableLocal("task-1", BpmnVariableConstants.TASK_VARIABLE_REASON,
  667 + BpmReasonEnum.CANCEL_BY_SYSTEM.getReason());
  668 + }
  669 +
  670 + @Test
  671 + public void testProcessTaskAssigned_noAssignee() {
  672 + // 准备参数
  673 + when(task.getAssignee()).thenReturn(null);
  674 +
  675 + // 调用
  676 + executeWithSynchronization(() -> taskService.processTaskAssigned(task),
  677 + TransactionSynchronization.STATUS_COMMITTED);
  678 + // 断言
  679 + verifyNoInteractions(processInstanceService, messageService, selfService);
  680 + }
  681 +
  682 + @Test
  683 + public void testProcessTaskAssigned_rolledBack() {
  684 + // 调用
  685 + executeWithSynchronization(() -> taskService.processTaskAssigned(task),
  686 + TransactionSynchronization.STATUS_ROLLED_BACK);
  687 + // 断言
  688 + verifyNoInteractions(task, processInstanceService, messageService, selfService);
  689 + }
  690 +
  691 + @Test
  692 + public void testProcessTaskAssigned_unknownStatusTaskNotExists() {
  693 + // 准备参数:事务状态未知,且任务已经不存在
  694 + BpmTaskServiceImpl spyService = spy(taskService);
  695 + when(task.getId()).thenReturn("task-1");
  696 + doReturn(null).when(spyService).getTask("task-1");
  697 +
  698 + // 调用
  699 + executeWithSynchronization(() -> spyService.processTaskAssigned(task),
  700 + TransactionSynchronization.STATUS_UNKNOWN);
  701 + // 断言
  702 + verifyNoInteractions(processInstanceService, messageService, selfService);
  703 + }
  704 +
  705 + @Test
  706 + public void testProcessTaskAssigned_processInstanceNotExists() {
  707 + // 准备参数
  708 + when(task.getAssignee()).thenReturn("1");
  709 + when(task.getProcessInstanceId()).thenReturn("process-instance-1");
  710 +
  711 + // 调用
  712 + executeWithSynchronization(() -> taskService.processTaskAssigned(task),
  713 + TransactionSynchronization.STATUS_COMMITTED);
  714 + // 断言
  715 + verifyNoInteractions(processDefinitionService, messageService, selfService);
  716 + }
  717 +
  718 + @Test
  719 + public void testProcessTaskAssigned_startUserSkipWhenReturn() {
  720 + // 准备参数:退回到该节点时,不自动跳过,而是发送通知
  721 + mockProcessTaskAssigned("task2", "1", null);
  722 + when(processInstance.getStartUserId()).thenReturn("1");
  723 + when(processInstance.getProcessInstanceId()).thenReturn("process-instance-1");
  724 + when(runtimeService.getVariable("process-instance-1",
  725 + String.format(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, "task2"), Boolean.class))
  726 + .thenReturn(true);
  727 + when(modelService.getBpmnModelByDefinitionId("definition-1")).thenReturn(buildBpmnModel("task2",
  728 + Map.of(BpmnModelConstants.USER_TASK_ASSIGN_START_USER_HANDLER_TYPE,
  729 + BpmUserTaskAssignStartUserHandlerTypeEnum.SKIP.getType())));
  730 + when(adminUserApi.getUser(1L)).thenReturn(new AdminUserRespDTO().setId(1L).setNickname("张三"));
  731 +
  732 + // 调用
  733 + executeWithSynchronization(() -> taskService.processTaskAssigned(task),
  734 + TransactionSynchronization.STATUS_COMMITTED);
  735 + // 断言
  736 + verifyNoInteractions(selfService);
  737 + verify(messageService).sendMessageWhenTaskAssigned(any(BpmMessageSendWhenTaskCreatedReqDTO.class));
  738 + }
  739 +
  740 + @Test
  741 + public void testProcessTaskCompleted_processInstanceNotExists() {
  742 + // 准备参数
  743 + when(task.getProcessInstanceId()).thenReturn("process-instance-1");
  744 +
  745 + // 调用
  746 + taskService.processTaskCompleted(task);
  747 + // 断言
  748 + verifyNoInteractions(processDefinitionService);
  749 + }
  750 +
  751 + @Test
  752 + public void testProcessTaskCompleted_noTriggerSetting() {
  753 + // 准备参数
  754 + when(task.getProcessInstanceId()).thenReturn("process-instance-1");
  755 + when(processInstanceService.getProcessInstance("process-instance-1")).thenReturn(processInstance);
  756 + when(processInstance.getProcessDefinitionId()).thenReturn("definition-1");
  757 + when(processDefinitionService.getProcessDefinitionInfo("definition-1"))
  758 + .thenReturn(new BpmProcessDefinitionInfoDO());
  759 +
  760 + // 调用
  761 + taskService.processTaskCompleted(task);
  762 + // 断言
  763 + verify(processDefinitionService).getProcessDefinitionInfo("definition-1");
  764 + }
  765 +
  766 + @Test
  767 + public void testProcessTaskCompleted_taskAfterTrigger() {
  768 + // 准备参数:配置任务后置通知
  769 + when(task.getProcessInstanceId()).thenReturn("process-instance-1");
  770 + when(processInstanceService.getProcessInstance("process-instance-1")).thenReturn(processInstance);
  771 + when(processInstance.getProcessDefinitionId()).thenReturn("definition-1");
  772 + BpmModelMetaInfoVO.HttpRequestSetting setting = new BpmModelMetaInfoVO.HttpRequestSetting()
  773 + .setUrl("http://127.0.0.1/task-after");
  774 + when(processDefinitionService.getProcessDefinitionInfo("definition-1"))
  775 + .thenReturn(new BpmProcessDefinitionInfoDO().setTaskAfterTriggerSetting(setting));
  776 +
  777 + // 调用
  778 + try (MockedStatic<BpmHttpRequestUtils> httpRequestUtilsMockedStatic = mockStatic(BpmHttpRequestUtils.class)) {
  779 + taskService.processTaskCompleted(task);
  780 + // 断言
  781 + httpRequestUtilsMockedStatic.verify(() -> BpmHttpRequestUtils.executeBpmHttpRequest(processInstance,
  782 + "http://127.0.0.1/task-after", null, null, true, null));
  783 + }
  784 + }
  785 +
  786 + @Test
  787 + public void testProcessTaskTimeout_processInstanceNotExists() {
  788 + // 调用
  789 + taskService.processTaskTimeout("process-instance-1", "task1",
  790 + BpmUserTaskTimeoutHandlerTypeEnum.REMINDER.getType());
  791 + // 断言
  792 + verifyNoInteractions(flowableTaskService, messageService);
  793 + }
  794 +
  795 + @Test
  796 + public void testProcessTaskTimeout_taskNotExists() {
  797 + // 准备参数
  798 + BpmTaskServiceImpl spyService = spy(taskService);
  799 + when(processInstanceService.getProcessInstance("process-instance-1")).thenReturn(processInstance);
  800 + doReturn(List.of()).when(spyService).getRunningTaskListByProcessInstanceId("process-instance-1", true, "task1");
  801 +
  802 + // 调用
  803 + spyService.processTaskTimeout("process-instance-1", "task1",
  804 + BpmUserTaskTimeoutHandlerTypeEnum.REMINDER.getType());
  805 + // 断言
  806 + verifyNoInteractions(messageService);
  807 + }
  808 +
  809 + // ========== 私有方法 ==========
  810 +
  811 + private static TaskEntityImpl buildTaskEntity(String id, String parentTaskId) {
  812 + TaskEntityImpl taskEntity = new TaskEntityImpl();
  813 + taskEntity.setId(id);
  814 + taskEntity.setParentTaskId(parentTaskId);
  815 + return taskEntity;
  816 + }
  817 +
  818 + private static Attachment buildAttachment(String taskId, String type) {
  819 + AttachmentEntityImpl attachment = new AttachmentEntityImpl();
  820 + attachment.setTaskId(taskId);
  821 + attachment.setType(type);
  822 + return attachment;
  823 + }
  824 +
  825 + /**
  826 + * 构建 task1 -> task2 的两级审批流程模型
  827 + */
  828 + private static BpmnModel buildTwoStepBpmnModel() {
  829 + return BpmnModelUtils.getBpmnModel(ResourceUtil.readBytes("bpmn/two-step-approve.bpmn20.xml"));
  830 + }
  831 +
  832 + /**
  833 + * 构建只有一个 UserTask 的流程模型
  834 + */
  835 + private static BpmnModel buildBpmnModel(String userTaskId, Map<String, Integer> extensions) {
  836 + UserTask userTask = new UserTask();
  837 + userTask.setId(userTaskId);
  838 + extensions.forEach((name, value) -> BpmnModelUtils.addExtensionElement(userTask, name, value));
  839 + Process process = new Process();
  840 + process.setId("process");
  841 + process.addFlowElement(userTask);
  842 + BpmnModel bpmnModel = new BpmnModel();
  843 + bpmnModel.addProcess(process);
  844 + return bpmnModel;
  845 + }
  846 +
  847 + private void mockApproveTask(BpmTaskServiceImpl spyService, String extensionName) {
  848 + doReturn(task).when(spyService).validateTask(1L, "task-1");
  849 + when(task.getProcessInstanceId()).thenReturn("process-instance-1");
  850 + when(task.getProcessDefinitionId()).thenReturn("definition-1");
  851 + when(task.getTaskDefinitionKey()).thenReturn("task1");
  852 + when(processInstanceService.getProcessInstance("process-instance-1")).thenReturn(processInstance);
  853 + BpmnModel bpmnModel = buildTwoStepBpmnModel();
  854 + BpmnModelUtils.addExtensionElement(bpmnModel.getFlowElement("task1"), extensionName, "true");
  855 + when(modelService.getBpmnModelByDefinitionId("definition-1")).thenReturn(bpmnModel);
  856 + }
  857 +
  858 + /**
  859 + * 模拟审批 task1:下一个节点 task2 使用指定的审批人策略
  860 + */
  861 + private BpmnModel mockApproveTaskSuccess(BpmTaskServiceImpl spyService, BpmTaskCandidateStrategyEnum task2Strategy,
  862 + Map<String, Object> processVariables) {
  863 + doReturn(task).when(spyService).validateTask(1L, "task-1");
  864 + when(task.getId()).thenReturn("task-1");
  865 + when(task.getProcessInstanceId()).thenReturn("process-instance-1");
  866 + when(task.getProcessDefinitionId()).thenReturn("definition-1");
  867 + when(task.getTaskDefinitionKey()).thenReturn("task1");
  868 + when(processInstanceService.getProcessInstance("process-instance-1")).thenReturn(processInstance);
  869 + when(processInstance.getProcessVariables()).thenReturn(processVariables);
  870 + BpmnModel bpmnModel = buildTwoStepBpmnModel();
  871 + FlowElement task2 = bpmnModel.getFlowElement("task2");
  872 + task2.getExtensionElements().remove(BpmnModelConstants.USER_TASK_CANDIDATE_STRATEGY);
  873 + BpmnModelUtils.addExtensionElement(task2, BpmnModelConstants.USER_TASK_CANDIDATE_STRATEGY,
  874 + task2Strategy.getStrategy());
  875 + when(modelService.getBpmnModelByDefinitionId("definition-1")).thenReturn(bpmnModel);
  876 + return bpmnModel;
  877 + }
  878 +
  879 + @SuppressWarnings("unchecked")
  880 + private Map<String, Object> captureProcessVariables() {
  881 + ArgumentCaptor<Map<String, Object>> variablesCaptor = ArgumentCaptor.forClass(Map.class);
  882 + verify(runtimeService).setVariables(eq("process-instance-1"), variablesCaptor.capture());
  883 + return variablesCaptor.getValue();
  884 + }
  885 +
  886 + private HistoricTaskInstance mockWithdrawHistoricTask(String taskDefinitionKey) {
  887 + HistoricTaskInstanceQuery historicTaskQuery = mockHistoricTaskQuery();
  888 + HistoricTaskInstance historicTask = mock(HistoricTaskInstance.class);
  889 + lenient().when(historicTask.getTaskDefinitionKey()).thenReturn(taskDefinitionKey);
  890 + when(historicTaskQuery.singleResult()).thenReturn(historicTask);
  891 + return historicTask;
  892 + }
  893 +
  894 + private HistoricTaskInstanceQuery mockHistoricTaskQuery() {
  895 + HistoricTaskInstanceQuery historicTaskQuery = mock(HistoricTaskInstanceQuery.class, RETURNS_SELF);
  896 + when(historyService.createHistoricTaskInstanceQuery()).thenReturn(historicTaskQuery);
  897 + return historicTaskQuery;
  898 + }
  899 +
  900 + private void mockTask(String assignee) {
  901 + when(task.getId()).thenReturn("task-1");
  902 + when(task.getProcessInstanceId()).thenReturn("process-instance-1");
  903 + when(task.getTaskLocalVariables()).thenReturn(new HashMap<>());
  904 + lenient().when(task.getAssignee()).thenReturn(assignee);
  905 + }
  906 +
  907 + private void mockProcessTaskCreated(String assignee, Map<String, Integer> extensions) {
  908 + mockProcessTaskCreated(assignee, extensions, new BpmProcessDefinitionInfoDO());
  909 + }
  910 +
  911 + private void mockProcessTaskCreated(String assignee, Map<String, Integer> extensions,
  912 + BpmProcessDefinitionInfoDO processDefinitionInfo) {
  913 + mockTask(assignee);
  914 + when(task.getTaskDefinitionKey()).thenReturn("task1");
  915 + when(processInstanceService.getProcessInstance("process-instance-1")).thenReturn(processInstance);
  916 + when(processInstance.getProcessDefinitionId()).thenReturn("definition-1");
  917 + when(processDefinitionService.getProcessDefinitionInfo("definition-1")).thenReturn(processDefinitionInfo);
  918 + when(modelService.getBpmnModelByDefinitionId("definition-1")).thenReturn(buildBpmnModel("task1", extensions));
  919 + }
  920 +
  921 + private void mockProcessTaskAssigned(String taskDefinitionKey, String assignee, Integer autoApprovalType) {
  922 + when(task.getId()).thenReturn("task-1");
  923 + when(task.getAssignee()).thenReturn(assignee);
  924 + when(task.getProcessInstanceId()).thenReturn("process-instance-1");
  925 + when(task.getProcessDefinitionId()).thenReturn("definition-1");
  926 + lenient().when(task.getTaskDefinitionKey()).thenReturn(taskDefinitionKey);
  927 + when(processInstanceService.getProcessInstance("process-instance-1")).thenReturn(processInstance);
  928 + lenient().when(processInstance.getProcessDefinitionId()).thenReturn("definition-1");
  929 + when(processDefinitionService.getProcessDefinitionInfo("definition-1"))
  930 + .thenReturn(new BpmProcessDefinitionInfoDO().setAutoApprovalType(autoApprovalType));
  931 + }
  932 +
  933 + /**
  934 + * 在事务同步上下文中执行,并模拟事务完成回调
  935 + */
  936 + private static void executeWithSynchronization(Runnable runnable, int transactionStatus) {
  937 + TransactionSynchronizationManager.initSynchronization();
  938 + try {
  939 + runnable.run();
  940 + TransactionSynchronizationManager.getSynchronizations()
  941 + .forEach(synchronization -> synchronization.afterCompletion(transactionStatus));
  942 + } finally {
  943 + TransactionSynchronizationManager.clearSynchronization();
  944 + }
  945 + }
  946 +
  947 + private BpmTaskApproveReqVO captureApproveTaskInternal(Long userId) {
  948 + ArgumentCaptor<BpmTaskApproveReqVO> reqVOCaptor = ArgumentCaptor.forClass(BpmTaskApproveReqVO.class);
  949 + verify(selfService).approveTaskInternal(eq(userId), reqVOCaptor.capture());
  950 + return reqVOCaptor.getValue();
  951 + }
  952 +
  953 +}
... ...
yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmUserTaskListenerTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.bpm.service.task.listener;
  2 +
  3 +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
  4 +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
  5 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmHttpRequestUtils;
  6 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
  7 +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
  8 +import org.flowable.common.engine.api.delegate.Expression;
  9 +import org.flowable.engine.runtime.ProcessInstance;
  10 +import org.flowable.task.service.delegate.DelegateTask;
  11 +import org.junit.jupiter.api.Test;
  12 +import org.mockito.ArgumentCaptor;
  13 +import org.mockito.InjectMocks;
  14 +import org.mockito.Mock;
  15 +import org.mockito.MockedStatic;
  16 +
  17 +import java.util.List;
  18 +
  19 +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
  20 +import static org.junit.jupiter.api.Assertions.assertEquals;
  21 +import static org.mockito.ArgumentMatchers.*;
  22 +import static org.mockito.Mockito.*;
  23 +
  24 +/**
  25 + * {@link BpmUserTaskListener} 的单元测试
  26 + *
  27 + * @author HUIHUI
  28 + */
  29 +public class BpmUserTaskListenerTest extends BaseMockitoUnitTest {
  30 +
  31 + @InjectMocks
  32 + private BpmUserTaskListener userTaskListener;
  33 +
  34 + @Mock
  35 + private BpmProcessInstanceService processInstanceService;
  36 + @Mock
  37 + private Expression listenerConfig;
  38 +
  39 + @Test
  40 + @SuppressWarnings("unchecked")
  41 + public void testNotify_bodyNull() {
  42 + // mock 方法:监听器未配置 body
  43 + ProcessInstance processInstance = mock(ProcessInstance.class);
  44 + when(processInstanceService.getProcessInstance("process-instance-1")).thenReturn(processInstance);
  45 + BpmSimpleModelNodeVO.ListenerHandler listenerHandler = new BpmSimpleModelNodeVO.ListenerHandler()
  46 + .setPath("http://127.0.0.1/listener");
  47 + // 准备参数
  48 + DelegateTask delegateTask = mock(DelegateTask.class);
  49 + when(delegateTask.getProcessInstanceId()).thenReturn("process-instance-1");
  50 + when(delegateTask.getAssignee()).thenReturn("1");
  51 + when(delegateTask.getTaskDefinitionKey()).thenReturn("task1");
  52 + when(delegateTask.getId()).thenReturn("task-1");
  53 +
  54 + try (MockedStatic<BpmnModelUtils> bpmnModelUtilsMockedStatic = mockStatic(BpmnModelUtils.class);
  55 + MockedStatic<BpmHttpRequestUtils> httpRequestUtilsMockedStatic = mockStatic(BpmHttpRequestUtils.class)) {
  56 + bpmnModelUtilsMockedStatic.when(() -> BpmnModelUtils.parseListenerConfig(listenerConfig))
  57 + .thenReturn(listenerHandler);
  58 +
  59 + // 调用
  60 + userTaskListener.notify(delegateTask);
  61 +
  62 + // 断言:body 为空时不会空指针,并追加任务相关的固定参数
  63 + ArgumentCaptor<List<BpmSimpleModelNodeVO.HttpRequestParam>> bodyCaptor = ArgumentCaptor.forClass(List.class);
  64 + httpRequestUtilsMockedStatic.verify(() -> BpmHttpRequestUtils.executeBpmHttpRequest(same(processInstance),
  65 + eq("http://127.0.0.1/listener"), isNull(), bodyCaptor.capture(), eq(false), isNull()));
  66 + List<BpmSimpleModelNodeVO.HttpRequestParam> body = bodyCaptor.getValue();
  67 + assertEquals(List.of("processInstanceId", "assignee", "taskDefinitionKey", "taskId"),
  68 + convertList(body, BpmSimpleModelNodeVO.HttpRequestParam::getKey));
  69 + assertEquals(List.of("process-instance-1", "1", "task1", "task-1"),
  70 + convertList(body, BpmSimpleModelNodeVO.HttpRequestParam::getValue));
  71 + }
  72 + }
  73 +
  74 +}
... ...
yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/task/trigger/http/BpmHttpCallbackTriggerTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.bpm.service.task.trigger.http;
  2 +
  3 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  4 +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
  5 +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
  6 +import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmHttpRequestUtils;
  7 +import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
  8 +import org.flowable.engine.runtime.ProcessInstance;
  9 +import org.junit.jupiter.api.Test;
  10 +import org.mockito.ArgumentCaptor;
  11 +import org.mockito.InjectMocks;
  12 +import org.mockito.Mock;
  13 +import org.mockito.MockedStatic;
  14 +
  15 +import java.util.List;
  16 +
  17 +import static org.junit.jupiter.api.Assertions.assertEquals;
  18 +import static org.mockito.ArgumentMatchers.*;
  19 +import static org.mockito.Mockito.*;
  20 +
  21 +/**
  22 + * {@link BpmHttpCallbackTrigger} 的单元测试
  23 + *
  24 + * @author HUIHUI
  25 + */
  26 +public class BpmHttpCallbackTriggerTest extends BaseMockitoUnitTest {
  27 +
  28 + @InjectMocks
  29 + private BpmHttpCallbackTrigger httpCallbackTrigger;
  30 +
  31 + @Mock
  32 + private BpmProcessInstanceService processInstanceService;
  33 +
  34 + @Test
  35 + @SuppressWarnings("unchecked")
  36 + public void testExecute_bodyNull() {
  37 + // mock 方法
  38 + ProcessInstance processInstance = mock(ProcessInstance.class);
  39 + when(processInstanceService.getProcessInstance("process-instance-1")).thenReturn(processInstance);
  40 + // 准备参数:回调配置未配置 body
  41 + BpmSimpleModelNodeVO.TriggerSetting.HttpRequestTriggerSetting setting =
  42 + new BpmSimpleModelNodeVO.TriggerSetting.HttpRequestTriggerSetting();
  43 + setting.setUrl("http://127.0.0.1/callback");
  44 + setting.setCallbackTaskDefineKey("task1");
  45 +
  46 + try (MockedStatic<BpmHttpRequestUtils> httpRequestUtilsMockedStatic = mockStatic(BpmHttpRequestUtils.class)) {
  47 + // 调用
  48 + httpCallbackTrigger.execute("process-instance-1", JsonUtils.toJsonString(setting));
  49 +
  50 + // 断言:body 为空时不会空指针,并追加回调的 taskDefineKey
  51 + ArgumentCaptor<List<BpmSimpleModelNodeVO.HttpRequestParam>> bodyCaptor = ArgumentCaptor.forClass(List.class);
  52 + httpRequestUtilsMockedStatic.verify(() -> BpmHttpRequestUtils.executeBpmHttpRequest(same(processInstance),
  53 + eq("http://127.0.0.1/callback"), isNull(), bodyCaptor.capture(), eq(false), isNull()));
  54 + List<BpmSimpleModelNodeVO.HttpRequestParam> body = bodyCaptor.getValue();
  55 + assertEquals(1, body.size());
  56 + assertEquals("taskDefineKey", body.get(0).getKey());
  57 + assertEquals("task1", body.get(0).getValue());
  58 + }
  59 + }
  60 +
  61 +}
... ...
yudao-module-bpm/src/test/resources/application-unit-test.yaml
... ... @@ -9,7 +9,7 @@ spring:
9 9 # 数据源配置项
10 10 datasource:
11 11 name: ruoyi-vue-pro
12   - url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写
  12 + url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value,day; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写
13 13 driver-class-name: org.h2.Driver
14 14 username: sa
15 15 password:
... ...
yudao-module-bpm/src/test/resources/bpmn/call-activity.bpmn20.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
  3 + xmlns:flowable="http://flowable.org/bpmn"
  4 + targetNamespace="http://www.yudao.iocoder.cn/bpm">
  5 + <!-- 主流程:通过 callActivity 发起 twoStepApprove 子流程,子流程挂载不中断的超时边界事件 -->
  6 + <process id="callActivityParent" name="主流程" isExecutable="true">
  7 + <startEvent id="start"/>
  8 + <sequenceFlow id="flow-start" sourceRef="start" targetRef="child"/>
  9 + <callActivity id="child" name="子流程" calledElement="twoStepApprove"/>
  10 + <boundaryEvent id="child-timeout" attachedToRef="child" cancelActivity="false">
  11 + <extensionElements>
  12 + <flowable:boundaryEventType>3</flowable:boundaryEventType>
  13 + </extensionElements>
  14 + <timerEventDefinition>
  15 + <timeDuration>PT1H</timeDuration>
  16 + </timerEventDefinition>
  17 + </boundaryEvent>
  18 + <sequenceFlow id="flow-child" sourceRef="child" targetRef="end"/>
  19 + <endEvent id="end"/>
  20 + </process>
  21 +</definitions>
... ...
yudao-module-bpm/src/test/resources/bpmn/delay-timer.bpmn20.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
  3 + xmlns:flowable="http://flowable.org/bpmn"
  4 + targetNamespace="http://www.yudao.iocoder.cn/bpm">
  5 + <!-- 延迟器:receiveTask 挂载不中断的定时边界事件,到期后触发 receiveTask 继续流转 -->
  6 + <process id="delayTimer" name="延迟器" isExecutable="true">
  7 + <startEvent id="start"/>
  8 + <sequenceFlow id="flow-start" sourceRef="start" targetRef="wait"/>
  9 + <receiveTask id="wait" name="延迟器"/>
  10 + <boundaryEvent id="wait-timeout" attachedToRef="wait" cancelActivity="false">
  11 + <extensionElements>
  12 + <flowable:boundaryEventType>2</flowable:boundaryEventType>
  13 + </extensionElements>
  14 + <timerEventDefinition>
  15 + <timeDuration>PT1H</timeDuration>
  16 + </timerEventDefinition>
  17 + </boundaryEvent>
  18 + <sequenceFlow id="flow-wait" sourceRef="wait" targetRef="end"/>
  19 + <endEvent id="end"/>
  20 + </process>
  21 +</definitions>
... ...
yudao-module-bpm/src/test/resources/bpmn/lifecycle/assign-empty-approve.bpmn20.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
  3 + xmlns:flowable="http://flowable.org/bpmn"
  4 + targetNamespace="http://www.yudao.iocoder.cn/bpm">
  5 + <!-- 人工审批 + 审批人为空时自动通过 -->
  6 + <process id="assignEmptyApprove" name="审批人为空自动通过" isExecutable="true">
  7 + <startEvent id="start"/>
  8 + <sequenceFlow id="flow-start" sourceRef="start" targetRef="task"/>
  9 + <userTask id="task" name="审批">
  10 + <extensionElements>
  11 + <flowable:candidateStrategy>30</flowable:candidateStrategy>
  12 + <flowable:approveType>1</flowable:approveType>
  13 + <flowable:assignEmptyHandlerType>1</flowable:assignEmptyHandlerType>
  14 + </extensionElements>
  15 + </userTask>
  16 + <sequenceFlow id="flow-task" sourceRef="task" targetRef="end"/>
  17 + <endEvent id="end"/>
  18 + </process>
  19 +</definitions>
... ...
yudao-module-bpm/src/test/resources/bpmn/lifecycle/assign-empty-reject.bpmn20.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
  3 + xmlns:flowable="http://flowable.org/bpmn"
  4 + targetNamespace="http://www.yudao.iocoder.cn/bpm">
  5 + <!-- 人工审批 + 审批人为空时自动拒绝 -->
  6 + <process id="assignEmptyReject" name="审批人为空自动拒绝" isExecutable="true">
  7 + <startEvent id="start"/>
  8 + <sequenceFlow id="flow-start" sourceRef="start" targetRef="task"/>
  9 + <userTask id="task" name="审批">
  10 + <extensionElements>
  11 + <flowable:candidateStrategy>30</flowable:candidateStrategy>
  12 + <flowable:approveType>1</flowable:approveType>
  13 + <flowable:assignEmptyHandlerType>2</flowable:assignEmptyHandlerType>
  14 + </extensionElements>
  15 + </userTask>
  16 + <sequenceFlow id="flow-task" sourceRef="task" targetRef="end"/>
  17 + <endEvent id="end"/>
  18 + </process>
  19 +</definitions>
... ...
yudao-module-bpm/src/test/resources/bpmn/lifecycle/child-parent.bpmn20.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
  3 + xmlns:flowable="http://flowable.org/bpmn"
  4 + targetNamespace="http://www.yudao.iocoder.cn/bpm">
  5 + <!-- 主流程:子流程(发起人同主流程,由 bpmCallActivityListener 设置) -> 后续审批 -->
  6 + <process id="childParent" name="主流程" isExecutable="true">
  7 + <startEvent id="start"/>
  8 + <sequenceFlow id="flow-start" sourceRef="start" targetRef="child"/>
  9 + <callActivity id="child" name="子流程" calledElement="lifecycleChild" flowable:calledElementType="key">
  10 + <extensionElements>
  11 + <flowable:nodeType>20</flowable:nodeType>
  12 + <flowable:in source="PROCESS_STATUS" target="PROCESS_STATUS"/>
  13 + <flowable:executionListener event="start" delegateExpression="${bpmCallActivityListener}">
  14 + <flowable:field name="listenerConfig">
  15 + <flowable:string><![CDATA[{"type":1,"emptyType":1}]]></flowable:string>
  16 + </flowable:field>
  17 + </flowable:executionListener>
  18 + </extensionElements>
  19 + </callActivity>
  20 + <sequenceFlow id="flow-child" sourceRef="child" targetRef="afterChild"/>
  21 + <userTask id="afterChild" name="子流程后审批">
  22 + <extensionElements>
  23 + <flowable:candidateStrategy>30</flowable:candidateStrategy>
  24 + </extensionElements>
  25 + </userTask>
  26 + <sequenceFlow id="flow-after-child" sourceRef="afterChild" targetRef="end"/>
  27 + <endEvent id="end"/>
  28 + </process>
  29 +</definitions>
... ...
yudao-module-bpm/src/test/resources/bpmn/lifecycle/child.bpmn20.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
  3 + xmlns:flowable="http://flowable.org/bpmn"
  4 + targetNamespace="http://www.yudao.iocoder.cn/bpm">
  5 + <!-- 子流程:单个审批 -->
  6 + <process id="lifecycleChild" name="子流程" isExecutable="true">
  7 + <startEvent id="start"/>
  8 + <sequenceFlow id="flow-start" sourceRef="start" targetRef="task"/>
  9 + <userTask id="task" name="子流程审批">
  10 + <extensionElements>
  11 + <flowable:candidateStrategy>30</flowable:candidateStrategy>
  12 + </extensionElements>
  13 + </userTask>
  14 + <sequenceFlow id="flow-task" sourceRef="task" targetRef="end"/>
  15 + <endEvent id="end"/>
  16 + </process>
  17 +</definitions>
... ...
yudao-module-bpm/src/test/resources/bpmn/lifecycle/start-user-node.bpmn20.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
  3 + xmlns:flowable="http://flowable.org/bpmn"
  4 + targetNamespace="http://www.yudao.iocoder.cn/bpm">
  5 + <!-- 发起人节点(StartUserNode)首次自动通过 -->
  6 + <process id="startUserNode" name="发起人节点" isExecutable="true">
  7 + <startEvent id="start"/>
  8 + <sequenceFlow id="flow-start" sourceRef="start" targetRef="StartUserNode"/>
  9 + <userTask id="StartUserNode" name="发起人">
  10 + <extensionElements>
  11 + <flowable:candidateStrategy>36</flowable:candidateStrategy>
  12 + </extensionElements>
  13 + </userTask>
  14 + <sequenceFlow id="flow-start-user" sourceRef="StartUserNode" targetRef="task"/>
  15 + <userTask id="task" name="审批">
  16 + <extensionElements>
  17 + <flowable:candidateStrategy>30</flowable:candidateStrategy>
  18 + </extensionElements>
  19 + </userTask>
  20 + <sequenceFlow id="flow-task" sourceRef="task" targetRef="end"/>
  21 + <endEvent id="end"/>
  22 + </process>
  23 +</definitions>
... ...
yudao-module-bpm/src/test/resources/bpmn/lifecycle/start-user-skip.bpmn20.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
  3 + xmlns:flowable="http://flowable.org/bpmn"
  4 + targetNamespace="http://www.yudao.iocoder.cn/bpm">
  5 + <!-- 审批人与发起人相同:自动跳过 -->
  6 + <process id="startUserSkip" name="发起人相同自动跳过" isExecutable="true">
  7 + <startEvent id="start"/>
  8 + <sequenceFlow id="flow-start" sourceRef="start" targetRef="task"/>
  9 + <userTask id="task" name="审批">
  10 + <extensionElements>
  11 + <flowable:candidateStrategy>30</flowable:candidateStrategy>
  12 + <flowable:assignStartUserHandlerType>2</flowable:assignStartUserHandlerType>
  13 + </extensionElements>
  14 + </userTask>
  15 + <sequenceFlow id="flow-task" sourceRef="task" targetRef="end"/>
  16 + <endEvent id="end"/>
  17 + </process>
  18 +</definitions>
... ...
yudao-module-bpm/src/test/resources/bpmn/lifecycle/start-user-transfer-dept-leader.bpmn20.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
  3 + xmlns:flowable="http://flowable.org/bpmn"
  4 + targetNamespace="http://www.yudao.iocoder.cn/bpm">
  5 + <!-- 审批人与发起人相同:转交部门负责人 -->
  6 + <process id="startUserTransferDeptLeader" name="发起人相同转交部门负责人" isExecutable="true">
  7 + <startEvent id="start"/>
  8 + <sequenceFlow id="flow-start" sourceRef="start" targetRef="task"/>
  9 + <userTask id="task" name="审批">
  10 + <extensionElements>
  11 + <flowable:candidateStrategy>30</flowable:candidateStrategy>
  12 + <flowable:assignStartUserHandlerType>3</flowable:assignStartUserHandlerType>
  13 + </extensionElements>
  14 + </userTask>
  15 + <sequenceFlow id="flow-task" sourceRef="task" targetRef="end"/>
  16 + <endEvent id="end"/>
  17 + </process>
  18 +</definitions>
... ...
yudao-module-bpm/src/test/resources/bpmn/multi-instance/call-activity.bpmn20.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
  3 + xmlns:flowable="http://flowable.org/bpmn"
  4 + targetNamespace="http://www.yudao.iocoder.cn/bpm">
  5 + <!-- 子流程多实例:与 SimpleModelUtils 生成的结构一致(子流程发起人同主流程),被调用的子流程为 multiChild -->
  6 + <process id="multiChild" name="子流程" isExecutable="true">
  7 + <startEvent id="multiChild-start"/>
  8 + <sequenceFlow id="multiChild-flow-start" sourceRef="multiChild-start" targetRef="multiChild-task"/>
  9 + <userTask id="multiChild-task" name="子流程审批">
  10 + <extensionElements>
  11 + <flowable:candidateStrategy>30</flowable:candidateStrategy>
  12 + </extensionElements>
  13 + </userTask>
  14 + <sequenceFlow id="multiChild-flow-task" sourceRef="multiChild-task" targetRef="multiChild-end"/>
  15 + <endEvent id="multiChild-end"/>
  16 + </process>
  17 + <!-- 固定数量:并行发起 2 个子流程 -->
  18 + <process id="multiChildFixedQuantity" name="固定数量子流程" isExecutable="true">
  19 + <startEvent id="multiChildFixedQuantity-start"/>
  20 + <sequenceFlow id="multiChildFixedQuantity-flow-start" sourceRef="multiChildFixedQuantity-start" targetRef="multiChildFixedQuantity-child"/>
  21 + <callActivity id="multiChildFixedQuantity-child" name="子流程" calledElement="multiChild" flowable:calledElementType="key">
  22 + <extensionElements>
  23 + <flowable:childProcessMultiInstanceSourceType>1</flowable:childProcessMultiInstanceSourceType>
  24 + <flowable:executionListener event="start" delegateExpression="${bpmCallActivityListener}">
  25 + <flowable:field name="listenerConfig">
  26 + <flowable:string><![CDATA[{"type":1,"emptyType":1}]]></flowable:string>
  27 + </flowable:field>
  28 + </flowable:executionListener>
  29 + </extensionElements>
  30 + <multiInstanceLoopCharacteristics isSequential="false">
  31 + <loopCardinality>2</loopCardinality>
  32 + <completionCondition>${ nrOfCompletedInstances/nrOfInstances >= 1.00}</completionCondition>
  33 + </multiInstanceLoopCharacteristics>
  34 + </callActivity>
  35 + <sequenceFlow id="multiChildFixedQuantity-flow-child" sourceRef="multiChildFixedQuantity-child" targetRef="multiChildFixedQuantity-end"/>
  36 + <endEvent id="multiChildFixedQuantity-end"/>
  37 + </process>
  38 + <!-- 数字表单:并行发起 count 个子流程 -->
  39 + <process id="multiChildNumberForm" name="数字表单子流程" isExecutable="true">
  40 + <startEvent id="multiChildNumberForm-start"/>
  41 + <sequenceFlow id="multiChildNumberForm-flow-start" sourceRef="multiChildNumberForm-start" targetRef="multiChildNumberForm-child"/>
  42 + <callActivity id="multiChildNumberForm-child" name="子流程" calledElement="multiChild" flowable:calledElementType="key">
  43 + <extensionElements>
  44 + <flowable:childProcessMultiInstanceSourceType>2</flowable:childProcessMultiInstanceSourceType>
  45 + <flowable:executionListener event="start" delegateExpression="${bpmCallActivityListener}">
  46 + <flowable:field name="listenerConfig">
  47 + <flowable:string><![CDATA[{"type":1,"emptyType":1}]]></flowable:string>
  48 + </flowable:field>
  49 + </flowable:executionListener>
  50 + </extensionElements>
  51 + <multiInstanceLoopCharacteristics isSequential="false" flowable:collection="count">
  52 + <completionCondition>${ nrOfCompletedInstances/nrOfInstances >= 1.00}</completionCondition>
  53 + </multiInstanceLoopCharacteristics>
  54 + </callActivity>
  55 + <sequenceFlow id="multiChildNumberForm-flow-child" sourceRef="multiChildNumberForm-child" targetRef="multiChildNumberForm-end"/>
  56 + <endEvent id="multiChildNumberForm-end"/>
  57 + </process>
  58 + <!-- 多选表单:按 users 的数量,依次发起子流程 -->
  59 + <process id="multiChildMultipleForm" name="多选表单子流程" isExecutable="true">
  60 + <startEvent id="multiChildMultipleForm-start"/>
  61 + <sequenceFlow id="multiChildMultipleForm-flow-start" sourceRef="multiChildMultipleForm-start" targetRef="multiChildMultipleForm-child"/>
  62 + <callActivity id="multiChildMultipleForm-child" name="子流程" calledElement="multiChild" flowable:calledElementType="key">
  63 + <extensionElements>
  64 + <flowable:childProcessMultiInstanceSourceType>3</flowable:childProcessMultiInstanceSourceType>
  65 + <flowable:executionListener event="start" delegateExpression="${bpmCallActivityListener}">
  66 + <flowable:field name="listenerConfig">
  67 + <flowable:string><![CDATA[{"type":1,"emptyType":1}]]></flowable:string>
  68 + </flowable:field>
  69 + </flowable:executionListener>
  70 + </extensionElements>
  71 + <multiInstanceLoopCharacteristics isSequential="true" flowable:collection="users">
  72 + <completionCondition>${ nrOfCompletedInstances/nrOfInstances >= 1.00}</completionCondition>
  73 + </multiInstanceLoopCharacteristics>
  74 + </callActivity>
  75 + <sequenceFlow id="multiChildMultipleForm-flow-child" sourceRef="multiChildMultipleForm-child" targetRef="multiChildMultipleForm-end"/>
  76 + <endEvent id="multiChildMultipleForm-end"/>
  77 + </process>
  78 +</definitions>
... ...
yudao-module-bpm/src/test/resources/bpmn/multi-instance/user-task.bpmn20.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
  3 + xmlns:flowable="http://flowable.org/bpmn"
  4 + targetNamespace="http://www.yudao.iocoder.cn/bpm">
  5 + <!-- 多人审批:与 SimpleModelUtils 生成的结构一致,审批人由 BpmTaskCandidateInvoker 计算,collection 仅用于通过 Flowable 校验 -->
  6 + <!-- 或签:一人审批即可 -->
  7 + <process id="multiAny" name="或签" isExecutable="true">
  8 + <startEvent id="multiAny-start"/>
  9 + <sequenceFlow id="multiAny-flow-start" sourceRef="multiAny-start" targetRef="multiAny-task"/>
  10 + <userTask id="multiAny-task" name="或签审批">
  11 + <extensionElements>
  12 + <flowable:candidateStrategy>30</flowable:candidateStrategy>
  13 + <flowable:approveMethod>3</flowable:approveMethod>
  14 + </extensionElements>
  15 + <multiInstanceLoopCharacteristics isSequential="false" flowable:collection="${coll_userList}">
  16 + <completionCondition>${ nrOfCompletedInstances > 0 }</completionCondition>
  17 + </multiInstanceLoopCharacteristics>
  18 + </userTask>
  19 + <sequenceFlow id="multiAny-flow-task" sourceRef="multiAny-task" targetRef="multiAny-end"/>
  20 + <endEvent id="multiAny-end"/>
  21 + </process>
  22 + <!-- 会签:通过比例 100%,所有人审批通过 -->
  23 + <process id="multiRatio" name="会签" isExecutable="true">
  24 + <startEvent id="multiRatio-start"/>
  25 + <sequenceFlow id="multiRatio-flow-start" sourceRef="multiRatio-start" targetRef="multiRatio-task"/>
  26 + <userTask id="multiRatio-task" name="会签审批">
  27 + <extensionElements>
  28 + <flowable:candidateStrategy>30</flowable:candidateStrategy>
  29 + <flowable:approveMethod>2</flowable:approveMethod>
  30 + </extensionElements>
  31 + <multiInstanceLoopCharacteristics isSequential="false" flowable:collection="${coll_userList}">
  32 + <completionCondition>${ nrOfCompletedInstances/nrOfInstances >= 1.00}</completionCondition>
  33 + </multiInstanceLoopCharacteristics>
  34 + </userTask>
  35 + <sequenceFlow id="multiRatio-flow-task" sourceRef="multiRatio-task" targetRef="multiRatio-end"/>
  36 + <endEvent id="multiRatio-end"/>
  37 + </process>
  38 + <!-- 会签:通过比例 60%,部分审批人通过即可 -->
  39 + <process id="multiRatioPartial" name="会签(部分通过)" isExecutable="true">
  40 + <startEvent id="multiRatioPartial-start"/>
  41 + <sequenceFlow id="multiRatioPartial-flow-start" sourceRef="multiRatioPartial-start" targetRef="multiRatioPartial-task"/>
  42 + <userTask id="multiRatioPartial-task" name="会签审批">
  43 + <extensionElements>
  44 + <flowable:candidateStrategy>30</flowable:candidateStrategy>
  45 + <flowable:approveMethod>2</flowable:approveMethod>
  46 + </extensionElements>
  47 + <multiInstanceLoopCharacteristics isSequential="false" flowable:collection="${coll_userList}">
  48 + <completionCondition>${ nrOfCompletedInstances/nrOfInstances >= 0.60}</completionCondition>
  49 + </multiInstanceLoopCharacteristics>
  50 + </userTask>
  51 + <sequenceFlow id="multiRatioPartial-flow-task" sourceRef="multiRatioPartial-task" targetRef="multiRatioPartial-end"/>
  52 + <endEvent id="multiRatioPartial-end"/>
  53 + </process>
  54 + <!-- 会签驳回:first -> 会签(100%),会签驳回时退回到 first -->
  55 + <process id="multiReturn" name="会签驳回" isExecutable="true">
  56 + <startEvent id="multiReturn-start"/>
  57 + <sequenceFlow id="multiReturn-flow-start" sourceRef="multiReturn-start" targetRef="multiReturn-first"/>
  58 + <userTask id="multiReturn-first" name="发起审批">
  59 + <extensionElements>
  60 + <flowable:candidateStrategy>30</flowable:candidateStrategy>
  61 + </extensionElements>
  62 + </userTask>
  63 + <sequenceFlow id="multiReturn-flow-first" sourceRef="multiReturn-first" targetRef="multiReturn-task"/>
  64 + <userTask id="multiReturn-task" name="会签审批">
  65 + <extensionElements>
  66 + <flowable:candidateStrategy>30</flowable:candidateStrategy>
  67 + <flowable:approveMethod>2</flowable:approveMethod>
  68 + <flowable:rejectHandlerType>2</flowable:rejectHandlerType>
  69 + <flowable:rejectReturnTaskId>multiReturn-first</flowable:rejectReturnTaskId>
  70 + </extensionElements>
  71 + <multiInstanceLoopCharacteristics isSequential="false" flowable:collection="${coll_userList}">
  72 + <completionCondition>${ nrOfCompletedInstances/nrOfInstances >= 1.00}</completionCondition>
  73 + </multiInstanceLoopCharacteristics>
  74 + </userTask>
  75 + <sequenceFlow id="multiReturn-flow-task" sourceRef="multiReturn-task" targetRef="multiReturn-end"/>
  76 + <endEvent id="multiReturn-end"/>
  77 + </process>
  78 + <!-- 依次审批:按审批人顺序逐个审批 -->
  79 + <process id="multiSequential" name="依次审批" isExecutable="true">
  80 + <startEvent id="multiSequential-start"/>
  81 + <sequenceFlow id="multiSequential-flow-start" sourceRef="multiSequential-start" targetRef="multiSequential-task"/>
  82 + <userTask id="multiSequential-task" name="依次审批">
  83 + <extensionElements>
  84 + <flowable:candidateStrategy>30</flowable:candidateStrategy>
  85 + <flowable:approveMethod>4</flowable:approveMethod>
  86 + </extensionElements>
  87 + <multiInstanceLoopCharacteristics isSequential="true" flowable:collection="${coll_userList}">
  88 + <loopCardinality>1</loopCardinality>
  89 + <completionCondition>${ nrOfCompletedInstances >= nrOfInstances }</completionCondition>
  90 + </multiInstanceLoopCharacteristics>
  91 + </userTask>
  92 + <sequenceFlow id="multiSequential-flow-task" sourceRef="multiSequential-task" targetRef="multiSequential-end"/>
  93 + <endEvent id="multiSequential-end"/>
  94 + </process>
  95 +</definitions>
... ...
yudao-module-bpm/src/test/resources/bpmn/simple-approve.bpmn20.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
  3 + xmlns:flowable="http://flowable.org/bpmn"
  4 + targetNamespace="http://www.yudao.iocoder.cn/bpm">
  5 + <process id="simpleApprove" name="简单审批" isExecutable="true">
  6 + <startEvent id="start"/>
  7 + <sequenceFlow id="flow-start" sourceRef="start" targetRef="approve"/>
  8 + <userTask id="approve" name="审批" flowable:assignee="${startUserId}">
  9 + <extensionElements>
  10 + <flowable:candidateStrategy>36</flowable:candidateStrategy>
  11 + </extensionElements>
  12 + </userTask>
  13 + <sequenceFlow id="flow-end" sourceRef="approve" targetRef="end"/>
  14 + <endEvent id="end"/>
  15 + </process>
  16 +</definitions>
... ...
yudao-module-bpm/src/test/resources/bpmn/timeout-approve.bpmn20.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
  3 + xmlns:flowable="http://flowable.org/bpmn"
  4 + targetNamespace="http://www.yudao.iocoder.cn/bpm">
  5 + <!-- 审批超时:task1 挂载不中断的定时边界事件,超时处理方式默认为自动同意 -->
  6 + <process id="timeoutApprove" name="超时审批" isExecutable="true">
  7 + <startEvent id="start"/>
  8 + <sequenceFlow id="flow-start" sourceRef="start" targetRef="task1"/>
  9 + <userTask id="task1" name="一级审批">
  10 + <extensionElements>
  11 + <flowable:candidateStrategy>30</flowable:candidateStrategy>
  12 + </extensionElements>
  13 + </userTask>
  14 + <boundaryEvent id="task1-timeout" attachedToRef="task1" cancelActivity="false">
  15 + <extensionElements>
  16 + <flowable:boundaryEventType>1</flowable:boundaryEventType>
  17 + <flowable:timeoutHandlerType>2</flowable:timeoutHandlerType>
  18 + </extensionElements>
  19 + <timerEventDefinition>
  20 + <timeDuration>PT1H</timeDuration>
  21 + </timerEventDefinition>
  22 + </boundaryEvent>
  23 + <sequenceFlow id="flow-task1" sourceRef="task1" targetRef="end"/>
  24 + <endEvent id="end"/>
  25 + </process>
  26 +</definitions>
... ...
yudao-module-bpm/src/test/resources/bpmn/trigger-task.bpmn20.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
  3 + targetNamespace="http://www.yudao.iocoder.cn/bpm">
  4 + <process id="triggerTask" name="触发任务" isExecutable="true">
  5 + <startEvent id="start"/>
  6 + <sequenceFlow id="flow-start" sourceRef="start" targetRef="wait"/>
  7 + <receiveTask id="wait" name="等待触发"/>
  8 + <sequenceFlow id="flow-end" sourceRef="wait" targetRef="end"/>
  9 + <endEvent id="end"/>
  10 + </process>
  11 +</definitions>
... ...
yudao-module-bpm/src/test/resources/bpmn/two-step-approve.bpmn20.xml 0 → 100644
  1 +<?xml version="1.0" encoding="UTF-8"?>
  2 +<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
  3 + xmlns:flowable="http://flowable.org/bpmn"
  4 + targetNamespace="http://www.yudao.iocoder.cn/bpm">
  5 + <process id="twoStepApprove" name="两级审批" isExecutable="true">
  6 + <startEvent id="start"/>
  7 + <sequenceFlow id="flow-start" sourceRef="start" targetRef="task1"/>
  8 + <userTask id="task1" name="一级审批">
  9 + <extensionElements>
  10 + <flowable:candidateStrategy>30</flowable:candidateStrategy>
  11 + </extensionElements>
  12 + </userTask>
  13 + <sequenceFlow id="flow-task1" sourceRef="task1" targetRef="task2"/>
  14 + <userTask id="task2" name="二级审批">
  15 + <extensionElements>
  16 + <flowable:candidateStrategy>30</flowable:candidateStrategy>
  17 + <flowable:rejectHandlerType>2</flowable:rejectHandlerType>
  18 + <flowable:rejectReturnTaskId>task1</flowable:rejectReturnTaskId>
  19 + </extensionElements>
  20 + </userTask>
  21 + <sequenceFlow id="flow-task2" sourceRef="task2" targetRef="end"/>
  22 + <endEvent id="end"/>
  23 + </process>
  24 +</definitions>
... ...
yudao-module-bpm/src/test/resources/sql/clean.sql
1 1 DELETE FROM "bpm_form";
2 2 DELETE FROM "bpm_user_group";
3 3 DELETE FROM "bpm_category";
  4 +DELETE FROM "bpm_process_expression";
  5 +DELETE FROM "bpm_process_listener";
  6 +DELETE FROM "bpm_oa_leave";
... ...
yudao-module-bpm/src/test/resources/sql/create_tables.sql
... ... @@ -41,3 +41,50 @@ CREATE TABLE IF NOT EXISTS &quot;bpm_form&quot; (
41 41 "deleted" bit NOT NULL DEFAULT FALSE,
42 42 PRIMARY KEY ("id")
43 43 ) COMMENT '动态表单';
  44 +
  45 +CREATE TABLE IF NOT EXISTS "bpm_process_expression" (
  46 + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
  47 + "name" varchar(63) NOT NULL,
  48 + "status" tinyint NOT NULL,
  49 + "expression" varchar(255) NOT NULL,
  50 + "creator" varchar(64) DEFAULT '',
  51 + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  52 + "updater" varchar(64) DEFAULT '',
  53 + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  54 + "deleted" bit NOT NULL DEFAULT FALSE,
  55 + PRIMARY KEY ("id")
  56 +) COMMENT '流程表达式';
  57 +
  58 +CREATE TABLE IF NOT EXISTS "bpm_process_listener" (
  59 + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
  60 + "name" varchar(63) NOT NULL,
  61 + "status" tinyint NOT NULL,
  62 + "type" varchar(30) NOT NULL,
  63 + "event" varchar(30) NOT NULL,
  64 + "value_type" varchar(30) NOT NULL,
  65 + "value" varchar(255) NOT NULL,
  66 + "creator" varchar(64) DEFAULT '',
  67 + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  68 + "updater" varchar(64) DEFAULT '',
  69 + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  70 + "deleted" bit NOT NULL DEFAULT FALSE,
  71 + PRIMARY KEY ("id")
  72 +) COMMENT '流程监听器';
  73 +
  74 +CREATE TABLE IF NOT EXISTS "bpm_oa_leave" (
  75 + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
  76 + "user_id" bigint NOT NULL,
  77 + "type" tinyint NOT NULL,
  78 + "reason" varchar(255) NOT NULL,
  79 + "start_time" timestamp NOT NULL,
  80 + "end_time" timestamp NOT NULL,
  81 + "day" bigint NOT NULL,
  82 + "status" tinyint NOT NULL,
  83 + "process_instance_id" varchar(64) DEFAULT '',
  84 + "creator" varchar(64) DEFAULT '',
  85 + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  86 + "updater" varchar(64) DEFAULT '',
  87 + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  88 + "deleted" bit NOT NULL DEFAULT FALSE,
  89 + PRIMARY KEY ("id")
  90 +) COMMENT 'OA 请假申请';
... ...
yudao-module-bpm/src/test/resources/sql/flowable/clean.sql 0 → 100644
  1 +DELETE FROM "bpm_process_definition_info";
... ...
yudao-module-bpm/src/test/resources/sql/flowable/create_tables.sql 0 → 100644
  1 +-- Flowable 集成测试(H2 LEGACY 模式)使用的业务表,不支持表注释语法
  2 +CREATE TABLE IF NOT EXISTS "bpm_process_definition_info" (
  3 + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
  4 + "process_definition_id" varchar(64) NOT NULL,
  5 + "model_id" varchar(64) NOT NULL,
  6 + "model_type" tinyint NOT NULL,
  7 + "category" varchar(64),
  8 + "icon" varchar(512),
  9 + "description" varchar(255),
  10 + "form_type" tinyint,
  11 + "form_id" bigint,
  12 + "form_conf" varchar(1000),
  13 + "form_fields" varchar(5000),
  14 + "form_custom_create_path" varchar(255),
  15 + "form_custom_view_path" varchar(255),
  16 + "simple_model" varchar(10000),
  17 + "visible" bit NOT NULL DEFAULT TRUE,
  18 + "sort" bigint,
  19 + "start_user_ids" varchar(255),
  20 + "start_dept_ids" varchar(255),
  21 + "manager_user_ids" varchar(255),
  22 + "allow_cancel_running_process" bit,
  23 + "allow_withdraw_task" bit,
  24 + "process_id_rule" varchar(255),
  25 + "auto_approval_type" tinyint,
  26 + "title_setting" varchar(512),
  27 + "summary_setting" varchar(512),
  28 + "process_before_trigger_setting" varchar(512),
  29 + "process_after_trigger_setting" varchar(512),
  30 + "task_before_trigger_setting" varchar(512),
  31 + "task_after_trigger_setting" varchar(512),
  32 + "print_template_setting" varchar(512),
  33 + "creator" varchar(64) DEFAULT '',
  34 + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  35 + "updater" varchar(64) DEFAULT '',
  36 + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  37 + "deleted" bit NOT NULL DEFAULT FALSE,
  38 + PRIMARY KEY ("id")
  39 +);
... ...