package jnpf.flowable.util; import jnpf.exception.WorkFlowException; import jnpf.base.ActionResult; import jnpf.flowable.entity.OperatorEntity; import jnpf.flowable.entity.TemplateNodeEntity; import jnpf.flowable.enums.EventEnum; import jnpf.flowable.enums.NodeEnum; import jnpf.flowable.enums.OperatorStateEnum; import jnpf.flowable.model.flowable.FlowAbleUrl; import jnpf.flowable.model.flowable.NextOrPrevFo; import jnpf.flowable.model.task.FlowModel; import jnpf.flowable.model.templatenode.nodejson.NodeModel; import jnpf.flowable.model.util.FlowContextHolder; import jnpf.flowable.model.util.FlowEventHolder; import jnpf.util.JsonUtil; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class BackEventsTest { @Mock private FlowAbleUrl engine; @Mock private FlowUtil flowUtil; @InjectMocks private OperatorUtil operatorUtil; private FlowModel model; private List operators; @Before public void setUp() { FlowEventHolder.clear(); FlowContextHolder.clearAll(); model = new FlowModel(); model.setDeploymentId("deployment"); model.getTaskEntity().setId("task"); operators = new ArrayList<>(); for (String code : Arrays.asList("A", "B", "C", "D")) { addNode(code, NodeEnum.APPROVER, OperatorStateEnum.RUNING); } model.setNodeEntity(model.getNodeEntityList().get(3)); } @After public void cleanUp() { FlowEventHolder.clear(); FlowContextHolder.clearAll(); } @Test public void sendsBackInReverseOrderExcludingTarget() throws Exception { graph(Map.of("D", List.of("C"), "C", List.of("B"), "B", List.of("A"))); assertEquals(List.of("D", "C", "B"), resolve(List.of("A"), List.of("B", "C", "D"))); } @Test public void disabledNodeDoesNotPreventEarlierConfiguredEvent() throws Exception { model.getNodes().get("C").getBackFuncConfig().setOn(false); graph(Map.of("D", List.of("C"), "C", List.of("B"), "B", List.of("A"))); assertEquals(List.of("D", "B"), resolve(List.of("A"), List.of("B", "C", "D"))); } @Test public void excludesInvalidUnvisitedFutureAndUnrelatedBranchNodes() throws Exception { operators.get(1).setStatus(OperatorStateEnum.FUTILITY.getCode()); addNode("X", NodeEnum.APPROVER, OperatorStateEnum.RUNING); addNode("future", NodeEnum.APPROVER, OperatorStateEnum.RUNING); addNode("unvisited", NodeEnum.APPROVER, OperatorStateEnum.WAITING); graph(Map.of("D", List.of("C", "unvisited"), "C", List.of("B"), "B", List.of("A"), "unvisited", List.of("A"))); assertEquals(List.of("D", "C"), resolve(List.of("A"), List.of("B", "C", "D", "X", "future", "unvisited"))); } @Test public void processingNodesParticipateAndDuplicateOperatorsRunOnce() throws Exception { model.getNodes().get("C").setType(NodeEnum.PROCESSING.getType()); operators.add(operators.get(2)); graph(Map.of("D", List.of("C"), "C", List.of("B"), "B", List.of("A"))); assertEquals(List.of("D", "C", "B"), resolve(List.of("A"), List.of("B", "C", "D"))); } @Test public void mergeWaitsForBothBranchesBeforeSharedPredecessor() throws Exception { addNode("X", NodeEnum.PROCESSING, OperatorStateEnum.RUNING); graph(Map.of("D", List.of("C", "X"), "C", List.of("B"), "X", List.of("B"), "B", List.of("A"))); List events = resolve(List.of("A"), List.of("B", "C", "X", "D")); assertEquals("D", events.get(0)); assertEquals(4, events.size()); assertTrue(events.indexOf("C") < events.indexOf("B")); assertTrue(events.indexOf("X") < events.indexOf("B")); } @Test public void stopsAtEverySelectedTarget() throws Exception { graph(Map.of("D", List.of("C", "B"), "C", List.of("A"))); assertEquals(List.of("D", "C"), resolve(List.of("A", "B"), List.of("B", "C", "D"))); } @Test public void noConfiguredIntermediateEventPreservesCurrentOnlyWithoutEngineCalls() throws Exception { model.getNodes().get("B").getBackFuncConfig().setInterfaceId(""); model.getNodes().get("C").getBackFuncConfig().setOn(false); assertEquals(List.of("D"), resolve(List.of("A"), List.of("B", "C", "D"))); verifyNoInteractions(engine); } @Test public void cycleTerminatesAndEachNodeRunsOnce() throws Exception { graph(Map.of("D", List.of("C"), "C", List.of("B", "D"), "B", List.of("A"))); assertEquals(List.of("D", "C", "B"), resolve(List.of("A"), List.of("B", "C", "D"))); } @Test public void queueDispatchesEachNodesOwnConfigurationAndFormInOrder() throws Exception { ServiceUtil service = org.mockito.Mockito.mock(ServiceUtil.class); FlowUtil eventUtil = new FlowUtil(null, null, service, null, null, null, null, null, null, null, null, null, null, null, null, null, null); List calls = new ArrayList<>(); doAnswer(invocation -> { FlowModel event = invocation.getArgument(0); eventUtil.event(event, invocation.getArgument(1), invocation.getArgument(2)); return null; }).when(flowUtil).event(any(FlowModel.class), any(Integer.class), any(Map.class)); doAnswer(invocation -> { calls.add(invocation.getArgument(0)); ActionResult result = new ActionResult<>(); result.setCode(200); return result; }).when(service).infoToId(any(), any()); // 校验真实事件处理器按各节点表单取数,而不是复用 D 的表单。 List forms = new ArrayList<>(); when(service.infoData(any(), any())).thenAnswer(invocation -> { forms.add(invocation.getArgument(0)); assertEquals("task", invocation.getArgument(1)); return Map.of("node", invocation.getArgument(0)); }); TemplateNodeEntity original = model.getNodeEntity(); operatorUtil.addBackEvents(model, List.of("D", "C", "B")); assertEquals(List.of("D", "C", "B"), FlowEventHolder.getAllEvent().stream() .map(e -> e.getFlowModel().getNodeEntity().getNodeCode()).collect(Collectors.toList())); assertTrue(FlowEventHolder.getAllEvent().stream().allMatch(e -> EventEnum.BACK.getStatus().equals(e.getStatus()))); assertSame(original, model.getNodeEntity()); assertEquals(Collections.emptyList(), calls); operatorUtil.handleEvent(); assertEquals(List.of("back-D", "back-C", "back-B"), calls); assertEquals(List.of("form-D", "form-C", "form-B"), forms); assertTrue(FlowEventHolder.getAllEvent().isEmpty()); } private void graph(Map> previous) throws WorkFlowException { when(engine.getPrev(any(NextOrPrevFo.class))).thenAnswer(invocation -> { NextOrPrevFo fo = invocation.getArgument(0); assertEquals("deployment", fo.getDeploymentId()); return previous.getOrDefault(fo.getTaskKey(), Collections.emptyList()); }); } private List resolve(List targets, List after) throws WorkFlowException { return operatorUtil.getBackEventNodeCodes(model, "D", targets, after, operators); } private void addNode(String code, NodeEnum type, OperatorStateEnum state) { NodeModel node = new NodeModel(); node.setNodeId(code); node.setType(type.getType()); node.getBackFuncConfig().setOn(true); node.getBackFuncConfig().setInterfaceId("back-" + code); model.getNodes().put(code, node); TemplateNodeEntity entity = new TemplateNodeEntity(); entity.setNodeCode(code); entity.setNodeType(type.getType()); entity.setFormId("form-" + code); entity.setNodeJson(JsonUtil.getObjectToString(node)); model.getNodeEntityList().add(entity); OperatorEntity operator = new OperatorEntity(); operator.setNodeCode(code); operator.setStatus(state.getCode()); operators.add(operator); } }