package jnpf.flowable.util; import jnpf.base.UserInfo; import jnpf.base.model.flow.FlowFormDataModel; import jnpf.exception.WorkFlowException; import jnpf.flowable.entity.*; import jnpf.flowable.enums.TaskStatusEnum; import jnpf.flowable.mapper.*; import jnpf.flowable.model.flowable.FlowAbleUrl; import jnpf.flowable.model.task.FlowModel; import jnpf.flowable.model.templatenode.nodejson.NodeModel; import jnpf.flowable.model.util.FlowStatusHolder; import jnpf.flowable.model.util.TaskInitiatorPermission; import jnpf.flowable.service.FilePermissionSnapshotService; import jnpf.util.UserProvider; import jnpf.util.context.SpringContext; import org.junit.*; import org.mockito.MockedStatic; import org.springframework.context.ApplicationContext; import java.lang.reflect.Constructor; import java.util.*; import java.util.concurrent.atomic.AtomicReference; import static org.junit.Assert.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; /** 调用真实暂存/提交分支,外部持久化和引擎边界使用替身。 */ public class WorkflowDraftLifecycleTest { private final Map, Object> dependencies = new HashMap<>(); private final AtomicReference stored = new AtomicReference<>(); private final AtomicReference currentUser = new AtomicReference<>(); private MockedStatic users; private TaskMapper tasks; private ServiceUtil forms; private FilePermissionSnapshotService snapshots; private FlowUtil flow; private OperatorUtil operator; @Before public void setUp() throws Exception { SpringContext.setApplicationContext(mock(ApplicationContext.class)); currentUser.set(user("owner")); users = mockStatic(UserProvider.class); users.when(UserProvider::getUser).thenAnswer(call -> currentUser.get()); users.when(UserProvider::getLoginUserId).thenAnswer(call -> currentUser.get().getUserId()); tasks = dependency(TaskMapper.class); forms = dependency(ServiceUtil.class); snapshots = dependency(FilePermissionSnapshotService.class); flow = spy(construct(FlowUtil.class)); dependencies.put(FlowUtil.class, flow); operator = spy(construct(OperatorUtil.class)); when(tasks.selectById(anyString())).thenAnswer(call -> stored.get()); when(tasks.insert(any(TaskEntity.class))).thenAnswer(call -> { stored.set(call.getArgument(0)); return 1; }); when(tasks.updateById(any(TaskEntity.class))).thenReturn(1); doAnswer(call -> { ((TaskEntity) call.getArgument(0)).setFilePermissionSnapshot("draft-snapshot"); return null; }).when(snapshots).prepareNewTask(any(), anyString(), anyMap()); TemplateEntity template = new TemplateEntity(); template.setId("template"); template.setFlowId("version"); template.setFullName("测试流程"); template.setType(0); TemplateJsonEntity version = new TemplateJsonEntity(); version.setId("version"); version.setTemplateId("template"); version.setVersion("1"); version.setFlowableId("deployment"); when(dependency(TemplateJsonMapper.class).selectById("version")).thenReturn(version); when(dependency(TemplateMapper.class).getInfo("template")).thenReturn(template); TemplateNodeEntity global = new TemplateNodeEntity(); global.setNodeCode("global"); global.setNodeType("global"); global.setNodeJson("{\"type\":\"global\",\"defaultContent\":\"测试流程\"}"); TemplateNodeEntity start = new TemplateNodeEntity(); start.setNodeCode("start"); start.setNodeType("start"); start.setFormId("form"); start.setNodeJson("{\"type\":\"start\",\"nodeId\":\"start\",\"nodeName\":\"发起\"}"); when(dependency(TemplateNodeMapper.class).getList("version")).thenReturn(List.of(global, start)); } @After public void tearDown() { users.close(); FlowStatusHolder.clear(); } @Test public void menuFirstSaveCreatesOwnedTaskBeforeSavingFormWithoutStartingEngine() throws Exception { FlowModel model = draft(0); operator.saveOrSubmit(model); TaskEntity task = stored.get(); assertNotNull(task); assertEquals(Integer.valueOf(0), task.getStatus()); assertEquals("owner", task.getCreatorUserId()); assertNotNull(task.getCreatorTime()); assertNull(task.getInstanceId()); assertNull(task.getStartTime()); assertEquals("draft-snapshot", task.getFilePermissionSnapshot()); org.mockito.InOrder order = inOrder(snapshots, tasks, forms); order.verify(snapshots).prepareNewTask(eq(task), eq("version"), anyMap()); order.verify(tasks).insert(task); order.verify(forms).saveOrUpdateFormData(argThat(data -> task.getId().equals(data.getId()) && Boolean.TRUE.equals(data.getSkipRequiredValidation()))); verifyNoInteractions(dependency(FlowAbleUrl.class), dependency(RecordMapper.class), dependency(NodeRecordMapper.class), dependency(OperatorMapper.class)); } @Test public void flowLaunchFirstSaveUsesSameDraftTaskContract() throws Exception { operator.saveOrSubmit(draft(1)); assertEquals(Integer.valueOf(0), stored.get().getStatus()); assertEquals("owner", stored.get().getCreatorUserId()); verify(tasks).insert(any(TaskEntity.class)); verify(forms).saveOrUpdateFormData(argThat(data -> Boolean.TRUE.equals(data.getSkipRequiredValidation()))); } @Test public void repeatedMenuSaveReusesIdCreatorTimeAndSnapshot() throws Exception { FlowModel model = draft(0); operator.saveOrSubmit(model); TaskEntity task = stored.get(); Date created = task.getCreatorTime(); model.setId(task.getId()); operator.saveOrSubmit(model); assertEquals(task.getId(), model.getTaskEntity().getId()); assertEquals("owner", task.getCreatorUserId()); assertSame(created, task.getCreatorTime()); assertEquals("draft-snapshot", task.getFilePermissionSnapshot()); assertNotNull(task.getLastModifyTime()); verify(tasks, times(1)).insert(any(TaskEntity.class)); verify(tasks, times(1)).updateById(any(TaskEntity.class)); verify(snapshots, times(1)).prepareNewTask(any(), anyString(), anyMap()); verify(forms, times(2)).saveOrUpdateFormData(argThat(data -> Boolean.TRUE.equals(data.getSkipRequiredValidation()))); } @Test public void delegateSaveKeepsOriginalCreatorAndDelegate() throws Exception { FlowModel model = draft(0); operator.saveOrSubmit(model); TaskEntity task = stored.get(); task.setDelegateUserId("delegate"); currentUser.set(user("delegate")); model.setId(task.getId()); model.setUserInfo(currentUser.get()); model.setUserId("delegate"); model.setDelegateUser("forged-replacement"); operator.saveOrSubmit(model); assertEquals("owner", task.getCreatorUserId()); assertEquals("delegate", task.getDelegateUserId()); assertEquals("delegate", task.getLastModifyUserId()); } @Test public void otherUserCannotSaveEvenIfBusinessApplicantOrRequestedCreatorMatches() throws Exception { FlowModel model = draft(0); operator.saveOrSubmit(model); currentUser.set(user("stranger")); model.setId(stored.get().getId()); model.setUserInfo(currentUser.get()); model.setUserId("stranger"); model.getFormData().put("shenqing_ren_id", "stranger"); try { operator.saveOrSubmit(model); fail("must reject another user's draft"); } catch (WorkFlowException expected) { } verify(forms, times(1)).saveOrUpdateFormData(any(FlowFormDataModel.class)); verify(tasks, never()).updateById(any(TaskEntity.class)); assertEquals("owner", stored.get().getCreatorUserId()); } @Test public void failedTaskInsertDoesNotSaveBusinessForm() throws Exception { when(tasks.insert(any(TaskEntity.class))).thenReturn(0); try { operator.saveOrSubmit(draft(0)); fail("must fail the save"); } catch (WorkFlowException expected) { } verify(forms, never()).saveOrUpdateFormData(any(FlowFormDataModel.class)); } @Test public void submissionStartsEngineAndUpdatesOriginalDraftInsteadOfInsertingAnotherTask() throws Exception { FlowModel model = draft(0); operator.saveOrSubmit(model); String id = stored.get().getId(); model.setId(id); model.setStatus(TaskStatusEnum.RUNNING.getCode()); // 即使携带自动草稿标记,正式提交也必须恢复必填校验。 model.setServerAutoDraft(true); when(dependency(FlowAbleUrl.class).startInstance(eq("deployment"), anyMap())).thenReturn("engine-instance"); when(forms.infoData("form", id)).thenReturn(new HashMap<>()); doNothing().when(flow).create(any(FlowModel.class), anyString(), anyList(), any()); doNothing().when(flow).createLaunchUser(anyString(), anyString()); doReturn(Collections.emptyList()).when(operator).handleOperator(any(FlowModel.class)); doNothing().when(operator).systemAudit(); operator.saveOrSubmit(model); assertEquals(id, stored.get().getId()); assertEquals(Integer.valueOf(1), stored.get().getStatus()); assertEquals("engine-instance", stored.get().getInstanceId()); assertNotNull(stored.get().getStartTime()); assertEquals("owner", stored.get().getCreatorUserId()); verify(tasks, times(1)).insert(any(TaskEntity.class)); verify(snapshots, times(1)).prepareNewTask(any(), anyString(), anyMap()); verify(dependency(FlowAbleUrl.class), times(1)).startInstance(eq("deployment"), anyMap()); verify(forms).saveOrUpdateFormData(argThat(data -> Boolean.TRUE.equals(data.getSkipRequiredValidation()))); verify(forms).saveOrUpdateFormData(argThat(data -> !Boolean.TRUE.equals(data.getSkipRequiredValidation()))); } @Test public void revisionAttachmentFailureBeforeEngineStartLeavesExistingDraftUntouched() throws Exception { FlowModel model = draft(0); operator.saveOrSubmit(model); model.setId(stored.get().getId()); model.setStatus(TaskStatusEnum.RUNNING.getCode()); var guard = dependency(jnpf.flowable.service.DmsLifecycleSubmissionGuard.class); when(guard.beforeSave(anyString(), anyString(), eq(1), anyMap())).thenAnswer(call -> { model.getFormData().put(jnpf.flowable.model.filepermission.DmsRevisionAttachmentCheck.REQUIRED, true); return true; }); doThrow(new WorkFlowException("修订附件内容不同")).when(forms).saveOrUpdateFormData(any()); try { operator.saveOrSubmit(model); fail("must reject changed content"); } catch (WorkFlowException expected) { assertTrue(expected.getMessage().contains("附件")); } verifyNoInteractions(dependency(jnpf.flowable.service.DmsLifecycleEngineRecovery.class), dependency(FlowAbleUrl.class)); verify(tasks, never()).updateById(any(TaskEntity.class)); assertEquals(Integer.valueOf(0), stored.get().getStatus()); } @Test public void ownerAndDelegateCanDeleteButOtherUserCannot() throws Exception { TaskEntity task = new TaskEntity(); task.setId("task"); task.setStatus(0); task.setCreatorUserId("owner"); task.setDelegateUserId("delegate"); when(tasks.getOrderStaList(anyList())).thenReturn(List.of(task)); TaskUtil deletion = spy(construct(TaskUtil.class)); doNothing().when(deletion).delete(anyList()); deletion.delete(List.of("task"), false); currentUser.set(user("delegate")); deletion.delete(List.of("task"), false); currentUser.set(user("stranger")); try { deletion.delete(List.of("task"), false); fail("must reject deletion"); } catch (WorkFlowException expected) { } verify(deletion, times(2)).delete(anyList()); } @Test public void taskOwnershipFailsClosedWithoutIdentityOrTask() { TaskEntity task = new TaskEntity(); task.setCreatorUserId("owner"); task.setDelegateUserId("delegate"); assertTrue(TaskInitiatorPermission.canOperate(task, "owner")); assertTrue(TaskInitiatorPermission.canOperate(task, "delegate")); assertFalse(TaskInitiatorPermission.canOperate(task, "applicant")); assertFalse(TaskInitiatorPermission.canOperate(task, null)); assertFalse(TaskInitiatorPermission.canOperate(task, " ")); assertFalse(TaskInitiatorPermission.canOperate(null, "owner")); } private FlowModel draft(int entry) { FlowModel model = new FlowModel(); model.setFlowId("version"); model.setIsFlow(entry); model.setStatus(0); model.setUserInfo(currentUser.get()); return model; } private static UserInfo user(String id) { UserInfo user = new UserInfo(); user.setUserId(id); user.setUserName(id); return user; } private T dependency(Class type) { return type.cast(dependencies.computeIfAbsent(type, key -> mock(type))); } private T construct(Class type) throws Exception { Constructor constructor = type.getConstructors()[0]; Object[] arguments = Arrays.stream(constructor.getParameterTypes()).map(this::dependency).toArray(); return type.cast(constructor.newInstance(arguments)); } }