Commit e0f0cab7f6a81d95e7aee1ac89fa6f059a3a7c86
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
yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderMultiStrategyTest.java
yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserDeptLeaderStrategyTest.java
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
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
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
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
yudao-module-bpm/src/test/resources/sql/create_tables.sql
| ... | ... | @@ -41,3 +41,50 @@ CREATE TABLE IF NOT EXISTS "bpm_form" ( |
| 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 | +); | ... | ... |