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<FilePermissionRuntimeSource> providers;
|
private FilePermissionRuntimeSource source;
|
private FilePermissionRuntimeService service;
|
private FilePermissionSnapshot snapshot;
|
private FilePermissionRuntime.Facts facts;
|
private MockedStatic<UserProvider> 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<String,Object> 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<Action,Boolean> 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<String,Object> 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<String,Object> 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<Action,Boolean> upper = new EnumMap<>(Action.class);
|
Map<Action,Boolean> 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());
|
}
|
|
}
|