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<Class<?>, Object> dependencies = new HashMap<>();
|
private final AtomicReference<TaskEntity> stored = new AtomicReference<>();
|
private final AtomicReference<UserInfo> currentUser = new AtomicReference<>();
|
private MockedStatic<UserProvider> 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> T dependency(Class<T> type) {
|
return type.cast(dependencies.computeIfAbsent(type, key -> mock(type)));
|
}
|
|
private <T> T construct(Class<T> type) throws Exception {
|
Constructor<?> constructor = type.getConstructors()[0];
|
Object[] arguments = Arrays.stream(constructor.getParameterTypes()).map(this::dependency).toArray();
|
return type.cast(constructor.newInstance(arguments));
|
}
|
}
|