package jnpf.flowable.service.dev; import jnpf.base.UserInfo; import jnpf.base.entity.VisualdevEntity; import jnpf.permission.entity.UserEntity; import jnpf.flowable.model.candidates.CandidateListModel; import jnpf.exception.WorkFlowException; import jnpf.flowable.entity.*; import jnpf.flowable.model.candidates.CandidateCheckVo; import jnpf.flowable.model.task.FlowModel; import jnpf.flowable.model.templatenode.nodejson.NodeModel; import jnpf.flowable.model.util.DevApprovalActor; import jnpf.flowable.service.OperatorService; import jnpf.flowable.service.TaskService; import jnpf.flowable.util.*; import jnpf.util.UserProvider; import org.junit.Test; import org.mockito.MockedStatic; import java.util.*; import java.util.concurrent.TimeUnit; import static org.junit.Assert.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; public class DevApprovalServiceTest { @org.junit.BeforeClass public static void context() { jnpf.util.context.SpringContext.setApplicationContext(mock(org.springframework.context.ApplicationContext.class)); } private final DevApprovalSignatures signatures = mock(DevApprovalSignatures.class); private final DevApprovalProperties properties = new DevApprovalProperties(); private final FlowUtil flow = mock(FlowUtil.class); private final OperatorUtil operators = mock(OperatorUtil.class); private final OperatorService operatorService = mock(OperatorService.class); private final TaskService tasks = mock(TaskService.class); private final ServiceUtil forms = mock(ServiceUtil.class); private final DevApprovalLocks locks = mock(DevApprovalLocks.class); private final DevApprovalStep step = mock(DevApprovalStep.class); private final DevApprovalService service = new DevApprovalService(properties, flow, operators, operatorService, tasks, forms, locks, step, signatures, mock(jnpf.base.DictionaryDataApi.class)); private final TaskEntity task = new TaskEntity(); private final NodeModel node = new NodeModel(); private final OperatorEntity operator = new OperatorEntity(); private final TemplateNodeEntity definition = new TemplateNodeEntity(); private void enableLocalRuntime() { var environment = new org.springframework.core.env.StandardEnvironment(); environment.setActiveProfiles("workflow-dev-tools"); properties.setRuntime(environment); } private void ready() throws Exception { enableLocalRuntime(); DevApprovalSignature signature = new DevApprovalSignature(); signature.setId("signed"); signature.setUserId("signer"); when(signatures.require("task", "signed")).thenReturn(signature); UserEntity signer = new UserEntity(); signer.setId("signer"); signer.setEnabledMark(1); when(forms.getUserInfo(anyString())).thenReturn(signer); VisualdevEntity release = new VisualdevEntity(); release.setFormData("{\"fields\":[]}"); when(forms.getReleaseInfo("form")).thenReturn(release); properties.setEnabled(true); properties.setEnvironment("development"); properties.setFormIds(Set.of("form")); task.setId("task"); task.setFlowId("flow-v2"); task.setStatus(1); node.setType("approver"); node.setNodeId("review"); definition.setNodeCode("review"); definition.setFormId("form"); definition.setNodeJson("{}"); TemplateNodeEntity start = new TemplateNodeEntity(); start.setNodeType("start"); start.setFormId("form"); doAnswer(call -> { FlowModel model = call.getArgument(1); model.setTaskEntity(task); model.setNodeEntityList(Arrays.asList(start, definition)); model.setNodes(new HashMap<>(Map.of("review", node, "global", new NodeModel()))); return null; }).when(flow).setFlowModel(eq("task"), any()); operator.setId("op"); operator.setTaskId("task"); operator.setNodeCode("review"); operator.setNodeId("node"); operator.setNodeName("质量审核"); operator.setHandleId("original-user"); operator.setStatus(1); operator.setCompletion(0); when(operatorService.getList("task")).thenReturn(List.of(operator)); when(forms.infoData("form", "task")).thenReturn(new HashMap<>(Map.of("name", "saved"))); when(tasks.checkCandidates(eq("op"), any())).thenReturn(new CandidateCheckVo()); when(tasks.getInfo("task")).thenReturn(task); when(locks.lock(anyString(), anyString(), anyLong(), any(TimeUnit.class))).thenReturn(true); } private MockedStatic admin() { MockedStatic users = mockStatic(UserProvider.class); UserInfo user = new UserInfo(); user.setUserId("admin"); user.setIsAdministrator(true); users.when(UserProvider::getUser).thenReturn(user); users.when(UserProvider::getLoginUserId).thenReturn("admin"); return users; } @Test public void disabledByDefaultAndRequiresExplicitDevelopment() { enableLocalRuntime(); assertFalse(properties.available()); properties.setEnabled(true); assertFalse(properties.available()); properties.setEnvironment("development"); assertTrue(properties.available()); properties.setFormIds(Set.of("form")); assertTrue(properties.available()); properties.setEnvironment("production"); assertFalse(properties.available()); } @Test public void productionOrMissingProfileCannotEnableEvenWithFlag() { properties.setEnabled(true); properties.setEnvironment("development"); assertFalse(properties.available()); var env = new org.springframework.core.env.StandardEnvironment(); properties.setRuntime(env); for (String[] profiles : List.of(new String[]{}, new String[]{"production"}, new String[]{"workflow-dev-tools", "production"}, new String[]{"workflow-dev-tools", "prod"})) { env.setActiveProfiles(profiles); assertFalse(properties.available()); } env.setActiveProfiles("workflow-dev-tools"); assertTrue(properties.available()); } @Test public void nonAdministratorCannotInspectOrAdvance() throws Exception { ready(); try (MockedStatic users = mockStatic(UserProvider.class)) { UserInfo user = new UserInfo(); user.setIsAdministrator(false); users.when(UserProvider::getUser).thenReturn(user); assertFalse(service.inspect("task", "signed").isEnabled()); try { service.approve("task", "op", "signed"); fail(); } catch (WorkFlowException expected) { } verifyNoInteractions(flow, step, locks); } } @Test public void nonAllowlistedFormIsNotOperated() throws Exception { ready(); properties.setFormIds(Set.of("other")); try (MockedStatic ignored = admin()) { assertFalse(service.inspect("task", "signed").isEnabled()); assertEquals("BLOCKED", service.approve("task", "op", "signed").getStatus()); verifyNoInteractions(step); } } @Test public void buttonAvailabilityDoesNotReadFormsOrCalculateCandidates() throws Exception { ready(); clearInvocations(forms, tasks, operatorService); when(forms.infoData("form", "task")).thenThrow(new IllegalStateException("form unavailable")); clearInvocations(forms); try (MockedStatic ignored = admin()) { DevApprovalService.State state = service.inspect("task", null); assertTrue(state.isEnabled()); assertEquals("READY", state.getStatus()); verifyNoInteractions(forms, tasks, operatorService, step); try { service.inspect("task", "signed"); fail("Signed inspection must still read and validate the form"); } catch (IllegalStateException expected) { assertEquals("form unavailable", expected.getMessage()); } } } @Test public void emptyAllowlistApprovesOtherWorkflowAndNodeForms() throws Exception { ready(); properties.setFormIds(Set.of()); definition.setFormId("other-node-form"); when(forms.infoData("other-node-form", "task")).thenReturn(new HashMap<>(Map.of("name", "saved"))); VisualdevEntity release = forms.getReleaseInfo("form"); when(forms.getReleaseInfo("other-node-form")).thenReturn(release); try (MockedStatic ignored = admin()) { assertTrue(service.inspect("task", "signed").isEnabled()); assertEquals("ADVANCED", service.approve("task", "op", "signed").getStatus()); verify(step).approve(eq(operator), any(), eq("signer")); } } @Test public void missingRequiredFieldNamesNodeAndFieldWithoutApproving() throws Exception { ready(); node.setFormOperates(List.of(Map.of("id", "reason", "name", "修订原因", "required", true))); try (MockedStatic ignored = admin()) { DevApprovalService.State result = service.approve("task", "op", "signed"); assertEquals("质量审核", result.getNodeName()); assertEquals("BLOCKED", result.getStatus()); assertTrue(result.getProblems().get(0).contains("修订原因")); verifyNoInteractions(step); } } @Test public void signatureCannotBeFabricated() throws Exception { ready(); node.setHasSign(true); try (MockedStatic ignored = admin()) { assertTrue(service.approve("task", "op", "signed").getProblems().get(0).contains("手写签名")); verifyNoInteractions(step); } } @Test public void candidateSelectionStopsBeforeMutation() throws Exception { ready(); CandidateCheckVo choice = new CandidateCheckVo(); choice.setType(2); when(tasks.checkCandidates(anyString(), any())).thenReturn(choice); try (MockedStatic ignored = admin()) { assertEquals("BLOCKED", service.approve("task", "op", "signed").getStatus()); verifyNoInteractions(step); } } @Test public void candidateAndApprovalActorUseVerifiedSignerInsteadOfAdmin() throws Exception { ready(); CandidateCheckVo choice = new CandidateCheckVo(); choice.setType(2); CandidateListModel next = new CandidateListModel(); next.setNodeCode("next"); next.setIsCandidates(true); choice.setList(List.of(next)); when(tasks.checkCandidates(anyString(), any())).thenReturn(choice); try (MockedStatic ignored = admin()) { assertEquals("ADVANCED", service.approve("task", "op", "signed").getStatus()); org.mockito.ArgumentCaptor capture = org.mockito.ArgumentCaptor.forClass(FlowModel.class); verify(step).approve(eq(operator), capture.capture(), eq("signer")); assertEquals(List.of("signer"), capture.getValue().getCandidateList().get("next")); assertEquals("signed", capture.getValue().getFormData().get("biz_sign")); } } @Test public void requiredTextIsFilledFromPublishedSchemaAndExistingValuesSurvive() throws Exception { ready(); node.setFormOperates(List.of(Map.of("id", "reason", "name", "原因", "required", true))); VisualdevEntity release = new VisualdevEntity(); release.setFormData("{\"fields\":[{\"__vModel__\":\"reason\",\"__config__\":{\"jnpfKey\":\"input\"}}]}"); when(forms.getReleaseInfo("form")).thenReturn(release); try (MockedStatic ignored = admin()) { assertEquals("ADVANCED", service.approve("task", "op", "signed").getStatus()); org.mockito.ArgumentCaptor capture = org.mockito.ArgumentCaptor.forClass(FlowModel.class); verify(step).approve(eq(operator), capture.capture(), eq("signer")); assertEquals("已输入", capture.getValue().getFormData().get("reason")); assertEquals("saved", capture.getValue().getFormData().get("name")); } } @Test public void frontendCustomValidationIsNotSilentlySkipped() throws Exception { ready(); definition.setNodeJson("{\"approveFuncConfig\":{\"validateFuncConfig\":{\"on\":true}}}"); try (MockedStatic ignored = admin()) { assertEquals("BLOCKED", service.approve("task", "op", "signed").getStatus()); verifyNoInteractions(step); } } @Test public void changedPendingIdDoesNotApproveDifferentOperator() throws Exception { ready(); try (MockedStatic ignored = admin()) { try { service.approve("task", "stale", "signed"); fail(); } catch (WorkFlowException expected) { } verifyNoInteractions(step); verify(locks).unlock(eq("dev-approve:task"), anyString()); } } @Test public void nativeNodeLockPreventsConcurrentApproval() throws Exception { ready(); when(locks.lock(eq("tasknode"), anyString(), anyLong(), any())).thenReturn(false); try (MockedStatic ignored = admin()) { try { service.approve("task", "op", "signed"); fail(); } catch (WorkFlowException expected) { } verifyNoInteractions(step); verify(locks, never()).unlock(eq("tasknode"), anyString()); } } @Test public void successfulStepKeepsCallbacksAndDoesNotSweepUncheckedNodes() throws Exception { ready(); try (MockedStatic ignored = admin()) { assertEquals("ADVANCED", service.approve("task", "op", "signed").getStatus()); org.mockito.InOrder order = inOrder(step, operators); order.verify(step).approve(eq(operator), any(), eq("signer")); order.verify(operators).handleOperator(); order.verify(operators).handleTaskStatusThenEvent(); order.verify(operators).launchTrigger(any()); order.verify(operators).handleTaskStatus(); verify(operators, never()).autoAudit(any(FlowModel.class)); assertEquals("original-user", operator.getHandleId()); verify(forms, times(1)).infoData("form", "task"); verify(tasks, times(1)).checkCandidates(eq("op"), any()); } } @Test public void failureCompensatesAndDoesNotReportSuccessOrSendCompletionEvents() throws Exception { ready(); doThrow(new WorkFlowException("业务校验失败")).when(step).approve(any(), any(), anyString()); try (MockedStatic ignored = admin()) { try { service.approve("task", "op", "signed"); fail(); } catch (WorkFlowException expected) { } verify(operators).compensate(task); verify(operators, never()).handleTaskStatusThenEvent(); verify(locks).unlock(eq("tasknode"), anyString()); } } @Test public void staleEngineTaskReportsRecoveryWithoutRetryingApproval() throws Exception { ready(); doThrow(new WorkFlowException("任务不存在")).when(step).approve(any(), any(), anyString()); try (MockedStatic ignored = admin()) { try { service.approve("task", "op", "signed"); fail(); } catch (WorkFlowException expected) { assertTrue(expected.getMessage().contains("已执行待办恢复")); } verify(step, times(1)).approve(any(), any(), anyString()); verify(operators).compensate(task); verify(operators, never()).handleTaskStatusThenEvent(); } } @Test public void failedCompensationDoesNotClaimTaskRecovered() throws Exception { ready(); doThrow(new WorkFlowException("任务不存在")).when(step).approve(any(), any(), anyString()); doThrow(new WorkFlowException("引擎不可用")).when(operators).compensate(task); try (MockedStatic ignored = admin()) { try { service.approve("task", "op", "signed"); fail(); } catch (WorkFlowException expected) { assertTrue(expected.getMessage().contains("恢复未完成")); assertTrue(expected.getMessage().contains("任务不存在")); assertFalse(expected.getMessage().contains("已执行待办恢复")); } verify(step, times(1)).approve(any(), any(), anyString()); verify(operators, never()).handleTaskStatusThenEvent(); } } @Test public void completedTaskIsIdempotent() throws Exception { ready(); task.setStatus(2); try (MockedStatic ignored = admin()) { assertEquals("PASSED", service.approve("task", "op", "signed").getStatus()); verifyNoInteractions(step); } } @Test public void attributionIsScopedToOneOperatorAndAlwaysCleared() { assertEquals("assigned", DevApprovalActor.resolve("op", "assigned")); try (DevApprovalActor ignored = DevApprovalActor.open("op", "admin")) { assertEquals("admin", DevApprovalActor.resolve("op", "assigned")); assertEquals("assigned", DevApprovalActor.resolve("other", "assigned")); } assertEquals("assigned", DevApprovalActor.resolve("op", "assigned")); } @Test public void pendingExcludesUnactivatedAndPreviouslyHandledOperators() { operator.setStatus(-2); assertFalse(DevApprovalService.pending(operator)); operator.setStatus(1); assertTrue(DevApprovalService.pending(operator)); operator.setHandleStatus(1); assertFalse(DevApprovalService.pending(operator)); } }