package jnpf.base.service; import jnpf.base.UserInfo; import jnpf.base.mapper.UploadEditDraftMapper; import jnpf.base.model.flow.DataModel; import jnpf.base.model.uploadedit.UploadEditDraft; import jnpf.exception.DataException; import jnpf.util.UserProvider; import org.junit.Before; import org.junit.After; import org.junit.Test; import org.mockito.MockedStatic; import java.util.*; import static org.junit.Assert.*; import static org.mockito.Mockito.*; import static org.mockito.ArgumentMatchers.*; public class UploadEditDraftServiceTest { private UploadEditDraftMapper mapper; private UploadEditDraftService service; private UploadEditAccess access; private UploadFilePermissionService permissions; private UploadEditTemporaryService temporary; private UploadEditDraft draft; private DataModel model; private MockedStatic users; @Before public void setup() { jnpf.util.context.SpringContext.setApplicationContext(mock(org.springframework.context.ApplicationContext.class)); UserInfo user = new UserInfo(); user.setUserId("owner"); user.setTenantId("tenant"); users = mockStatic(UserProvider.class); users.when(UserProvider::getUser).thenReturn(user); users.when(UserProvider::getLoginUserId).thenReturn("owner"); mapper = mock(UploadEditDraftMapper.class); access = mock(UploadEditAccess.class); when(access.hasEnabledFields("model")).thenReturn(true); temporary = mock(UploadEditTemporaryService.class); permissions = mock(UploadFilePermissionService.class); service = new UploadEditDraftService(mapper, access, temporary, permissions); draft = new UploadEditDraft(); draft.setId("draft"); draft.setModelId("model"); draft.setRecordId("record"); draft.setOwnerId("owner"); draft.setTenantId("tenant"); draft.setRevision(3L); draft.setField("files"); draft.setBaselineJson("[{\"fileId\":\"A\"}]"); draft.setCurrentJson("[{\"fileId\":\"B\"}]"); draft.setLeaseId("lease"); when(mapper.lockRecord("tenant", "model", "record")).thenReturn(Collections.singletonList(draft)); when(mapper.compareAndSet(any(), anyLong())).thenReturn(1); model = new DataModel(); model.setVisualId("model"); model.setMainId("record"); model.setDataNewMap(new HashMap<>()); model.getDataNewMap().put("files", Collections.singletonList(Collections.singletonMap("fileId", "B"))); Map revision = new HashMap<>(); revision.put("draftId", "draft"); revision.put("revision", 3L); model.getDataNewMap().put(UploadEditDraftService.SUBMIT_STATE, Collections.singletonList(revision)); } @After public void teardown() { if (users != null) users.close(); } @Test public void incompatibleDmsLoadDoesNotCreatePrivateDraft() { when(access.isDmsAudit(any())).thenReturn(true); when(access.checkedBaseline(any(), eq("owner"))).thenReturn(new UploadEditAccess.CheckedBaseline(List.of(), false)); assertIncompatible(() -> service.load(UploadEditDraftService.context(draft))); verifyNoInteractions(mapper); } @Test public void incompatibleDmsSaveIsRejectedEvenWithoutAnyDraft() { when(access.isDmsAudit(any())).thenReturn(true); when(access.hasEnabledFields("model")).thenReturn(false); assertIncompatible(() -> service.prepareSave(model, field -> false)); verifyNoInteractions(mapper); } @Test public void privateDmsDraftCannotBeMutatedThroughOldCommands() { when(access.isDmsAudit(any())).thenReturn(true); when(mapper.find("tenant", "draft")).thenReturn(draft); var command = new jnpf.base.model.uploadedit.UploadEditCommand(); command.setDraftId("draft"); command.setRevision(3L); assertIncompatible(() -> service.begin(command)); assertIncompatible(() -> service.replace(command)); assertIncompatible(() -> service.discard(command)); verify(mapper, never()).compareAndSet(any(), anyLong()); } @Test public void privateDmsLeaseCannotOpenOrSave() { when(access.isDmsAudit(any())).thenReturn(true); when(mapper.findLease("tenant", "lease")).thenReturn(draft); assertIncompatible(() -> service.lease("tenant", "owner", "lease")); var command = new jnpf.base.model.uploadedit.UploadEditSaveCommand(); command.setTenantId("tenant"); command.setOwnerId("owner"); command.setLeaseId("lease"); assertIncompatible(() -> service.save(command)); verify(mapper, never()).compareAndSet(any(), anyLong()); } @Test public void privateDmsDraftCannotBeAdoptedEvenWithCurrentPermissions() { var context = shared(); draft.setTaskId(null); when(access.isDmsAudit(any())).thenReturn(true); assertIncompatible(() -> service.prepareSave(model, field -> false, context)); assertEquals("[{\"fileId\":\"B\"}]", draft.getCurrentJson()); verify(mapper, never()).compareAndSet(any(), anyLong()); } private static void assertIncompatible(Runnable action) { try { action.run(); fail("incompatible DMS history must fail"); } catch (DataException expected) { assertEquals(UploadEditAccess.DMS_INCOMPATIBLE, expected.getMessage()); } } @Test public void reviewPermissionCannotBeUpgradedByRequestedEditMode() { org.springframework.data.redis.core.StringRedisTemplate redis = mock(org.springframework.data.redis.core.StringRedisTemplate.class); service = new UploadEditDraftService(mapper, access, new UploadEditTemporaryService(access, mock(DmsAuditTemporaryEditService.class), redis), permissions); shared(); draft.setLeaseId(null); draft.setCurrentJson("[{\"fileId\":\"B\",\"name\":\"B.docx\"}]"); when(mapper.find("tenant", draft.getId())).thenReturn(draft); when(access.baseline(any(), eq("owner"))).thenReturn(List.of(Map.of("fileId", "A"))); when(access.checkedBaseline(any(), eq("owner"))).thenReturn(new UploadEditAccess.CheckedBaseline(List.of(Map.of("fileId", "A")), true, "review")); jnpf.base.model.uploadedit.UploadEditCommand command = new jnpf.base.model.uploadedit.UploadEditCommand(); command.setDraftId(draft.getId()); command.setRevision(3L); command.setFileId("B"); command.setMode("edit"); Map result = service.begin(command); assertEquals("review", result.get("mode")); assertTrue(draft.getLeaseId().startsWith("review_")); when(mapper.findLease("tenant", draft.getLeaseId())).thenReturn(draft); assertEquals("review", service.lease("tenant", "owner", draft.getLeaseId()).getMode()); assertTrue(service.canReadStatus("tenant", "owner", draft.getLeaseId())); when(mapper.findLease("tenant", draft.getLeaseId())).thenReturn(null); assertFalse(service.canReadStatus("tenant", "owner", draft.getLeaseId())); try { service.lease("tenant", "owner", draft.getLeaseId()); fail("expired review lease"); } catch (DataException expected) { assertEquals("编辑租约已失效", expected.getMessage()); } verifyNoInteractions(redis); } @Test public void downgradedFullEditLeaseCannotOpenOrSave() { when(mapper.findLease("tenant", "lease")).thenReturn(draft); draft.setLeaseFileId("B"); when(access.baseline(any(), eq("owner"))).thenReturn(List.of(Map.of("fileId", "A"))); when(access.checkedBaseline(any(), eq("owner"))).thenReturn(new UploadEditAccess.CheckedBaseline(List.of(Map.of("fileId", "A")), true, "review")); try { service.lease("tenant", "owner", "lease"); fail("downgraded lease"); } catch (DataException expected) { } jnpf.base.model.uploadedit.UploadEditSaveCommand command = new jnpf.base.model.uploadedit.UploadEditSaveCommand(); command.setTenantId("tenant"); command.setOwnerId("owner"); command.setLeaseId("lease"); try { service.save(command); fail("downgraded callback"); } catch (DataException expected) { } verify(mapper, never()).compareAndSet(any(), anyLong()); } @Test public void loadedDraftReportsRestorePermissionInsteadOfOnlyContentDifference() { when(access.checkedBaseline(any(), eq("owner"))).thenReturn(new UploadEditAccess.CheckedBaseline(Collections.singletonList(Collections.singletonMap("fileId", "A")), false)); when(mapper.find(anyString(), anyString())).thenReturn(draft); when(permissions.canDiscardDraft(draft)).thenReturn(false, true); assertEquals(false, service.load(UploadEditDraftService.context(draft)).get("canDiscard")); assertEquals(true, service.load(UploadEditDraftService.context(draft)).get("canDiscard")); assertTrue(draft.getCurrentJson().contains("B")); } @Test public void leaseRefreshesDownloadPermissionFromReleasedForm() { when(mapper.findLease("tenant", "lease")).thenReturn(draft); when(access.checkedBaseline(any(), eq("owner"))).thenReturn(new UploadEditAccess.CheckedBaseline(Collections.singletonList(Collections.singletonMap("fileId", "A")), false)); draft.setLeaseFileId("B"); when(access.allowDownload(any())).thenReturn(true, false); assertTrue(service.lease("tenant", "owner", "lease").isAllowDownload()); assertFalse(service.lease("tenant", "owner", "lease").isAllowDownload()); verify(access, times(2)).allowDownload(argThat(context -> "model".equals(context.getModelId()) && "files".equals(context.getField()))); } @Test public void controlledLeaseStillChecksPermissionAndBaselineBeforeReturningFile() { when(mapper.findLease("tenant", "lease")).thenReturn(draft); draft.setLeaseFileId("B"); draft.setTaskId("old-alias"); when(access.checkedBaseline(any(), eq("owner"))).thenAnswer(call -> { jnpf.base.model.uploadedit.UploadEditContext context = call.getArgument(0); context.setTaskId("actual-task"); return new UploadEditAccess.CheckedBaseline(Collections.singletonList(Collections.singletonMap("fileId", "A")), true); }); assertEquals("B", service.lease("tenant", "owner", "lease").getFile().get("fileId")); verify(access).checkedBaseline(any(), eq("owner")); draft.setBaselineJson("[]"); try { service.lease("tenant", "owner", "lease"); fail("stale baseline"); } catch (DataException expected) { } } @Test public void statusChecksOnlyCurrentLeaseOwnershipWithoutLoadingForm() { when(mapper.findLease("tenant", "lease")).thenReturn(draft); assertTrue(service.canReadStatus("tenant", "owner", "lease")); assertFalse(service.canReadStatus("tenant", "another-owner", "lease")); assertFalse(service.canReadStatus("other-tenant", "owner", "lease")); verifyNoInteractions(access); verify(temporary, never()).find(any(), any(), any()); } @Test public void replacedOrAdoptedLeaseCannotReadStatus() { when(mapper.findLease("tenant", "lease")).thenReturn(draft); assertTrue(service.canReadStatus("tenant", "owner", "lease")); when(mapper.findLease("tenant", "lease")).thenReturn(null); assertFalse(service.canReadStatus("tenant", "owner", "lease")); verifyNoInteractions(access); } @Test public void temporaryStatusUsesOwnershipProbeRatherThanFileAuthorization() { when(temporary.canReadStatus("tenant", "owner", "temporary")).thenReturn(true); assertTrue(service.canReadStatus("tenant", "owner", "temporary")); verifyNoInteractions(access); verify(temporary, never()).find(any(), any(), any()); } @Test public void statusAccessDoesNotBypassOpenOrSavePermissionChecks() { when(mapper.findLease("tenant", "lease")).thenReturn(draft); when(access.baseline(any(), eq("owner"))).thenThrow(new DataException("权限已撤销")); when(access.checkedBaseline(any(), eq("owner"))).thenThrow(new DataException("权限已撤销")); assertTrue(service.canReadStatus("tenant", "owner", "lease")); try { service.lease("tenant", "owner", "lease"); fail("opening must recheck permissions"); } catch (DataException expected) { assertEquals("权限已撤销", expected.getMessage()); } jnpf.base.model.uploadedit.UploadEditSaveCommand command = new jnpf.base.model.uploadedit.UploadEditSaveCommand(); command.setTenantId("tenant"); command.setOwnerId("owner"); command.setLeaseId("lease"); try { service.save(command); fail("saving must recheck permissions"); } catch (DataException expected) { assertEquals("权限已撤销", expected.getMessage()); } verify(access).baseline(any(), eq("owner")); verify(access).checkedBaseline(any(), eq("owner")); verify(mapper, never()).compareAndSet(any(), anyLong()); } @Test public void adoptsOnlyAfterNativeSaveAndStripsTransportMetadata() { List prepared = service.prepareSave(model, field -> false); assertFalse(model.getDataNewMap().containsKey(UploadEditDraftService.SUBMIT_STATE)); verify(mapper, never()).compareAndSet(any(), anyLong()); assertTrue(draft.getBaselineJson().contains("A")); service.completeSave(prepared); assertEquals(draft.getCurrentJson(), draft.getBaselineJson()); assertNull(draft.getLeaseId()); verify(mapper).compareAndSet(draft, 3L); } @Test(expected = DataException.class) public void missingVersionCannotSilentlyOverwriteDraft() { model.getDataNewMap().remove(UploadEditDraftService.SUBMIT_STATE); service.prepareSave(model, field -> false); } @Test public void omittedFieldDoesNotAdoptDraft() { model.getDataNewMap().remove("files"); assertTrue(service.prepareSave(model, field -> false).isEmpty()); } @Test public void nativeDeniedFieldDoesNotAdvanceBaseline() { assertTrue(service.prepareSave(model, field -> "files".equals(field)).isEmpty()); assertTrue(draft.getBaselineJson().contains("A")); } @Test public void deletingChildRowInvalidatesDraftOnSuccessfulAdoption() { draft.setParentField("rows"); draft.setRowId("row"); model.getDataNewMap().put("rows", Collections.emptyList()); List prepared = service.prepareSave(model, field -> false); assertEquals(1, prepared.size()); service.completeSave(prepared); assertEquals("[]", draft.getBaselineJson()); assertNull(draft.getLeaseId()); } @Test public void partialChildUpdateDoesNotDeleteMissingRowDraft() { draft.setParentField("rows"); draft.setRowId("row"); model.getDataNewMap().put("rows", Collections.emptyList()); model.setOnlyUpdate(true); assertTrue(service.prepareSave(model, field -> false).isEmpty()); assertEquals("lease", draft.getLeaseId()); } @Test public void controlledNativeSaveMaySupersedeOldDraftButDoesNotBecomeTrustedProof() { model.getDataNewMap().remove(UploadEditDraftService.SUBMIT_STATE); model.getDataNewMap().put("files", Collections.emptyList()); jnpf.flowable.model.filepermission.FilePermissionView view = new jnpf.flowable.model.filepermission.FilePermissionView(); UploadFilePermissionService.SaveContext permissions = new UploadFilePermissionService.SaveContext(Collections.emptyMap(), view); List prepared = service.prepareSave(model, field -> false, permissions); assertEquals(1, prepared.size()); assertTrue(permissions.nativeDraftIds.contains(draft.getId())); assertEquals("[]", draft.getCurrentJson()); assertNull(draft.getEditOriginsJson()); verify(mapper, never()).compareAndSet(any(), anyLong()); } @Test public void controlledSaveStillRequiresVersionToAdoptAnExistingEditedDraft() { model.getDataNewMap().remove(UploadEditDraftService.SUBMIT_STATE); UploadFilePermissionService.SaveContext permissions = new UploadFilePermissionService.SaveContext(Collections.emptyMap(), new jnpf.flowable.model.filepermission.FilePermissionView()); try { service.prepareSave(model, field -> false, permissions); fail("missing revision"); } catch (DataException expected) { assertTrue(expected.getMessage().contains("版本缺失")); } } private UploadFilePermissionService.SaveContext shared() { draft.setTaskId("task"); draft.setLeaseId(null); draft.setId(jnpf.base.util.UploadEditWorkingCopy.id("tenant", UploadEditDraftService.context(draft))); draft.setEditOriginsJson("{\"B\":\"A\"}"); model.getDataNewMap().put(UploadEditDraftService.SUBMIT_STATE, Collections.singletonList(Map.of("draftId", draft.getId(), "revision", 3L))); jnpf.flowable.model.filepermission.FilePermissionView view = jnpf.flowable.model.filepermission.FilePermissionView.legacy("task", "model"); view.setEnabled(true); view.setRevision("r1"); jnpf.flowable.model.filepermission.FilePermissionView.FieldAccess field = new jnpf.flowable.model.filepermission.FilePermissionView.FieldAccess(); field.setStatus("ready"); field.setActions(Map.of(jnpf.flowable.model.filepermission.FilePermissionPolicy.Action.VIEW, true)); view.setFields(Map.of("files", field)); return new UploadFilePermissionService.SaveContext(new HashMap<>(Map.of("files", List.of(Map.of("fileId", "A")))), view); } @Test public void viewerAdoptsAnotherEditorsPureChangeInReadonlyField() { UploadFilePermissionService.SaveContext context = shared(); draft.setOwnerId("reviewer"); model.getDataNewMap().put("files", List.of(Map.of("fileId", "forged"))); assertEquals(1, service.prepareSave(model, field -> true, context).size()); assertTrue(model.getWorkflowWorkingFields().contains("files")); assertEquals("B", ((Map)((List)model.getDataNewMap().get("files")).get(0)).get("fileId")); assertTrue(context.sharedDraftIds.contains(draft.getId())); assertTrue(draft.getBaselineJson().contains("A")); service.completeSave(List.of(draft)); assertTrue(draft.getBaselineJson().contains("B")); } @Test public void readonlyAdoptionRejectsUploadReplacementDisguisedAsEdit() { UploadFilePermissionService.SaveContext context = shared(); draft.setEditOriginsJson(null); try { service.prepareSave(model, field -> true, context); fail("replacement"); } catch (DataException expected) { } assertTrue(model.getWorkflowWorkingFields().isEmpty()); } @Test public void sharedAdoptionRejectsStaleRevisionAndActiveLease() { UploadFilePermissionService.SaveContext context = shared(); draft.setRevision(4L); try { service.prepareSave(model, field -> true, context); fail("stale"); } catch (DataException expected) { } context = shared(); draft.setLeaseId("active"); draft.setUpdatedAt(new Date()); try { service.prepareSave(model, field -> true, context); fail("active lease"); } catch (DataException expected) { } } @Test public void sharedAdoptionRequiresVersionExceptForServerNodeCheckpoint() { UploadFilePermissionService.SaveContext context = shared(); model.getDataNewMap().remove(UploadEditDraftService.SUBMIT_STATE); try { service.prepareSave(model, field -> true, context); fail("missing version"); } catch (DataException expected) { } model.setWorkflowFileCheckpoint(true); assertEquals(1, service.prepareSave(model, field -> true, context).size()); } @Test public void sharedReadDoesNotRequireEditorOwnership() { shared(); draft.setOwnerId("reviewer"); when(access.sharedBaseline(any(), eq("owner"))).thenReturn(new UploadEditAccess.CheckedBaseline(List.of(Map.of("fileId", "A")), true)); when(mapper.find("tenant", draft.getId())).thenReturn(draft); assertEquals(true, service.loadShared(UploadEditDraftService.context(draft)).get("shared")); verify(access, never()).checkedBaseline(any(), anyString()); } @Test public void sharedLeaseCannotBeTakenOverUntilClosedOrExpired() { when(access.checkedBaseline(any(), eq("owner"))).thenReturn(new UploadEditAccess.CheckedBaseline(List.of(Map.of("fileId", "A")), true)); shared(); draft.setCurrentJson("[{\"fileId\":\"B\",\"name\":\"B.docx\"}]"); draft.setOwnerId("reviewer"); draft.setLeaseId("active"); draft.setUpdatedAt(new Date()); when(mapper.find("tenant", draft.getId())).thenReturn(draft); when(access.baseline(any(), eq("owner"))).thenReturn(List.of(Map.of("fileId", "A"))); jnpf.base.model.uploadedit.UploadEditCommand command = new jnpf.base.model.uploadedit.UploadEditCommand(); command.setDraftId(draft.getId()); command.setRevision(3L); command.setFileId("B"); try { service.begin(command); fail("active lease"); } catch (DataException expected) { } draft.setUpdatedAt(new Date(System.currentTimeMillis() - 121_000)); service.begin(command); assertEquals("owner", draft.getOwnerId()); assertFalse("active".equals(draft.getLeaseId())); command.setFileId(draft.getLeaseId()); service.close(command); assertNull(draft.getLeaseId()); } @Test public void expiredLeaseCannotSaveOrRenewAndForeignUserCannotClose() { shared(); draft.setLeaseId("active"); draft.setUpdatedAt(new Date(System.currentTimeMillis() - 121_000)); when(mapper.find("tenant", draft.getId())).thenReturn(draft); when(mapper.findLease("tenant", "active")).thenReturn(draft); jnpf.base.model.uploadedit.UploadEditCommand command = new jnpf.base.model.uploadedit.UploadEditCommand(); command.setDraftId(draft.getId()); command.setFileId("active"); try { service.heartbeat(command); fail("expired"); } catch (DataException expected) { } try { service.lease("tenant", "owner", "active"); fail("expired"); } catch (DataException expected) { } draft.setOwnerId("other"); try { service.close(command); fail("foreign"); } catch (DataException expected) { } verify(mapper, never()).touchLease(any(), any(), any(), any(), any(), any()); } @Test public void sharedChildAdoptionUsesEachRowsServerValueWithoutGrantingOtherRowWrites() { UploadFilePermissionService.SaveContext initial = shared(); draft.setParentField("rows"); draft.setRowId("one"); draft.setId(jnpf.base.util.UploadEditWorkingCopy.id("tenant", UploadEditDraftService.context(draft))); UploadEditDraft second = new UploadEditDraft(); second.setTenantId("tenant"); second.setTaskId("task"); second.setModelId("model"); second.setRecordId("record"); second.setParentField("rows"); second.setRowId("two"); second.setField("files"); second.setOwnerId("reviewer"); second.setRevision(3L); second.setBaselineJson("[{\"fileId\":\"C\"}]"); second.setCurrentJson("[{\"fileId\":\"D\"}]"); second.setEditOriginsJson("{\"D\":\"C\"}"); second.setId(jnpf.base.util.UploadEditWorkingCopy.id("tenant",UploadEditDraftService.context(second))); when(mapper.lockRecord("tenant","model","record")).thenReturn(List.of(draft,second)); initial.permissions.setFields(Map.of("rows-files",initial.permissions.getFields().get("files"))); Map one = new HashMap<>(Map.of("id","one","files",List.of(Map.of("fileId","A")),"note","keep")); Map two = new HashMap<>(Map.of("id","two","files",List.of(Map.of("fileId","C")))); Map baseline = Map.of("rows", List.of(new HashMap<>(one),new HashMap<>(two))); model.getDataNewMap().put("rows",List.of(one,two)); model.getDataNewMap().put(UploadEditDraftService.SUBMIT_STATE,List.of(Map.of("draftId",draft.getId(),"revision",3L),Map.of("draftId",second.getId(),"revision",3L))); assertEquals(2,service.prepareSave(model,key -> !model.getWorkflowWorkingFields().contains(key),new UploadFilePermissionService.SaveContext(baseline,initial.permissions)).size()); assertEquals(List.of(Map.of("fileId","B")),one.get("files")); assertEquals(List.of(Map.of("fileId","D")),two.get("files")); assertEquals("keep",one.get("note")); assertEquals(1,model.getWorkflowWorkingFields().size()); } @Test public void nativeMainSaveKeepsPersistedSourceButClearsOrdinaryReplacement() { when(access.isDmsAuditTemporary(any())).thenReturn(true); when(mapper.lockRecord("tenant", "model", "record")).thenReturn(List.of()); when(permissions.dmsMainAttachmentBaseline(model)).thenReturn(List.of(Map.of("fileId", "A", "laiyuan_muban_id", "templateA"))); model.getDataNewMap().put("wenjian", "[{\"fileId\":\"A\",\"laiyuan_muban_id\":\"forged\"}]"); service.prepareSave(model, field -> false); assertTrue(model.getDataNewMap().get("wenjian").toString().contains("templateA")); when(access.hasEnabledFields("model")).thenReturn(false); service.prepareSave(model, field -> false); assertTrue(model.getDataNewMap().get("wenjian").toString().contains("templateA")); model.getDataNewMap().put("wenjian", List.of(Map.of("fileId", "new", "laiyuan_muban_id", "templateA"))); service.prepareSave(model, field -> false); assertFalse(((Map)((List)model.getDataNewMap().get("wenjian")).get(0)).containsKey("laiyuan_muban_id")); } @Test public void nativeMainSaveAcceptsNewTemplateForSameFileId() { when(access.isDmsAuditTemporary(any())).thenReturn(true); when(mapper.lockRecord("tenant", "model", "record")).thenReturn(List.of()); when(permissions.dmsMainAttachmentBaseline(model)).thenReturn(List.of(Map.of("fileId", "A", "laiyuan_muban_id", "templateA"))); try(var tickets=mockStatic(jnpf.util.TicketUtil.class)) { tickets.when(() -> jnpf.util.TicketUtil.parseTicket("new")).thenReturn(jnpf.util.JsonUtil.getObjectToString(Map.of( "purpose", "upload-edit-source", "tenantId", "tenant", "ownerId", "owner", "fileId", "A", "laiyuan_muban_id", "templateB"))); model.getDataNewMap().put("wenjian", List.of(Map.of("fileId", "A", "uploadReceipt", "new"))); service.prepareSave(model, field -> false); assertEquals("templateB", ((Map)((List)model.getDataNewMap().get("wenjian")).get(0)).get("laiyuan_muban_id")); } } @Test public void dmsLoadWithoutDraftIsReadOnlyAndReturnsContentIdentity() { shared(); when(access.sharedBaseline(any(), eq("owner"))).thenReturn(new UploadEditAccess.CheckedBaseline(List.of(Map.of("fileId", "A")), true, "edit", true)); Map view = service.loadShared(UploadEditDraftService.context(draft)); assertEquals(true, view.get("dmsAudit")); verify(access, never()).isDmsAudit(any()); assertEquals(jnpf.base.util.UploadEditStateMachine.contentToken(draft.getBaselineJson()), view.get("contentToken")); verify(mapper, never()).insert(any()); verify(mapper, never()).compareAndSet(any(), anyLong()); } @Test public void dmsStaleLoadDoesNotResetSavedDraft() { shared(); when(mapper.find(anyString(), anyString())).thenReturn(draft); when(access.sharedBaseline(any(), eq("owner"))).thenReturn(new UploadEditAccess.CheckedBaseline(List.of(Map.of("fileId", "C")), true, "edit", true)); Map view = service.loadShared(UploadEditDraftService.context(draft)); assertEquals(true, view.get("dmsAudit")); verify(access, never()).isDmsAudit(any()); assertEquals(List.of(Map.of("fileId", "C")), view.get("files")); assertTrue(draft.getCurrentJson().contains("B")); verify(mapper, never()).insert(any()); verify(mapper, never()).compareAndSet(any(), anyLong()); } @Test public void dmsContentConfirmationSurvivesLeaseRevisionButRejectsChangedContent() { UploadFilePermissionService.SaveContext context = shared(); when(access.isDmsAudit(any())).thenReturn(true); String token = jnpf.base.util.UploadEditStateMachine.contentToken(draft.getCurrentJson()); model.getDataNewMap().put(UploadEditDraftService.SUBMIT_STATE, List.of(Map.of("draftId", draft.getId(), "revision", 1L, "contentToken", token))); assertEquals(1, service.prepareSave(model, field -> true, context).size()); model.getDataNewMap().put(UploadEditDraftService.SUBMIT_STATE, List.of(Map.of("draftId", draft.getId(), "revision", 3L, "contentToken", "old"))); try { service.prepareSave(model, field -> true, context); fail("changed content"); } catch (DataException expected) { assertTrue(expected.getMessage().contains("重新预览")); } } @Test public void dmsCheckpointCannotAdoptUnconfirmedSavedContent() { UploadFilePermissionService.SaveContext context = shared(); when(access.isDmsAudit(any())).thenReturn(true); model.setWorkflowFileCheckpoint(true); model.getDataNewMap().remove(UploadEditDraftService.SUBMIT_STATE); try { service.prepareSave(model, field -> true, context); fail("unconfirmed"); } catch (DataException expected) { assertTrue(expected.getMessage().contains("尚未确认")); } } @Test public void dmsBeginLocksExistingParentBeforeCreatingFirstDraftAndCloseKeepsContent() { shared(); when(access.isDmsAudit(any())).thenReturn(true); when(access.dmsAuditTable("model")).thenReturn("dms_wenjian_xiuding_shenhe"); when(mapper.lockDmsRecord("tenant", "dms_wenjian_xiuding_shenhe", "record")).thenReturn("record"); var baseline = new UploadEditAccess.CheckedBaseline(List.of(Map.of("fileId", "A", "name", "A.docx")), true, "edit", true); when(access.sharedBaseline(any(), eq("owner"))).thenReturn(baseline); when(access.checkedBaseline(any(), eq("owner"))).thenReturn(baseline); when(access.baseline(any(), eq("owner"))).thenReturn(baseline.getFiles()); java.util.concurrent.atomic.AtomicReference stored = new java.util.concurrent.atomic.AtomicReference<>(); when(mapper.find(anyString(), anyString())).thenAnswer(call -> stored.get()); when(mapper.insert(any())).thenAnswer(call -> { stored.set(call.getArgument(0)); return 1; }); var command = new jnpf.base.model.uploadedit.UploadEditCommand(); command.setContext(UploadEditDraftService.context(draft)); command.setDraftId(draft.getId()); command.setFileId("A"); command.setContentToken(jnpf.base.util.UploadEditStateMachine.contentToken(baseline.getFiles())); var lease = service.begin(command); var order = inOrder(mapper); order.verify(mapper).lockDmsRecord("tenant", "dms_wenjian_xiuding_shenhe", "record"); order.verify(mapper).insert(any()); try { service.begin(command); fail("concurrent lease"); } catch (DataException expected) { assertTrue(expected.getMessage().contains("正在编辑")); } var close = new jnpf.base.model.uploadedit.UploadEditCommand(); close.setDraftId(draft.getId()); close.setFileId((String)lease.get("leaseId")); service.close(close); assertEquals(lease.get("contentToken"), service.loadShared(command.getContext()).get("contentToken")); } @Test public void dmsMetadataChangesAreRejectedEvenWhenContentTokenMatches() { var context = shared(); when(access.isDmsAudit(any())).thenReturn(true); model.getDataNewMap().put(UploadEditDraftService.SUBMIT_STATE, List.of(Map.of("draftId", draft.getId(), "revision", 3L, "contentToken", jnpf.base.util.UploadEditStateMachine.contentToken(draft.getCurrentJson()), "metadataToken", "stale"))); try { service.prepareSave(model, f -> false, context); fail("metadata conflict must reject"); } catch (DataException expected) { assertTrue(expected.getMessage().contains("模板来源")); } } @Test public void callbackRechecksLeaseAfterAcquiringNativeParentLock() { shared(); draft.setLeaseId("lease"); draft.setUpdatedAt(new Date()); when(access.isDmsAudit(any())).thenReturn(true); when(access.dmsAuditTable("model")).thenReturn("dms_wenjian_xinzeng_shenhe"); when(mapper.lockDmsRecord("tenant", "dms_wenjian_xinzeng_shenhe", "record")).thenReturn("record"); when(mapper.findLease("tenant", "lease")).thenReturn(draft, null); var command = new jnpf.base.model.uploadedit.UploadEditSaveCommand(); command.setTenantId("tenant"); command.setOwnerId("owner"); command.setLeaseId("lease"); assertFalse(service.save(command)); var order = inOrder(mapper); order.verify(mapper).findLease("tenant", "lease"); order.verify(mapper).lockDmsRecord("tenant", "dms_wenjian_xinzeng_shenhe", "record"); order.verify(mapper).findLease("tenant", "lease"); verify(mapper, never()).compareAndSet(any(), anyLong()); } }