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<UserProvider> admin() {
|
MockedStatic<UserProvider> 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<UserProvider> 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<UserProvider> 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<UserProvider> 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<UserProvider> 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<UserProvider> 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<UserProvider> 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<UserProvider> 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<UserProvider> ignored = admin()) {
|
assertEquals("ADVANCED", service.approve("task", "op", "signed").getStatus());
|
org.mockito.ArgumentCaptor<FlowModel> 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<UserProvider> ignored = admin()) {
|
assertEquals("ADVANCED", service.approve("task", "op", "signed").getStatus());
|
org.mockito.ArgumentCaptor<FlowModel> 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<UserProvider> ignored = admin()) {
|
assertEquals("BLOCKED", service.approve("task", "op", "signed").getStatus());
|
verifyNoInteractions(step);
|
}
|
}
|
|
@Test public void changedPendingIdDoesNotApproveDifferentOperator() throws Exception {
|
ready();
|
try (MockedStatic<UserProvider> 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<UserProvider> 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<UserProvider> 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<UserProvider> 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<UserProvider> 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<UserProvider> 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<UserProvider> 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));
|
}
|
}
|