package jnpf.flowable.service.dev; import jnpf.base.UserInfo; import jnpf.config.ConfigValueUtil; import jnpf.exception.WorkFlowException; import jnpf.util.UserProvider; import jnpf.database.util.TenantDataSourceUtil; import org.junit.*; import org.mockito.MockedStatic; import java.util.*; import java.util.concurrent.Executor; import static org.junit.Assert.*; import static org.mockito.Mockito.*; import static org.mockito.ArgumentMatchers.*; public class DevApprovalRunsTest { private final DevApprovalService service = mock(DevApprovalService.class); private final DevApprovalSignatures signatures = mock(DevApprovalSignatures.class); private final DevApprovalRuns runs = new DevApprovalRuns(service, signatures, mock(ConfigValueUtil.class)); private final List jobs = new ArrayList<>(); private MockedStatic users; private MockedStatic tenants; private UserInfo user; @Before public void setup() throws Exception { jnpf.util.context.SpringContext.setApplicationContext(mock(org.springframework.context.ApplicationContext.class)); users = mockStatic(UserProvider.class); tenants = mockStatic(TenantDataSourceUtil.class); user = new UserInfo(); user.setUserId("admin"); user.setTenantId("tenant"); user.setToken("session"); user.setIsAdministrator(true); users.when(UserProvider::getUser).thenReturn(user); users.when(() -> UserProvider.isValidToken("session")).thenReturn(true); when(service.allowed()).thenReturn(true); when(service.inspect("task", null)).thenReturn(state("READY", null)); var field = DevApprovalRuns.class.getDeclaredField("executor"); field.setAccessible(true); field.set(runs, (Executor) jobs::add); } private interface Checked { void run() throws Exception; } private void rejected(Checked action) throws Exception { try { action.run(); fail("Must reject"); } catch (WorkFlowException expected) { } } @After public void cleanup() { users.close(); tenants.close(); } private DevApprovalService.State state(String status, String operator) { var s = new DevApprovalService.State(); s.setEnabled(true); s.setStatus(status); s.setOperatorId(operator); return s; } @Test public void startReturnsBeforeExecutionAndRepeatedStartIsIdempotent() throws Exception { var first = runs.start("task", "sign"); assertEquals("RUNNING", first.status()); assertEquals(1, jobs.size()); assertEquals(first.runId(), runs.start("task", "sign").runId()); verify(service, never()).approve(anyString(), any(), anyString()); when(service.approve("task", null, "sign")).thenReturn(state("ADVANCED", "op1"), state("PASSED", "op2")); jobs.getFirst().run(); assertEquals("PASSED", runs.status("task").status()); assertEquals(2, runs.status("task").completed()); assertEquals(first.runId(), runs.start("task", "sign").runId()); assertEquals(1, jobs.size()); verify(service, times(2)).approve("task", null, "sign"); users.verify(UserProvider::clearLocalUser); } @Test public void disabledOrNonAdminCannotStartReadOrStop() throws Exception { when(service.allowed()).thenReturn(false); rejected(() -> runs.start("task", "sign")); rejected(() -> runs.status("task")); rejected(() -> runs.stop("task", "id")); assertTrue(jobs.isEmpty()); verifyNoInteractions(signatures); } @Test public void invalidSignatureNeverSchedules() throws Exception { when(signatures.require("task", "bad")).thenThrow(new WorkFlowException("签名无效")); rejected(() -> runs.start("task", "bad")); assertTrue(jobs.isEmpty()); } @Test public void ownerAndTenantIsolationAndStaleStop() throws Exception { var first = runs.start("task", "sign"); user.setUserId("other"); rejected(() -> runs.status("task")); rejected(() -> runs.start("task", "sign")); rejected(() -> runs.stop("task", first.runId())); user.setTenantId("another"); assertEquals("IDLE", runs.status("task").status()); user.setTenantId("tenant"); user.setUserId("admin"); rejected(() -> runs.stop("task", "stale")); } @Test public void stopCompletesCurrentStepAndNeverAdvancesNext() throws Exception { var first = runs.start("task", "sign"); when(service.approve("task", null, "sign")).thenAnswer(call -> { runs.stop("task", first.runId()); return state("ADVANCED", "op1"); }); jobs.getFirst().run(); assertEquals("STOPPED", runs.status("task").status()); assertEquals(1, runs.status("task").completed()); verify(service, times(1)).approve("task", null, "sign"); } @Test public void expiryAndBusinessFailureStopWithoutRetry() throws Exception { runs.start("task", "sign"); users.when(() -> UserProvider.isValidToken("session")).thenReturn(false); jobs.getFirst().run(); assertEquals("FAILED", runs.status("task").status()); verify(service, never()).approve(anyString(), any(), anyString()); users.when(() -> UserProvider.isValidToken("session")).thenReturn(true); when(service.approve("task", null, "new-sign")).thenThrow(new WorkFlowException("必填数据不完整")); runs.start("task", "new-sign"); jobs.get(1).run(); assertEquals("FAILED", runs.status("task").status()); assertEquals("必填数据不完整", runs.status("task").message()); verify(service, times(1)).approve("task", null, "new-sign"); } @Test public void longFlowsStopAtTheServerLimitAndReleaseCapacity() throws Exception { var sequence = new java.util.concurrent.atomic.AtomicInteger(); when(service.approve("task", null, "sign")).thenAnswer(call -> state("ADVANCED", "op" + sequence.incrementAndGet())); runs.start("task", "sign"); jobs.getFirst().run(); assertEquals("STOPPED", runs.status("task").status()); assertEquals(100, runs.status("task").completed()); verify(service, times(100)).approve("task", null, "sign"); assertEquals("RUNNING", runs.start("task", "new-sign").status()); assertEquals(2, jobs.size()); } }