package jnpf.flowable.service; import jnpf.base.UserInfo; import jnpf.flowable.model.filepermission.FilePermissionView; import jnpf.flowable.model.filepermission.FilePermissionPolicy.Action; import jnpf.flowable.util.filepermission.FilePermissionRuntime; import jnpf.flowable.util.filepermission.FilePermissionSnapshot; import jnpf.util.UserProvider; import org.junit.*; import org.mockito.MockedStatic; import org.springframework.beans.factory.ObjectProvider; import java.util.*; import static org.junit.Assert.*; import static org.mockito.Mockito.*; import static org.mockito.ArgumentMatchers.*; public class FilePermissionRuntimeServiceTest { private FilePermissionSnapshotService snapshots; private ObjectProvider providers; private FilePermissionRuntimeSource source; private FilePermissionRuntimeService service; private FilePermissionSnapshot snapshot; private FilePermissionRuntime.Facts facts; private MockedStatic users; @Before @SuppressWarnings("unchecked") public void setup() { jnpf.util.context.SpringContext.setApplicationContext(mock(org.springframework.context.ApplicationContext.class)); users = mockStatic(UserProvider.class); UserInfo user = new UserInfo(); user.setUserId("user"); user.setTenantId("tenant"); users.when(UserProvider::getUser).thenReturn(user); users.when(UserProvider::getLoginUserId).thenReturn("user"); snapshots = mock(FilePermissionSnapshotService.class); providers = mock(ObjectProvider.class); source = mock(FilePermissionRuntimeSource.class); when(providers.getIfAvailable()).thenReturn(source); service = new FilePermissionRuntimeService(snapshots, providers); Map rule = Map.of("id", "rule", "formId", "form", "field", "files", "trigger", "COMPLETED", "subject", "PARTICIPANTS", "actions", Map.of("DOWNLOAD", "DENY")); snapshot = FilePermissionSnapshot.read(FilePermissionSnapshot.create("tenant", "task", "flow", Map.of("version", 1, "rules", List.of(rule))), "tenant", "task"); when(snapshots.find("task")).thenReturn(snapshot); facts = new FilePermissionRuntime.Facts(); facts.tenantId="tenant"; facts.taskId="task"; facts.formId="form"; facts.userId="user"; facts.creatorId="user"; facts.revision="r1"; facts.verified=true; Map allow = Map.of(Action.VIEW, true, Action.DOWNLOAD, true); facts.nativeUpperBounds.put("files", allow); facts.inherited.put("files", allow); when(source.read("tenant", "task", "form", "user", snapshot)).thenReturn(facts); } @After public void cleanup() { if (users != null) users.close(); } @Test public void reviewRuleDoesNotGrantEditAndKeepsCommentsVisibleAfterCompletion() { var policy = Map.of("version", 1, "rules", List.of(Map.of("id", "review", "formId", "form", "field", "files", "trigger", "ACTIVE", "triggerNodeId", "audit", "subject", "CURRENT_HANDLERS", "actions", Map.of("REVIEW", "ALLOW", "EDIT", "DENY")))); var rules = jnpf.flowable.model.filepermission.FilePermissionPolicy.parse(policy); facts.activeNodes.add("audit"); facts.currentHandlers.put("audit", Set.of("user")); facts.nativeUpperBounds.put("files", Map.of(Action.VIEW, true, Action.EDIT, true, Action.REVIEW, true)); FilePermissionView active = FilePermissionRuntime.resolve(rules, facts); assertTrue(active.allows("files", Action.REVIEW)); assertFalse(active.allows("files", Action.EDIT)); assertTrue(active.getFields().get("files").isReviewEnabled()); facts.terminal = FilePermissionRuntime.Terminal.COMPLETED; FilePermissionView completed = FilePermissionRuntime.resolve(rules, facts); assertFalse(completed.allows("files", Action.REVIEW)); assertTrue(completed.getFields().get("files").isReviewEnabled()); assertFalse(FilePermissionRuntime.resolve(List.of(), facts).getFields().get("files").isReviewEnabled()); } @Test public void nullSnapshotAloneRetainsOldBehavior() { when(snapshots.find("task")).thenReturn(null); FilePermissionView view = service.current("task", "form"); assertFalse(view.isEnabled()); assertEquals("ready", view.getStatus()); verifyNoInteractions(providers, source); } @Test public void failedSnapshotReadIsNotLegacy() { when(snapshots.find("task")).thenThrow(new IllegalArgumentException("damaged snapshot")); denied(service.current("task", "form")); verifyNoInteractions(source); } @Test public void confirmedAbsentTaskHasExplicitReasonAndStillDeniesByDefault() { when(snapshots.find("task")).thenThrow(new FilePermissionSnapshotService.TaskNotFoundException()); FilePermissionView view = service.current("task", "form"); denied(view); assertEquals("TASK_NOT_FOUND", view.getReason()); verifyNoInteractions(source); } @Test public void missingRuntimeSourceIsNotLegacy() { when(providers.getIfAvailable()).thenReturn(null); denied(service.current("task", "form")); } @Test public void runtimeFailureIsNotLegacy() { when(source.read(anyString(), anyString(), anyString(), anyString(), any())).thenThrow(new IllegalStateException("unavailable")); denied(service.current("task", "form")); } @Test public void rejectsCrossTenantTaskFormAndUserFacts() { facts.tenantId="other"; denied(service.current("task", "form")); facts.tenantId="tenant"; facts.taskId="other"; denied(service.current("task", "form")); facts.taskId="task"; facts.formId="other"; denied(service.current("task", "form")); facts.formId="form"; facts.userId="other"; denied(service.current("task", "form")); } @Test public void rereadsCommittedStateForEveryAction() { assertTrue(service.current("task", "form").allows("files", Action.DOWNLOAD)); facts.terminal=FilePermissionRuntime.Terminal.COMPLETED; facts.revision="r2"; FilePermissionView next=service.current("task", "form"); assertFalse(next.allows("files", Action.DOWNLOAD)); assertTrue(next.allows("files", Action.VIEW)); assertEquals("r2", next.getRevision()); verify(source, times(2)).read("tenant", "task", "form", "user", snapshot); } @Test public void inconsistentReadsAndUnverifiedProjectionAreDenied() { facts.transitioning=true; denied(service.current("task", "form")); facts.transitioning=false; facts.verified=false; denied(service.current("task", "form")); } @Test public void invalidInputAndUnauthenticatedUserAreDenied() { denied(service.current("", "form")); denied(service.current("task", "")); users.when(UserProvider::getLoginUserId).thenReturn(""); denied(service.current("task", "form")); verifyNoInteractions(source); } @Test public void nonCurrentHandlersExcludeUsersHandlingAnyActiveNode() { Map current = new HashMap<>(Map.of( "id", "current", "formId", "form", "field", "files", "subject", "CURRENT_HANDLERS", "trigger", "ENTERED", "triggerNodeId", "department", "onReturn", "RELEASE", "actions", Map.of("VIEW", "ALLOW", "DOWNLOAD", "ALLOW", "EDIT", "ALLOW"))); Map nonCurrent = new HashMap<>(Map.of( "id", "non-current", "formId", "form", "field", "files", "subject", "NON_CURRENT_HANDLERS", "trigger", "ENTERED", "triggerNodeId", "department", "onReturn", "RELEASE", "actions", Map.of("VIEW", "ALLOW", "DOWNLOAD", "DENY", "EDIT", "DENY", "ADD", "DENY", "REPLACE", "DENY", "REMOVE", "DENY"))); FilePermissionSnapshot scoped = FilePermissionSnapshot.read(FilePermissionSnapshot.create("tenant", "task", "flow", Map.of("version", 1, "rules", List.of(current, nonCurrent))), "tenant", "task"); facts.activeEnteredRules.addAll(Set.of("current", "non-current")); facts.activeNodes.add("file-admin"); facts.participants.put("department", Set.of("user")); facts.currentHandlers.put("file-admin", Set.of("user")); Map upper = new EnumMap<>(Action.class); Map inherited = new EnumMap<>(Action.class); for (Action action : Action.values()) { upper.put(action, true); inherited.put(action, action == Action.VIEW || action == Action.DOWNLOAD); } facts.nativeUpperBounds.put("files", upper); facts.inherited.put("files", inherited); FilePermissionView active = FilePermissionRuntime.resolve(scoped.getRules(), facts); assertTrue(active.allows("files", Action.VIEW)); assertTrue(active.allows("files", Action.DOWNLOAD)); assertTrue(active.allows("files", Action.EDIT)); facts.currentHandlers.put("file-admin", Set.of("another-user")); FilePermissionView historical = FilePermissionRuntime.resolve(scoped.getRules(), facts); assertTrue(historical.allows("files", Action.VIEW)); assertFalse(historical.allows("files", Action.DOWNLOAD)); assertFalse(historical.allows("files", Action.EDIT)); facts.participants.clear(); facts.creatorId = "user"; FilePermissionView creator = FilePermissionRuntime.resolve(scoped.getRules(), facts); assertTrue(creator.allows("files", Action.VIEW)); assertFalse(creator.allows("files", Action.DOWNLOAD)); facts.activeNodes.add("parallel"); facts.currentHandlers.put("parallel", Set.of("user")); FilePermissionView parallelHandler = FilePermissionRuntime.resolve(scoped.getRules(), facts); assertTrue(parallelHandler.allows("files", Action.DOWNLOAD)); assertTrue(parallelHandler.allows("files", Action.EDIT)); } private static void denied(FilePermissionView view) { assertTrue(view.isEnabled()); assertEquals("error", view.getStatus()); assertFalse(view.allows("files", Action.DOWNLOAD)); assertTrue(view.getFields().isEmpty()); } @Test public void publishedFormReuseStillRechecksFactsAndRejectsStateOrIdentityChanges() { when(source.readWithPublishedForm("tenant","task","form","user",snapshot,"{}")).thenReturn(facts); assertEquals("ready",service.currentWithPublishedForm("task","form","{}").getStatus()); facts.transitioning = true; assertNotEquals("ready",service.currentWithPublishedForm("task","form","{}").getStatus()); facts.transitioning = false; facts.tenantId = "other"; assertEquals("INVALID_CONTEXT",service.currentWithPublishedForm("task","form","{}").getReason()); verify(snapshots,times(3)).find("task"); verify(source,times(3)).readWithPublishedForm("tenant","task","form","user",snapshot,"{}"); verify(source,never()).read(anyString(),anyString(),anyString(),anyString(),any()); } }