diff --git a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/TaskServiceImpl.java b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/TaskServiceImpl.java index c87345f21..a6dbbda27 100644 --- a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/TaskServiceImpl.java +++ b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/TaskServiceImpl.java @@ -1110,6 +1110,7 @@ public class TaskServiceImpl implements ITaskService { return; } + runtimeService.setVariable(processInstanceId, ProcessInstanceConstant.FINISH_REASON_VAR, finishReason); runtimeService.createChangeActivityStateBuilder() .moveExecutionsToSingleActivityId(executionIds, "end") .changeState(); diff --git a/pigx-flow/pigx-flow-biz/src/test/java/com/pig4cloud/pigx/flow/service/impl/ProcessInstanceServiceImplTests.java b/pigx-flow/pigx-flow-biz/src/test/java/com/pig4cloud/pigx/flow/service/impl/ProcessInstanceServiceImplTests.java index aab4f9457..1ce2ff71a 100644 --- a/pigx-flow/pigx-flow-biz/src/test/java/com/pig4cloud/pigx/flow/service/impl/ProcessInstanceServiceImplTests.java +++ b/pigx-flow/pigx-flow-biz/src/test/java/com/pig4cloud/pigx/flow/service/impl/ProcessInstanceServiceImplTests.java @@ -15,6 +15,8 @@ import org.flowable.engine.HistoryService; import org.flowable.engine.RuntimeService; import org.flowable.engine.TaskService; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentCaptor; import java.util.LinkedHashMap; @@ -31,14 +33,16 @@ import static org.mockito.Mockito.when; class ProcessInstanceServiceImplTests { - @Test - void endPersistsExplicitRefusalWhenHistoricalApprovalVariableAppearsLast() { + @ParameterizedTest + @ValueSource(strings = { ProcessInstanceConstant.FINISH_REASON_REFUSE, + ProcessInstanceConstant.FINISH_REASON_WITHDRAW, ProcessInstanceConstant.FINISH_REASON_TERMINATE }) + void endPersistsExplicitFinishReasonWhenHistoricalApprovalVariableAppearsLast(String finishReason) { Map variables = new LinkedHashMap<>(); variables.put("current_approve_condition", false); variables.put("previous_approve_condition", true); - variables.put(ProcessInstanceConstant.FINISH_REASON_VAR, ProcessInstanceConstant.FINISH_REASON_REFUSE); + variables.put(ProcessInstanceConstant.FINISH_REASON_VAR, finishReason); - assertThat(captureFinishReason(variables)).isEqualTo(ProcessInstanceConstant.FINISH_REASON_REFUSE); + assertThat(captureFinishReason(variables)).isEqualTo(finishReason); } @Test diff --git a/pigx-flow/pigx-flow-biz/src/test/java/com/pig4cloud/pigx/flow/service/impl/TaskServiceImplTests.java b/pigx-flow/pigx-flow-biz/src/test/java/com/pig4cloud/pigx/flow/service/impl/TaskServiceImplTests.java new file mode 100644 index 000000000..52b0785d8 --- /dev/null +++ b/pigx-flow/pigx-flow-biz/src/test/java/com/pig4cloud/pigx/flow/service/impl/TaskServiceImplTests.java @@ -0,0 +1,495 @@ +package com.pig4cloud.pigx.flow.service.impl; + +import com.baomidou.mybatisplus.core.MybatisConfiguration; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; +import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.pig4cloud.pigx.admin.api.entity.SysUser; +import com.pig4cloud.pigx.admin.api.feign.RemoteUserService; +import com.pig4cloud.pigx.common.core.util.R; +import com.pig4cloud.pigx.common.data.tenant.TenantContextHolder; +import com.pig4cloud.pigx.common.security.service.PigxUser; +import com.pig4cloud.pigx.flow.constant.NodeStatusEnum; +import com.pig4cloud.pigx.flow.constant.ProcessInstanceConstant; +import com.pig4cloud.pigx.flow.dto.AddSignResultDto; +import com.pig4cloud.pigx.flow.dto.AddSignTaskDto; +import com.pig4cloud.pigx.flow.dto.Node; +import com.pig4cloud.pigx.flow.dto.TaskParamDto; +import com.pig4cloud.pigx.flow.entity.ProcessInstanceRecord; +import com.pig4cloud.pigx.flow.entity.ProcessNodeRecord; +import com.pig4cloud.pigx.flow.entity.ProcessNodeRecordAssignUser; +import com.pig4cloud.pigx.flow.entity.ProcessTaskAddSignRecord; +import com.pig4cloud.pigx.flow.mapper.ProcessNodeRecordMapper; +import com.pig4cloud.pigx.flow.mapper.ProcessTaskAddSignRecordMapper; +import com.pig4cloud.pigx.flow.service.IFlowableEngineService; +import com.pig4cloud.pigx.flow.service.IProcessCopyService; +import com.pig4cloud.pigx.flow.service.IProcessInstanceRecordService; +import com.pig4cloud.pigx.flow.service.IProcessNodeDataService; +import com.pig4cloud.pigx.flow.service.IProcessNodeRecordAssignUserService; +import com.pig4cloud.pigx.flow.service.IProcessNodeRecordService; +import com.pig4cloud.pigx.flow.service.IProcessService; +import com.pig4cloud.pigx.flow.service.ITaskService; +import org.apache.ibatis.builder.MapperBuilderAssistant; +import org.flowable.common.engine.api.FlowableOptimisticLockingException; +import org.flowable.engine.HistoryService; +import org.flowable.engine.RuntimeService; +import org.flowable.engine.TaskService; +import org.flowable.engine.runtime.ChangeActivityStateBuilder; +import org.flowable.engine.runtime.Execution; +import org.flowable.engine.runtime.ExecutionQuery; +import org.flowable.engine.runtime.ProcessInstance; +import org.flowable.engine.runtime.ProcessInstanceQuery; +import org.flowable.task.api.Task; +import org.flowable.task.api.TaskQuery; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource; +import org.springframework.transaction.interceptor.TransactionInterceptor; +import org.springframework.transaction.support.AbstractPlatformTransactionManager; +import org.springframework.transaction.support.DefaultTransactionStatus; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class TaskServiceImplTests { + + private static final long USER_ID = 11L; + + private static final long TENANT_ID = 1L; + + private static final long TARGET_USER_ID = 31L; + + private static final long NODE_RECORD_ID = 101L; + + private static final long ADD_SIGN_RECORD_ID = 901L; + + private static final String TASK_ID = "12501"; + + private static final String ADDED_TASK_ID = "12502"; + + private static final String FLOW_ID = "flow-1"; + + private static final String PROCESS_DEFINITION_ID = FLOW_ID + ":1:deployment"; + + private static final String PROCESS_INSTANCE_ID = "process-1"; + + private static final String NODE_ID = "approve-1"; + + @Mock + private IProcessService processService; + @Mock + private IProcessCopyService processCopyService; + @Mock + private IProcessNodeDataService nodeDataService; + @Mock + private IProcessNodeRecordAssignUserService processNodeRecordAssignUserService; + @Mock + private IProcessInstanceRecordService processInstanceRecordService; + @Mock + private IProcessNodeRecordService processNodeRecordService; + @Mock + private IFlowableEngineService flowableEngineService; + @Mock + private TaskService taskService; + @Mock + private HistoryService historyService; + @Mock + private RuntimeService runtimeService; + @Spy + private ObjectMapper objectMapper = new ObjectMapper(); + @Mock + private RemoteUserService remoteUserService; + @Mock + private ProcessNodeRecordMapper processNodeRecordMapper; + @Mock + private ProcessTaskAddSignRecordMapper processTaskAddSignRecordMapper; + @Mock(answer = Answers.RETURNS_SELF) + private TaskQuery taskQuery; + @Mock + private BaseMapper processInstanceBaseMapper; + @Mock + private BaseMapper assignmentBaseMapper; + @Mock + private Task sourceTask; + @Mock + private Task addedTask; + @Mock + private Execution addedExecution; + + @InjectMocks + private TaskServiceImpl taskServiceImpl; + + @BeforeAll + static void initializeTableInfo() { + MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), "test"); + TableInfoHelper.initTableInfo(assistant, ProcessInstanceRecord.class); + TableInfoHelper.initTableInfo(assistant, ProcessNodeRecord.class); + TableInfoHelper.initTableInfo(assistant, ProcessNodeRecordAssignUser.class); + } + + @BeforeEach + void setCurrentUserAndTenant() { + PigxUser user = new PigxUser(USER_ID, "approver", List.of(), null, List.of(), List.of(), null, null, + "审批人", "审批人", null, TENANT_ID, "password", true, true, "sys", true, + LocalDateTime.now(), true, List.of()); + SecurityContextHolder.getContext() + .setAuthentication(UsernamePasswordAuthenticationToken.authenticated(user, "password", List.of())); + TenantContextHolder.setTenantId(TENANT_ID); + } + + @AfterEach + void clearCurrentUserAndTenant() { + SecurityContextHolder.clearContext(); + TenantContextHolder.clear(); + } + + @Test + void stopProcessInstancePropagatesTerminateReasonBeforeMovingToEnd() { + ChangeActivityStateBuilder stateChangeBuilder = prepareTerminationFlow(null); + TaskParamDto request = new TaskParamDto(); + request.setProcessInstanceId(PROCESS_INSTANCE_ID); + + R result = taskServiceImpl.stopProcessInstance(request); + + assertThat(result.getCode()).isZero(); + verifyFinishReasonBeforeStateChange(stateChangeBuilder, ProcessInstanceConstant.FINISH_REASON_TERMINATE); + } + + @Test + void withdrawProcessInstancePropagatesWithdrawReasonBeforeMovingToEnd() { + ProcessInstanceRecord record = new ProcessInstanceRecord() + .setProcessInstanceId(PROCESS_INSTANCE_ID) + .setUserId(USER_ID) + .setStatus(NodeStatusEnum.JXZ.getCode()); + ChangeActivityStateBuilder stateChangeBuilder = prepareTerminationFlow(record); + TaskParamDto request = new TaskParamDto(); + request.setProcessInstanceId(PROCESS_INSTANCE_ID); + + R result = taskServiceImpl.withdrawProcessInstance(request); + + assertThat(result.getCode()).isZero(); + verifyFinishReasonBeforeStateChange(stateChangeBuilder, ProcessInstanceConstant.FINISH_REASON_WITHDRAW); + } + + @Test + void addSignRejectsEmptyTargetBatch() { + AddSignTaskDto request = request(List.of(), "需要共同确认"); + + R result = taskServiceImpl.addSign(request); + + assertThat(result.getCode()).isEqualTo(1); + assertThat(result.getMsg()).isEqualTo("请选择加签人员"); + } + + @Test + void addSignRejectsDuplicateTargetUsers() { + AddSignTaskDto request = request(List.of(31L, 31L), "需要共同确认"); + + R result = taskServiceImpl.addSign(request); + + assertThat(result.getCode()).isEqualTo(1); + assertThat(result.getMsg()).isEqualTo("加签人员不能重复"); + } + + @Test + void addSignRejectsReasonOutsideLengthLimit() { + AddSignTaskDto request = request(List.of(31L), "短"); + + R result = taskServiceImpl.addSign(request); + + assertThat(result.getCode()).isEqualTo(1); + assertThat(result.getMsg()).isEqualTo("加签原因需为 2~100 字"); + } + + @Test + void addSignCreatesTaskAndAuditSnapshotForEligibleApprover() { + prepareTaskAndNode(addedTask); + prepareOriginalApprover(); + prepareTargetValidation(List.of(originalAssignment()), validUser(TARGET_USER_ID, TENANT_ID)); + prepareAuditInsert(); + when(runtimeService.addMultiInstanceExecution(eq(NODE_ID), eq(PROCESS_INSTANCE_ID), anyMap())) + .thenReturn(addedExecution); + when(assignmentBaseMapper.selectCount(any())).thenReturn(1L); + + R result = taskServiceImpl.addSign(request(List.of(TARGET_USER_ID), "需要共同确认")); + + assertThat(result.getCode()).isZero(); + assertThat(result.getData()).isInstanceOf(AddSignResultDto.class); + AddSignResultDto addSignResult = (AddSignResultDto) result.getData(); + assertThat(addSignResult.getRecordId()).isEqualTo(ADD_SIGN_RECORD_ID); + assertThat(addSignResult.getTasks()).hasSize(1); + assertThat(addSignResult.getTasks().get(0).getTaskId()).isEqualTo(ADDED_TASK_ID); + assertThat(addSignResult.getTasks().get(0).getUserId()).isEqualTo(TARGET_USER_ID); + + ArgumentCaptor recordCaptor = ArgumentCaptor + .forClass(ProcessTaskAddSignRecord.class); + verify(processTaskAddSignRecordMapper).insert(recordCaptor.capture()); + ProcessTaskAddSignRecord record = recordCaptor.getValue(); + assertThat(record.getSourceTaskId()).isEqualTo(TASK_ID); + assertThat(record.getAddedByUserId()).isEqualTo(USER_ID); + assertThat(record.getTargetUserIds()).isEqualTo("[31]"); + assertThat(record.getParticipantUserIdsBefore()).isEqualTo("[11]"); + assertThat(record.getParticipantUserIdsAfter()).isEqualTo("[11,31]"); + verify(processTaskAddSignRecordMapper).updateById(record); + verify(processNodeRecordMapper).selectActiveByIdForUpdate(NODE_RECORD_ID, PROCESS_INSTANCE_ID, NODE_ID, + TENANT_ID); + verify(runtimeService).addMultiInstanceExecution(eq(NODE_ID), eq(PROCESS_INSTANCE_ID), + argThat(variables -> Long.valueOf(TARGET_USER_ID).equals(variables.get(NODE_ID + "_assignee_temp")) + && Long.valueOf(ADD_SIGN_RECORD_ID) + .equals(variables.get(ProcessInstanceConstant.VAR_ADD_SIGN_RECORD_ID)))); + } + + @Test + void addSignRejectsProcessStarterEvenWhenStarterOwnsTask() { + prepareTaskAndNode(null); + prepareActiveAssignment(); + when(processInstanceRecordService.lambdaQuery()) + .thenAnswer(invocation -> new LambdaQueryChainWrapper<>(processInstanceBaseMapper)); + when(processInstanceBaseMapper.selectCount(any())).thenReturn(1L); + + R result = taskServiceImpl.addSign(request(List.of(TARGET_USER_ID), "需要共同确认")); + + assertThat(result.getCode()).isEqualTo(1); + assertThat(result.getMsg()).isEqualTo("流程发起人不能发起加签"); + verify(processTaskAddSignRecordMapper, never()).insert(any(ProcessTaskAddSignRecord.class)); + } + + @Test + void addSignRejectsTargetUserFromAnotherTenant() { + prepareTaskAndNode(null); + prepareOriginalApprover(); + prepareTargetValidation(List.of(originalAssignment()), validUser(TARGET_USER_ID, TENANT_ID + 1)); + + R result = taskServiceImpl.addSign(request(List.of(TARGET_USER_ID), "需要共同确认")); + + assertThat(result.getCode()).isEqualTo(1); + assertThat(result.getMsg()).isEqualTo("加签用户不存在或不属于当前租户"); + verify(processTaskAddSignRecordMapper, never()).insert(any(ProcessTaskAddSignRecord.class)); + } + + @Test + void addSignRejectsUserAlreadyParticipatingInCurrentCountersign() { + prepareTaskAndNode(null); + prepareOriginalApprover(); + ProcessNodeRecordAssignUser existingParticipant = new ProcessNodeRecordAssignUser() + .setNodeRecordId(NODE_RECORD_ID) + .setUserId(TARGET_USER_ID); + prepareTargetValidation(List.of(originalAssignment(), existingParticipant), + validUser(TARGET_USER_ID, TENANT_ID)); + + R result = taskServiceImpl.addSign(request(List.of(TARGET_USER_ID), "需要共同确认")); + + assertThat(result.getCode()).isEqualTo(1); + assertThat(result.getMsg()).isEqualTo("用户已参与当前会签:目标审批人"); + verify(processTaskAddSignRecordMapper, never()).insert(any(ProcessTaskAddSignRecord.class)); + } + + @Test + void addSignRollsBackAuditWhenFlowableReportsConcurrentChange() { + prepareTaskAndNode(null); + prepareOriginalApprover(); + prepareTargetValidation(List.of(originalAssignment()), validUser(TARGET_USER_ID, TENANT_ID)); + prepareAuditInsert(); + when(runtimeService.addMultiInstanceExecution(anyString(), anyString(), anyMap())) + .thenThrow(new FlowableOptimisticLockingException("conflict")); + RecordingTransactionManager transactionManager = new RecordingTransactionManager(); + + R result = transactionalService(transactionManager) + .addSign(request(List.of(TARGET_USER_ID), "需要共同确认")); + + assertThat(result.getCode()).isEqualTo(1); + assertThat(result.getMsg()).isEqualTo("节点状态已变化,请刷新后重试"); + assertThat(transactionManager.rolledBack).isTrue(); + assertThat(transactionManager.committed).isFalse(); + verify(processTaskAddSignRecordMapper).insert(any(ProcessTaskAddSignRecord.class)); + verify(processTaskAddSignRecordMapper, never()).updateById(any(ProcessTaskAddSignRecord.class)); + } + + private void prepareTaskAndNode(Task taskCreatedAfterAddSign) { + when(taskService.createTaskQuery()).thenReturn(taskQuery); + if (taskCreatedAfterAddSign == null) { + when(taskQuery.singleResult()).thenReturn(sourceTask); + } + else { + when(taskQuery.singleResult()).thenReturn(sourceTask, taskCreatedAfterAddSign); + } + when(sourceTask.getProcessInstanceId()).thenReturn(PROCESS_INSTANCE_ID); + when(sourceTask.getTaskDefinitionKey()).thenReturn(NODE_ID); + when(sourceTask.getProcessDefinitionId()).thenReturn(PROCESS_DEFINITION_ID); + if (taskCreatedAfterAddSign != null) { + when(taskCreatedAfterAddSign.getId()).thenReturn(ADDED_TASK_ID); + } + + ProcessNodeRecord nodeRecord = new ProcessNodeRecord().setId(NODE_RECORD_ID); + when(processNodeRecordMapper.selectActiveByIdForUpdate(NODE_RECORD_ID, PROCESS_INSTANCE_ID, NODE_ID, + TENANT_ID)) + .thenReturn(nodeRecord); + Node node = new Node(); + node.setAssignedType(ProcessInstanceConstant.AssignedTypeClass.ROLE); + node.setMultipleMode(ProcessInstanceConstant.MULTIPLE_MODE_AL_SAME); + node.setAllowAddSign(true); + when(nodeDataService.getNodeData(FLOW_ID, NODE_ID)).thenReturn(R.ok(node)); + } + + private void prepareOriginalApprover() { + prepareActiveAssignment(); + when(sourceTask.getAssignee()).thenReturn(String.valueOf(USER_ID)); + when(processInstanceRecordService.lambdaQuery()) + .thenAnswer(invocation -> new LambdaQueryChainWrapper<>(processInstanceBaseMapper)); + } + + private void prepareActiveAssignment() { + when(sourceTask.getId()).thenReturn(TASK_ID); + when(processNodeRecordAssignUserService.lambdaQuery()) + .thenAnswer(invocation -> new LambdaQueryChainWrapper<>(assignmentBaseMapper)); + when(assignmentBaseMapper.selectOne(any())).thenReturn(originalAssignment()); + } + + private void prepareTargetValidation(List participants, SysUser targetUser) { + when(assignmentBaseMapper.selectList(any())).thenReturn(participants); + when(remoteUserService.getUserById(List.of(TARGET_USER_ID))).thenReturn(R.ok(List.of(targetUser))); + } + + private void prepareAuditInsert() { + when(processTaskAddSignRecordMapper.insert(any(ProcessTaskAddSignRecord.class))).thenAnswer(invocation -> { + ProcessTaskAddSignRecord record = invocation.getArgument(0); + record.setId(ADD_SIGN_RECORD_ID); + return 1; + }); + } + + private ChangeActivityStateBuilder prepareTerminationFlow(ProcessInstanceRecord activeRecord) { + when(processInstanceRecordService.lambdaQuery()) + .thenAnswer(invocation -> new LambdaQueryChainWrapper<>(processInstanceBaseMapper)); + when(processInstanceBaseMapper.selectList(any())).thenReturn(List.of()); + if (activeRecord != null) { + when(processInstanceBaseMapper.selectOne(any())).thenReturn(activeRecord); + when(processNodeRecordAssignUserService.lambdaQuery()) + .thenAnswer(invocation -> new LambdaQueryChainWrapper<>(assignmentBaseMapper)); + when(assignmentBaseMapper.selectCount(any())).thenReturn(0L); + } + when(processInstanceRecordService.lambdaUpdate()) + .thenAnswer(invocation -> new LambdaUpdateChainWrapper<>(processInstanceBaseMapper)); + when(processNodeRecordService.lambdaUpdate()) + .thenAnswer(invocation -> new LambdaUpdateChainWrapper<>(processNodeRecordMapper)); + when(processNodeRecordAssignUserService.lambdaUpdate()) + .thenAnswer(invocation -> new LambdaUpdateChainWrapper<>(assignmentBaseMapper)); + + ProcessInstanceQuery processInstanceQuery = mock(ProcessInstanceQuery.class, Answers.RETURNS_SELF); + when(runtimeService.createProcessInstanceQuery()).thenReturn(processInstanceQuery); + when(processInstanceQuery.singleResult()).thenReturn(mock(ProcessInstance.class)); + + ExecutionQuery executionQuery = mock(ExecutionQuery.class, Answers.RETURNS_SELF); + Execution execution = mock(Execution.class); + when(execution.getId()).thenReturn("execution-1"); + when(runtimeService.createExecutionQuery()).thenReturn(executionQuery); + when(executionQuery.list()).thenReturn(List.of(execution)); + + ChangeActivityStateBuilder stateChangeBuilder = mock(ChangeActivityStateBuilder.class, Answers.RETURNS_SELF); + when(runtimeService.createChangeActivityStateBuilder()).thenReturn(stateChangeBuilder); + return stateChangeBuilder; + } + + private void verifyFinishReasonBeforeStateChange(ChangeActivityStateBuilder stateChangeBuilder, + String finishReason) { + var inOrder = inOrder(runtimeService, stateChangeBuilder); + inOrder.verify(runtimeService) + .setVariable(PROCESS_INSTANCE_ID, ProcessInstanceConstant.FINISH_REASON_VAR, finishReason); + inOrder.verify(runtimeService).createChangeActivityStateBuilder(); + inOrder.verify(stateChangeBuilder).moveExecutionsToSingleActivityId(List.of("execution-1"), "end"); + inOrder.verify(stateChangeBuilder).changeState(); + } + + private ProcessNodeRecordAssignUser originalAssignment() { + return new ProcessNodeRecordAssignUser() + .setNodeRecordId(NODE_RECORD_ID) + .setTaskId(TASK_ID) + .setUserId(USER_ID) + .setInitialUserId(USER_ID) + .setAssignmentOrigin(ProcessInstanceConstant.ASSIGNMENT_ORIGIN_ORIGINAL) + .setStatus(NodeStatusEnum.JXZ.getCode()); + } + + private SysUser validUser(long userId, long tenantId) { + SysUser user = new SysUser(); + user.setUserId(userId); + user.setUsername("target"); + user.setName("目标审批人"); + user.setTenantId(tenantId); + user.setDelFlag("0"); + user.setLockFlag("0"); + return user; + } + + private ITaskService transactionalService(RecordingTransactionManager transactionManager) { + ProxyFactory proxyFactory = new ProxyFactory(taskServiceImpl); + proxyFactory.addInterface(ITaskService.class); + proxyFactory.addAdvice(new TransactionInterceptor(transactionManager, + new AnnotationTransactionAttributeSource())); + return (ITaskService) proxyFactory.getProxy(); + } + + private AddSignTaskDto request(List userIds, String reason) { + AddSignTaskDto request = new AddSignTaskDto(); + request.setTaskId(TASK_ID); + request.setTargetUserIdList(userIds); + request.setReason(reason); + return request; + } + + private static final class RecordingTransactionManager extends AbstractPlatformTransactionManager { + + private boolean committed; + + private boolean rolledBack; + + @Override + protected Object doGetTransaction() { + return new Object(); + } + + @Override + protected void doBegin(Object transaction, TransactionDefinition definition) { + } + + @Override + protected void doCommit(DefaultTransactionStatus status) { + committed = true; + } + + @Override + protected void doRollback(DefaultTransactionStatus status) { + rolledBack = true; + } + + } + +}