package jnpf.base.service; import cn.dev33.satoken.stp.StpUtil; import jnpf.base.ActionResult; import jnpf.base.ModuleButtonApi; import jnpf.base.entity.ModuleButtonEntity; import jnpf.base.entity.VisualdevEntity; import jnpf.base.model.button.ModuleButtonModel; import jnpf.base.model.uploadedit.UploadEditContext; import jnpf.base.util.FlowFormDataUtil; import jnpf.exception.DataException; import jnpf.flowable.WorkFlowApi; import jnpf.flowable.entity.TaskEntity; import jnpf.flowable.enums.TaskStatusEnum; import jnpf.onlyoffice.UploadFileCapabilityApi; import jnpf.flowable.entity.TemplateNodeEntity; import jnpf.flowable.model.filepermission.FilePermissionView; import jnpf.flowable.model.filepermission.FilePermissionPolicy.Action; import jnpf.util.UserProvider; import org.junit.Before; import org.junit.Test; import org.mockito.MockedStatic; import org.springframework.beans.factory.ObjectProvider; import java.util.*; import static org.junit.Assert.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; public class UploadEditAccessTest { private UploadEditAccess access; private UploadEditContext context; private WorkFlowApi workflow; private VisualdevEntity model; private UploadFileCapabilityApi capabilities; private FlowFormDataUtil form; private ModuleButtonApi buttons; private Map data; private VisualdevService modelService; private UploadEditReadService sharedReads; @Before @SuppressWarnings("unchecked") public void setup() { jnpf.util.context.SpringContext.setApplicationContext(mock(org.springframework.context.ApplicationContext.class)); ObjectProvider models = mock(ObjectProvider.class); modelService = mock(VisualdevService.class); when(models.getObject()).thenReturn(modelService); ObjectProvider forms = mock(ObjectProvider.class); form = mock(FlowFormDataUtil.class); when(forms.getObject()).thenReturn(form); buttons = mock(ModuleButtonApi.class); workflow = mock(WorkFlowApi.class); when(workflow.filePermissions("alias", "model")).thenReturn(FilePermissionView.legacy("alias", "model")); capabilities = mock(UploadFileCapabilityApi.class); ActionResult> options = new ActionResult<>(); options.setCode(200); options.setData(Collections.singletonMap("editEnabled", true)); when(capabilities.options()).thenReturn(options); model = new VisualdevEntity(); model.setFormData("{\"fields\":[{\"__vModel__\":\"files\",\"onlineEdit\":true,\"__config__\":{\"jnpfKey\":\"uploadFile\"}}]}"); model.setColumnData("{}"); when(modelService.getReleaseInfo("model")).thenReturn(model); sharedReads = mock(UploadEditReadService.class); when(sharedReads.model("model")).thenReturn(model); when(sharedReads.dmsAttachment(any(), any(), anyMap())).thenReturn(null); data = new HashMap<>(); data.put("id", "real-record"); data.put("flowTaskId", "alias"); data.put("files", Collections.singletonList(Collections.singletonMap("fileId", "source"))); when(form.getEditDataInfo(eq(model), anyString(), any())).thenReturn(data); access = new UploadEditAccess(models, forms, workflow, new UploadFilePermissionQuery(workflow, mock(ObjectProvider.class)), capabilities, buttons, sharedReads); context = new UploadEditContext(); context.setModelId("model"); context.setRecordId("alias"); context.setField("files"); } @Test public void copyLifecycleOnlyAppliesToUnsavedDmsAuditAttachmentFields() { context.setRecordId(null); context.setField("wenjian"); for (String table : List.of("dms_wenjian_xinzeng_shenhe", "dms_wenjian_xiuding_shenhe")) { model.setFormData("{\"tableName\":\""+table+"\"}"); assertTrue(access.isDmsAuditTemporary(context)); context.setTaskId("task"); assertFalse(access.isDmsAuditTemporary(context)); context.setTaskId(null); context.setRecordId("saved"); assertFalse(access.isDmsAuditTemporary(context)); context.setRecordId(null); } model.setFormData("{\"tableName\":\"other_business\"}"); assertFalse(access.isDmsAuditTemporary(context)); model.setFormData("{}"); assertFalse(access.isDmsAuditTemporary(context)); } @Test public void savedDmsAuditRejectsLegacyPermissionsForBothReadAndEdit() { model.setFormData("{\"tableName\":\"dms_wenjian_xiuding_shenhe\",\"fields\":[{\"__vModel__\":\"wenjian\",\"onlineEdit\":true,\"__config__\":{\"jnpfKey\":\"uploadFile\"}}]}"); context.setField("wenjian"); for (boolean shared : List.of(false, true)) { try { if (shared) access.sharedBaseline(context, "owner"); else access.checkedBaseline(context, "owner"); fail("legacy DMS permissions must be rejected"); } catch (DataException expected) { assertEquals(UploadEditAccess.DMS_INCOMPATIBLE, expected.getMessage()); } } } @Test public void newDmsAuditStillUsesCurrentTemporaryAuthorization() { model.setFormData("{\"tableName\":\"dms_wenjian_xinzeng_shenhe\",\"fields\":[{\"__vModel__\":\"wenjian\",\"onlineEdit\":true,\"__config__\":{\"jnpfKey\":\"uploadFile\"}}]}"); context.setField("wenjian"); context.setRecordId(null); try (MockedStatic auth = mockStatic(StpUtil.class)) { access.authorizeTemporary(context, "owner"); auth.verify(() -> StpUtil.checkPermission("model")); } } @Test public void nonWorkflowRecordUsesNativeIdAndOrdinaryPermission() { try (MockedStatic auth = mockStatic(StpUtil.class)) { assertEquals("source", access.baseline(context, "owner").get(0).get("fileId")); assertEquals("real-record", context.getRecordId()); auth.verify(() -> StpUtil.checkPermission("model")); } } @Test public void qianpiyePluginRequiresExplicitPublishedFieldSwitch() { assertFalse(access.qianpiyeAnnotatorEnabled(context)); model.setFormData("{\"fields\":[{\"__vModel__\":\"files\",\"onlineEdit\":true,\"qianpiyeAnnotatorEnabled\":true,\"__config__\":{\"jnpfKey\":\"uploadFile\"}}]}"); assertTrue(access.qianpiyeAnnotatorEnabled(context)); model.setFormData("{\"fields\":[{\"__vModel__\":\"files\",\"onlineEdit\":false,\"qianpiyeAnnotatorEnabled\":true,\"__config__\":{\"jnpfKey\":\"uploadFile\"}}]}"); assertFalse(access.qianpiyeAnnotatorEnabled(context)); } @Test public void unassociatedEditButtonDoesNotRestrictOnlineAttachmentEditing() { model.setColumnData("{\"useBtnPermission\":true,\"onlyAssociatedBtnPermission\":true}"); when(buttons.getListByModuleIdsForPermission(any(ModuleButtonModel.class))).thenReturn(Collections.emptyList()); try (MockedStatic auth = mockStatic(StpUtil.class)) { access.baseline(context, "owner"); auth.verify(() -> StpUtil.checkPermission("model")); auth.verify(() -> StpUtil.checkPermission("model::btn_edit"), never()); } } @Test public void associatedEditButtonStillRequiresPermission() { model.setColumnData("{\"useBtnPermission\":true,\"onlyAssociatedBtnPermission\":true}"); ModuleButtonEntity resource = new ModuleButtonEntity(); resource.setEnCode("btn_edit"); when(buttons.getListByModuleIdsForPermission(any(ModuleButtonModel.class))).thenReturn(Collections.singletonList(resource)); try (MockedStatic auth = mockStatic(StpUtil.class)) { access.baseline(context, "owner"); auth.verify(() -> StpUtil.checkPermission("model::btn_edit")); verify(buttons).getListByModuleIdsForPermission(argThat(query -> query.getIds().equals(Collections.singletonList("model")))); } } @Test public void associatedAddButtonStillRequiresPermissionForNewRecords() { model.setColumnData("{\"useBtnPermission\":true,\"onlyAssociatedBtnPermission\":true}"); context.setRecordId(""); ModuleButtonEntity resource = new ModuleButtonEntity(); resource.setEnCode("btn_add"); when(buttons.getListByModuleIdsForPermission(any(ModuleButtonModel.class))).thenReturn(Collections.singletonList(resource)); try (MockedStatic auth = mockStatic(StpUtil.class)) { access.baseline(context, "owner"); auth.verify(() -> StpUtil.checkPermission("model::btn_add")); } } @Test public void defaultModeRetainsStrictButtonPermission() { model.setColumnData("{\"useBtnPermission\":true}"); try (MockedStatic auth = mockStatic(StpUtil.class)) { access.baseline(context, "owner"); auth.verify(() -> StpUtil.checkPermission("model::btn_edit")); verifyNoInteractions(buttons); } } @Test public void missingAssociationResponseKeepsExistingPermissionCheck() { model.setColumnData("{\"useBtnPermission\":true,\"onlyAssociatedBtnPermission\":true}"); when(buttons.getListByModuleIdsForPermission(any(ModuleButtonModel.class))).thenReturn(null); try (MockedStatic auth = mockStatic(StpUtil.class)) { access.baseline(context, "owner"); auth.verify(() -> StpUtil.checkPermission("model::btn_edit")); } } @Test public void nativeMenuDraftWithoutTaskStillChecksOrdinaryPermissions() { data.put(FilePermissionNativeDraft.STATE_KEY, nativeState(0)); when(form.getFilePermissionDataInfo(eq(model), eq("real-record"), any())).thenReturn(data); when(workflow.filePermissions("alias", "model")).thenReturn(FilePermissionView.unavailable("alias", "model", "TASK_NOT_FOUND")); try (MockedStatic auth = mockStatic(StpUtil.class)) { assertFalse(access.checkedBaseline(context, "owner").isControlled()); assertTrue(access.allowDownload(context)); auth.verify(() -> StpUtil.checkPermission("model")); } } @Test public void absentTaskCannotRescueRunningDeletedOrUnverifiedRecord() { when(workflow.filePermissions("alias", "model")).thenReturn(FilePermissionView.unavailable("alias", "model", "TASK_NOT_FOUND")); when(form.getFilePermissionDataInfo(eq(model), anyString(), any())).thenReturn(data); rejects(() -> access.baseline(context, "owner")); data.put(FilePermissionNativeDraft.STATE_KEY, nativeState(1)); rejects(() -> access.baseline(context, "owner")); Map state = nativeState(0); state.put("deleteMark", 1); data.put(FilePermissionNativeDraft.STATE_KEY, state); rejects(() -> access.baseline(context, "owner")); data.put(FilePermissionNativeDraft.STATE_KEY, nativeState(0)); context.setTaskId("alias"); rejects(() -> access.baseline(context, "owner")); } @Test public void nativeDraftCannotConvertNetworkFailureIntoLegacy() { data.put(FilePermissionNativeDraft.STATE_KEY, nativeState(0)); when(workflow.filePermissions("alias", "model")).thenReturn(FilePermissionView.unavailable("alias", "model", "PERMISSION_UNAVAILABLE")); rejects(() -> access.baseline(context, "owner")); rejects(() -> access.allowDownload(context)); verify(form, never()).getFilePermissionDataInfo(any(), anyString(), any()); } private Map nativeState(int status) { Map state = new HashMap<>(); state.put("flowId", "flow"); state.put("flowState", status); state.put("deleteMark", null); return state; } @Test(expected = DataException.class) public void disabledCapabilityRejectsAccess() { ActionResult> options = new ActionResult<>(); options.setCode(200); options.setData(Collections.singletonMap("editEnabled", false)); when(capabilities.options()).thenReturn(options); access.baseline(context, "owner"); } @Test(expected = DataException.class) public void workflowCannotBeEditedByAnotherUser() { TaskEntity task = new TaskEntity(); task.setId("alias"); task.setCreatorUserId("someone-else"); task.setStatus(TaskStatusEnum.TO_BE_SUBMIT.getCode()); when(workflow.getInfoSubmit("alias")).thenReturn(task); access.baseline(context, "owner"); } @Test(expected = DataException.class) public void nonDraftWorkflowCannotBeEditedEvenByCreator() { TaskEntity task = new TaskEntity(); task.setId("alias"); task.setCreatorUserId("owner"); task.setStatus(-999); when(workflow.getInfoSubmit("alias")).thenReturn(task); access.baseline(context, "owner"); } @Test(expected = DataException.class) public void missingWorkflowIsNotTreatedAsOrdinaryRecord() { context.setTaskId("task"); access.baseline(context, "owner"); } @Test(expected = DataException.class) public void disabledParentCannotBeBypassedThroughChildField() { model.setFormData("{\"fields\":[{\"__vModel__\":\"rows\",\"disabled\":true,\"__config__\":{\"jnpfKey\":\"table\",\"children\":[{\"__vModel__\":\"files\",\"onlineEdit\":true,\"__config__\":{\"jnpfKey\":\"uploadFile\"}}]}}]}"); context.setParentField("rows"); context.setRowId("row"); access.baseline(context, "owner"); } @Test public void downloadUsesReleasedFieldAndDefaultsOnlyForOldSchemas() { assertTrue(access.allowDownload(context)); model.setFormData("{\"fields\":[{\"__vModel__\":\"files\",\"allowDownload\":false,\"__config__\":{\"jnpfKey\":\"uploadFile\"}}]}"); assertFalse(access.allowDownload(context)); model.setFormData(model.getFormData().replace("false", "\"false\"")); assertFalse(access.allowDownload(context)); } @Test public void downloadSeparatesMainAndChildFieldsWithSameName() { model.setFormData("{\"fields\":[{\"__vModel__\":\"files\",\"allowDownload\":true,\"__config__\":{\"jnpfKey\":\"uploadFile\"}},{\"__vModel__\":\"rows\",\"__config__\":{\"jnpfKey\":\"table\",\"children\":[{\"__vModel__\":\"files\",\"allowDownload\":false,\"__config__\":{\"jnpfKey\":\"uploadFile\"}}]}}]}"); assertTrue(access.allowDownload(context)); context.setParentField("rows"); assertFalse(access.allowDownload(context)); } @Test(expected = DataException.class) public void missingDownloadFieldDoesNotDefaultToAllowed() { context.setField("missing"); access.allowDownload(context); } @Test public void oldDraftStillUsesNativeCreatorAndNodePermissions() { TaskEntity task = task("owner", 0); TemplateNodeEntity start = new TemplateNodeEntity(); start.setNodeType("start"); start.setFormId("model"); start.setNodeJson("{\"formOperates\":[{\"id\":\"files\",\"write\":true}]}"); when(workflow.getNodesByFlowId(task.getFlowId())).thenReturn(List.of(start)); assertEquals("source", access.baseline(context, "owner").get(0).get("fileId")); start.setNodeJson("{\"formOperates\":[{\"id\":\"files\",\"write\":false}]}"); rejects(() -> access.baseline(context, "owner")); } @Test public void controlledCurrentHandlerCanEditWhileCreatorRuleNoLongerApplies() { task("creator", 1); when(workflow.filePermissions("alias", "model")).thenReturn(controlled(true, false)); try (MockedStatic users = mockStatic(UserProvider.class)) { users.when(UserProvider::getLoginUserId).thenReturn("handler"); assertEquals("source", access.baseline(context, "handler").get(0).get("fileId")); assertEquals("alias", context.getTaskId()); verify(workflow, never()).getNodesByFlowId(any()); } } @Test public void controlledDenialAndUnavailableDoNotFallBackToCreator() { task("owner", 0); try (MockedStatic users = mockStatic(UserProvider.class)) { users.when(UserProvider::getLoginUserId).thenReturn("owner"); when(workflow.filePermissions("alias", "model")).thenReturn(controlled(false, true)); rejects(() -> access.baseline(context, "owner")); when(workflow.filePermissions("alias", "model")).thenReturn(FilePermissionView.unavailable("alias", "model", "OFFLINE")); rejects(() -> access.baseline(context, "owner")); when(workflow.filePermissions("alias", "model")).thenReturn(null); rejects(() -> access.baseline(context, "owner")); } } @Test public void rejectsForeignPermissionContextAndOwner() { task("creator", 1); FilePermissionView view = controlled(true, true); when(workflow.filePermissions("alias", "model")).thenReturn(view); try (MockedStatic users = mockStatic(UserProvider.class)) { users.when(UserProvider::getLoginUserId).thenReturn("handler"); rejects(() -> access.baseline(context, "someone-else")); view.setTaskId("other-task"); rejects(() -> access.baseline(context, "handler")); } } @Test public void downloadRechecksActualRecordTaskAndDoesNotRequireEditing() { context.setTaskId("forged-task"); when(workflow.filePermissions("alias", "model")).thenReturn(controlled(false, true)); assertTrue(access.allowDownload(context)); when(workflow.filePermissions("alias", "model")).thenReturn(controlled(true, false)); assertFalse(access.allowDownload(context)); verify(workflow, never()).filePermissions(eq("forged-task"), any()); } @Test public void controlledMarkerComesFromRuntimeAndMissingTaskCannotDowngrade() { when(workflow.filePermissions("alias", "model")).thenReturn(controlled(true, true)); rejects(() -> access.checkedBaseline(context, "handler")); task("creator", 1); try (MockedStatic users = mockStatic(UserProvider.class)) { users.when(UserProvider::getLoginUserId).thenReturn("handler"); assertTrue(access.checkedBaseline(context, "handler").isControlled()); assertEquals("alias", context.getTaskId()); } } @Test public void missingTaskWithUnavailableRuntimeCannotTakeOrdinaryPath() { when(workflow.filePermissions("alias", "model")).thenReturn(null); rejects(() -> access.baseline(context, "owner")); when(workflow.filePermissions("alias", "model")).thenReturn(FilePermissionView.unavailable("alias", "model", "OFFLINE")); rejects(() -> access.baseline(context, "owner")); } private TaskEntity task(String creator, int status) { TaskEntity task = new TaskEntity(); task.setId("alias"); task.setCreatorUserId(creator); task.setStatus(status); task.setFlowId("flow"); when(workflow.getInfoSubmit("alias")).thenReturn(task); return task; } private FilePermissionView controlled(boolean edit, boolean download) { FilePermissionView view = new FilePermissionView(); view.setTaskId("alias"); view.setFormId("model"); view.setStatus("ready"); view.setRevision("state"); FilePermissionView.FieldAccess field = new FilePermissionView.FieldAccess(); field.setStatus("ready"); field.setActions(Map.of(Action.EDIT, edit, Action.VIEW, true, Action.DOWNLOAD, download)); view.getFields().put("files", field); return view; } private static void rejects(Runnable action) { try { action.run(); } catch (DataException expected) { return; } fail("expected access denial"); } @Test public void sharedReadWorksForViewerWithoutGlobalEditingCapability() { task("creator", 1); when(workflow.filePermissions("alias", "model")).thenReturn(controlled(false,false)); when(capabilities.options()).thenThrow(new IllegalStateException("edit disabled")); try (MockedStatic users = mockStatic(UserProvider.class)) { users.when(UserProvider::getLoginUserId).thenReturn("viewer"); assertEquals("source",access.sharedBaseline(context,"viewer").getFiles().get(0).get("fileId")); verifyNoInteractions(capabilities); context.setTaskId("other"); rejects(() -> access.sharedBaseline(context,"viewer")); } } @Test public void sharedReadUsesOneDefinitionAndRechecksItOnEachRequest() { task("creator", 1); context.setField("wenjian"); data.put("wenjian", data.get("files")); FilePermissionView view = controlled(false, false); view.getFields().put("wenjian", view.getFields().get("files")); when(workflow.filePermissions("alias", "model")).thenReturn(view); try (MockedStatic users = mockStatic(UserProvider.class)) { users.when(UserProvider::getLoginUserId).thenReturn("viewer"); for (String table : List.of("dms_wenjian_xinzeng_shenhe", "dms_wenjian_xiuding_shenhe", "other_business")) { model.setFormData("{\"tableName\":\"" + table + "\",\"fields\":[{\"__vModel__\":\"wenjian\",\"onlineEdit\":true,\"__config__\":{\"jnpfKey\":\"uploadFile\"}}]}"); clearInvocations(sharedReads); UploadEditAccess.CheckedBaseline result = access.sharedBaseline(context, "viewer"); assertEquals(!"other_business".equals(table), result.isDmsAudit()); assertEquals("source", result.getFiles().get(0).get("fileId")); verify(sharedReads, times(1)).model("model"); verify(modelService, never()).getReleaseInfo("model"); } when(workflow.filePermissions("alias", "model")).thenReturn(FilePermissionView.unavailable("alias", "model", "OFFLINE")); rejects(() -> access.sharedBaseline(context, "viewer")); } } @Test public void sharedDmsReadUsesRawProjectionAndNeverLoadsNativeDisplayData() { task("creator", 1); context.setField("wenjian"); data.put("wenjian", data.get("files")); model.setFormData("{\"tableName\":\"dms_wenjian_xinzeng_shenhe\",\"fields\":[{\"__vModel__\":\"wenjian\",\"onlineEdit\":true,\"__config__\":{\"jnpfKey\":\"uploadFile\"}}]}"); FilePermissionView view = controlled(false, false); view.getFields().put("wenjian", view.getFields().get("files")); when(workflow.filePermissions("alias", "model")).thenReturn(view); when(sharedReads.dmsAttachment(eq(model), eq(context), anyMap())).thenReturn(data); try (MockedStatic users = mockStatic(UserProvider.class)) { users.when(UserProvider::getLoginUserId).thenReturn("viewer"); assertEquals("source", access.sharedBaseline(context, "viewer").getFiles().get(0).get("fileId")); verifyNoInteractions(form, modelService); when(sharedReads.dmsAttachment(eq(model), eq(context), anyMap())).thenThrow(new DataException("query failed")); rejects(() -> access.sharedBaseline(context, "viewer")); verifyNoInteractions(form, modelService); } } @Test public void batchSharesDefinitionAndPermissionsButNotAcrossRequests() { task("creator", 1); context.setField("wenjian"); data.put("wenjian", data.get("files")); model.setFormData("{\"tableName\":\"dms_wenjian_xinzeng_shenhe\",\"fields\":[{\"__vModel__\":\"wenjian\",\"onlineEdit\":true,\"__config__\":{\"jnpfKey\":\"uploadFile\"}}]}"); FilePermissionView view = controlled(false, false); view.getFields().put("wenjian", view.getFields().get("files")); when(workflow.filePermissions("alias", "model")).thenReturn(view); when(sharedReads.dmsAttachment(eq(model), any(), anyMap())).thenReturn(data); try (MockedStatic users = mockStatic(UserProvider.class)) { users.when(UserProvider::getLoginUserId).thenReturn("viewer"); assertEquals(2, access.sharedBaselines(List.of(context, context), "viewer").size()); verify(sharedReads, times(1)).model("model"); verify(workflow, times(1)).filePermissions("alias", "model"); verify(workflow, times(1)).getInfoSubmit("alias"); when(workflow.filePermissions("alias", "model")).thenReturn(FilePermissionView.unavailable("alias", "model", "OFFLINE")); rejects(() -> access.sharedBaselines(List.of(context), "viewer")); } } @Test public void batchRejectsCrossRecordBeforeAnyReads() { UploadEditContext other = new UploadEditContext(); other.setModelId("model"); other.setRecordId("other"); other.setField("files"); rejects(() -> access.sharedBaselines(List.of(context, other), "viewer")); verifyNoInteractions(sharedReads); } }