package jnpf.dmsService; import jnpf.dmsEntity.storage.approval.DmsApprovedItem; import jnpf.dmsEntity.storage.approval.DmsApprovedBatch; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.annotation.JsonInclude; import jnpf.dmsEntity.*; import jnpf.dmsEntity.center.DmsCenterVersionSnapshot; import jnpf.dmsMapper.*; import jnpf.exception.DataException; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.*; import org.mockito.junit.jupiter.MockitoExtension; import java.util.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class DmsApprovedAuditSourceTest { @Mock private jnpf.dmsService.DmsXiudingKanwuService revisionErrata; @Mock private jnpf.file.FileContentApi fileContentApi; @Mock DmsWenjianXinzengShenheMapper newMapper; @Mock DmsWailaiWenjianXinzengShenheMapper externalMapper; @Mock DmsWenjianXiudingShenheMapper revisionMapper; @Mock DmsWenjianXinzengShenheJiluMingxiMapper newRecords; @Mock DmsWenjianXiudingShenheJiluMingxiMapper revisionRecords; @Mock DmsWenjianXiudingShenqingZixiangMapper revisionDetails; @Mock DmsWenjianXiudingShenqingMapper revisionRequests; @Mock DmsWenjianGuanxiMapper existingRelations; @Mock DmsWenjianBianhaoMapper numbers; @Mock DmsWenjianBanbenMapper businessVersions; @Mock jnpf.dmsService.center.DmsCenterQueryService centerVersions; @Mock DmsWenjianGuanlianMingxiMapper relations; @Spy ObjectMapper json = new ObjectMapper(); @Mock DmsDisableApplicationGuard disableApplications; @InjectMocks DmsApprovedAuditSource source; String folder = "018f0000-0000-7000-8000-000000000001"; Date approved = new Date(System.currentTimeMillis() - 10000); DmsWenjianXinzengShenheEntity arrangeNew(List rows) { DmsWenjianXinzengShenheEntity a = new DmsWenjianXinzengShenheEntity(); a.setId("audit"); a.setTenantId("tenant"); a.setFlowId("flow"); a.setFlowTaskId("task"); a.setFlowState(2); a.setWenjianBianhaoId("number"); a.setWenjianBianhao("SOP-001"); a.setWenjianMingcheng("SOP"); a.setWenjianMuluId(folder); a.setGuishuBumenId("dept"); a.setWenjian("[{\"url\":\"/api/file/Image/annex/main.docx\"}]"); a.setCreatorUserId("author"); a.setCreatorTime(approved); a.setShengxiaoRiqi(approved); DmsWenjianBianhaoEntity n = new DmsWenjianBianhaoEntity(); n.setWenjianBianhao("SOP-001"); n.setBizStatus("using"); n.setDangqianWenjianMingcheng("SOP"); when(newMapper.selectByFlowForUpdate("tenant", "flow", "task")).thenReturn(List.of(a)); when(numbers.selectActiveByIdForUpdate("tenant", "number")).thenReturn(n); when(newRecords.selectByAuditForUpdate("tenant", "audit")).thenReturn(rows); return a; } DmsWenjianXinzengShenheJiluMingxiEntity record(String id) { DmsWenjianXinzengShenheJiluMingxiEntity r = new DmsWenjianXinzengShenheJiluMingxiEntity(); r.setId(id); r.setFujianBianhao("REC-" + id); r.setFujianMingcheng("记录" + id); r.setWenjianMuluId(folder); r.setGuishuBumenId("dept"); r.setJiluWenjian("[{\"url\":\"/api/file/Image/annex/" + id + ".docx\"}]"); r.setJiluLeixing("record"); r.setDengji("low"); r.setShifouShoukong("no"); return r; } DmsApprovedAuditSource.Source load() { return source.load("tenant", "flow", "task", "xinzeng_shenhe", "approver", approved); } DmsApprovedAuditSource.Source loadRevision() { return source.load("tenant", "flow", "task", "xiuding_shenhe", "approver", approved); } @Test void resolvesInitialTargetVersionsAsD00ForMainAndRecords() { arrangeNew(List.of(record("one"))); assertEquals(List.of("D(00)", "D(00)"), load().batch().getItems().stream() .map(DmsApprovedItem::getVersionLabel).toList()); } @Test void resolvesUpgradeFromOriginalSequenceRatherThanFormOrCenterLabel() { DmsWenjianBanbenEntity base = arrangeRevision("shengji"); base.setBanbenXuhao(9); assertEquals("D(10)", loadRevision().batch().getItems().get(0).getVersionLabel()); } @Test void resolvesUnchangedVersionFromOriginalCenterVersion() { DmsWenjianBanbenEntity base = arrangeRevision("wubianhua"); base.setWenjianNeirong(revisionMapper.selectByFlowForUpdate("tenant", "flow", "task").get(0).getWenjian()); UUID versionId = UUID.fromString(base.getWenjianZhongxinBanbenId()); DmsCenterVersionSnapshot original = centerVersions.version("tenant", versionId); when(centerVersions.version("tenant", versionId)).thenReturn(new DmsCenterVersionSnapshot( original.documentId(), versionId, "D(07)", original.checksumSha256(), "author", null, original.fileObjectId(), "EFFECTIVE", approved, null)); assertEquals("D(07)", loadRevision().batch().getItems().get(0).getVersionLabel()); } @Test void departmentScopesAreIndependentFrozenAndAbsentLegacyFieldsKeepTheirDigest() { var row = record("one"); var audit = arrangeNew(List.of(row)); String legacy = load().batch().getSourceDigest(); assertEquals("[]", audit.getChayueShouquanFanweiId()); assertEquals("[]", row.getChayueShouquanFanweiId()); audit.setChayueShouquanFanweiId("[\"main-dept\"]"); row.setChayueShouquanFanweiId("[\"record-dept--subOrg\"]"); var batch = load().batch(); assertEquals("[\"main-dept\"]", batch.getItems().get(0).getReadAccessScope()); assertEquals("[\"record-dept--subOrg\"]", batch.getItems().get(1).getReadAccessScope()); assertNotEquals(legacy, batch.getSourceDigest()); for (String empty : java.util.Arrays.asList(null, "", "null", "[]")) { audit.setChayueShouquanFanweiId(empty); row.setChayueShouquanFanweiId(empty); assertEquals(legacy, load().batch().getSourceDigest()); assertTrue(load().batch().getItems().stream().allMatch(i -> "[]".equals(i.getReadAccessScope()))); } } @Test void newAuditReadsPersistedReviewersAndProtectsThemInSourceDigest() { var audit = arrangeNew(List.of()); String legacy = load().batch().getSourceDigest(); assertFalse(json.valueToTree(audit).has("shenheRenId")); audit.setShenheRenId("[\"reviewer\"]"); var result = load(); assertEquals("[\"reviewer\"]", result.reviewerIds()); assertNotEquals(legacy, result.batch().getSourceDigest()); audit.setShenheRenId(null); assertEquals(legacy, load().batch().getSourceDigest()); } @Test void revisionReadsItsOwnPersistedReviewerField() { arrangeRevision("shengji"); var audit = revisionMapper.selectByFlowForUpdate("tenant", "flow", "task").get(0); audit.setShenheRenId("[\"revision-reviewer\"]"); assertEquals("[\"revision-reviewer\"]", loadRevision().reviewerIds()); } @Test void carriesTrustedControlFlagsForApprovedMainAndRecordFiles() { DmsWenjianXinzengShenheJiluMingxiEntity controlled = record("two"); controlled.setShifouShoukong("yes"); controlled.setJiluDengjiGuanliId("rule"); controlled.setJiluDengjiGuanliBanben(1); arrangeNew(List.of(record("one"), controlled)); List items = load().batch().getItems(); assertEquals("yes", items.get(0).getShifouShoukong()); assertEquals("no", items.get(1).getShifouShoukong()); assertEquals("yes", items.get(2).getShifouShoukong()); } @org.junit.jupiter.params.ParameterizedTest @org.junit.jupiter.params.provider.ValueSource(strings = {"yes", "no"}) void newAuditAcceptsDirectRecordModeWithoutLegacyMetadata(String controlled) { var row = record("mode"); row.setJiluLeixing(null); row.setDengji(null); row.setShifouShoukong(controlled); arrangeNew(List.of(row)); assertEquals(controlled, load().batch().getItems().get(1).getShifouShoukong()); } @org.junit.jupiter.params.ParameterizedTest @org.junit.jupiter.params.provider.ValueSource(strings = {"yes", "no"}) void revisionAuditAcceptsNewRecordWithDirectMode(String controlled) { arrangeRevision("shengji"); var row = new DmsWenjianXiudingShenheJiluMingxiEntity(); row.setId("mode"); row.setWenjianMuluId(folder); row.setGuishuBumenId("dept"); row.setFujianMingcheng("Record"); row.setFujianBianhao("SOP-001-A"); row.setJiluWenjian("[{\"url\":\"/api/file/Image/annex/record.docx\"}]"); row.setBiangengFangshi("xinjian"); row.setShifouShoukong(controlled); when(revisionRecords.selectByAuditForUpdate("tenant", "revision-audit")).thenReturn(List.of(row)); assertEquals(controlled, loadRevision().batch().getItems().get(1).getShifouShoukong()); } @org.junit.jupiter.params.ParameterizedTest @org.junit.jupiter.params.provider.NullAndEmptySource @org.junit.jupiter.params.provider.ValueSource(strings = {"online", "offline", "YES"}) void auditSourceStillRejectsMissingOrInvalidControlFlag(String controlled) { var row = record("mode"); row.setShifouShoukong(controlled); arrangeNew(List.of(row)); assertThrows(DataException.class, this::load); } DmsWenjianBanbenEntity arrangeRevision(String change) { DmsWenjianXiudingShenheEntity audit = new DmsWenjianXiudingShenheEntity(); audit.setId("revision-audit"); audit.setTenantId("tenant"); audit.setFlowId("flow"); audit.setFlowTaskId("task"); audit.setFlowState(2); audit.setXiudingShenqingZixiangId("detail"); audit.setBiangengFangshi(change); audit.setWenjianMingcheng("SOP"); audit.setWenjian("[{\"fileId\":\"revision\",\"url\":\"/api/file/Image/annex/revision.docx\"}]"); audit.setWenjianMuluId(folder); audit.setGuishuBumenId("dept"); audit.setCreatorUserId("author"); audit.setCreatorTime(approved); audit.setShengxiaoRiqi(approved); DmsWenjianXiudingShenqingZixiangEntity detail = new DmsWenjianXiudingShenqingZixiangEntity(); detail.setId("detail"); detail.setForeignId("request"); detail.setWenjianId("resource"); detail.setYuanWenjianBanbenId("base-version"); detail.setWenjianBianhao("SOP-001"); DmsWenjianXiudingShenqingEntity request = new DmsWenjianXiudingShenqingEntity(); request.setId("request"); request.setFlowState(2); request.setCreatorUserId("request-author"); request.setCreatorTime(approved); UUID centerVersionId = UUID.randomUUID(); DmsWenjianBanbenEntity base = new DmsWenjianBanbenEntity(); base.setId("base-version"); base.setWenjianId("resource"); base.setBanbenXuhao(0); base.setWenjianZhongxinBanbenId(centerVersionId.toString()); base.setWenjianBianhaoId("number"); base.setWenjianBianhao("SOP-001"); DmsCenterVersionSnapshot center = new DmsCenterVersionSnapshot( UUID.randomUUID(), centerVersionId, "D(00)", "a".repeat(64), "author", null, UUID.randomUUID(), "EFFECTIVE", approved, null); when(revisionMapper.selectByFlowForUpdate("tenant", "flow", "task")).thenReturn(List.of(audit)); when(revisionRecords.selectByAuditForUpdate("tenant", "revision-audit")).thenReturn(List.of()); when(revisionDetails.selectByIdForUpdate("tenant", "detail")).thenReturn(detail); when(revisionRequests.selectByIdForUpdate("tenant", "request")).thenReturn(request); when(businessVersions.selectById("tenant", "base-version")).thenReturn(base); when(centerVersions.version("tenant", centerVersionId)).thenReturn(center); return base; } @Test void revisionDigestIncludesHandlingButIgnoresLiveProcessedFlag() { arrangeRevision("shengji"); String noErrata = loadRevision().batch().getSourceDigest(); jnpf.dmsEntity.kanwu.DmsXiudingKanwuRow row = new jnpf.dmsEntity.kanwu.DmsXiudingKanwuRow(); row.setKanwuZixiangId("note"); row.setChuliJielun("bucaina"); row.setChuliYuanyin("reason"); when(revisionErrata.loadAndValidate("tenant", "revision-audit", "resource", "base-version")).thenReturn(List.of(row)); String handled = loadRevision().batch().getSourceDigest(); assertNotEquals(noErrata, handled); row.setAlreadyProcessed(true); assertEquals(handled, loadRevision().batch().getSourceDigest()); row.setChuliYuanyin("changed reason"); assertNotEquals(handled, loadRevision().batch().getSourceDigest()); row.setChuliYuanyin("reason"); row.setChuliJielun("yixiugai_queren"); assertNotEquals(handled, loadRevision().batch().getSourceDigest()); } @Test void revisionFallsBackToApprovedRequestCreatorWhenAuditCopyIsMissing() { arrangeRevision("shengji"); DmsWenjianXiudingShenheEntity audit = revisionMapper .selectByFlowForUpdate("tenant", "flow", "task").get(0); audit.setCreatorUserId(null); audit.setCreatorTime(null); DmsApprovedAuditSource.Source result = loadRevision(); assertEquals("request-author", result.preparedBy()); assertEquals(approved, result.preparedOn()); } @Test void reuseAcceptsEquivalentFileJsonWithDifferentPropertyOrder() { DmsWenjianBanbenEntity base = arrangeRevision("wubianhua"); base.setWenjianNeirong("[{\"name\":\"SOP.docx\",\"fileId\":\"revision\",\"url\":\"/api/file/Image/annex/revision.docx\"}]"); DmsWenjianXiudingShenheEntity audit = revisionMapper .selectByFlowForUpdate("tenant", "flow", "task").get(0); audit.setWenjian("[{\"fileId\":\"revision\",\"url\":\"/api/file/Image/annex/revision.docx\",\"name\":\"SOP.docx\"}]"); assertEquals("REUSE", loadRevision().batch().getItems().get(0).getChangeType()); } @Test void reuseIgnoresDisplayMetadataButKeepsAuthoritativeOriginal() throws Exception { var apiField = DmsApprovedAuditSource.class.getDeclaredField("fileContentApi"); apiField.setAccessible(true); apiField.set(source, fileContentApi); DmsWenjianBanbenEntity base = arrangeRevision("wubianhua"); String original = "[{\"fileId\":\"original\",\"name\":\"source.txt\",\"url\":\"/api/file/Image/annex/source.txt\",\"uploadReceipt\":\"historical-token\",\"fileVersionId\":null}]"; base.setWenjianNeirong(original); DmsWenjianXiudingShenheEntity audit = revisionMapper.selectByFlowForUpdate("tenant", "flow", "task").get(0); audit.setWenjian(original); assertEquals(original, loadRevision().batch().getItems().get(0).getFileJson()); for (String changed : List.of(original.replace(",\"uploadReceipt\":\"historical-token\"", ""), original.replace(",\"fileVersionId\":null", ""))) { audit.setWenjian(changed); assertEquals(original, loadRevision().batch().getItems().get(0).getFileJson()); } var response = new jnpf.base.ActionResult(); response.setCode(200); response.setData(false); when(fileContentApi.sameContent(eq("jnpf-dms"), any())).thenReturn(response); audit.setWenjian(original.replace("original", "other-file")); assertEquals("沿用版本不能修改文件内容", assertThrows(DataException.class, this::loadRevision).getMessage()); } @Test void reuseStillRejectsARealFileChange() { DmsWenjianBanbenEntity base = arrangeRevision("wubianhua"); base.setWenjianNeirong("[{\"url\":\"/api/file/Image/annex/original.docx\"}]"); DataException exception = assertThrows(DataException.class, this::loadRevision); assertEquals("沿用版本不能修改文件内容", exception.getMessage()); } DmsApprovedAuditSource sourceWithJson(ObjectMapper mapper) { return new DmsApprovedAuditSource(newMapper, externalMapper, revisionMapper, newRecords, revisionRecords, revisionDetails, revisionRequests, existingRelations, numbers, businessVersions, centerVersions, relations, mapper, disableApplications, revisionErrata); } @Test void externalApprovalUsesSavedVersionAndOneControlledFile() { DmsWailaiWenjianXinzengShenheEntity audit = new DmsWailaiWenjianXinzengShenheEntity(); audit.setId("external"); audit.setFlowState(2); audit.setWailaiWenjianBianhao("EXT-001"); audit.setWenjianMingcheng("外来文件"); audit.setBanbenhao("D(01)"); audit.setBizFiles("[{\"name\":\"external.pdf\",\"url\":\"/file\"}]"); audit.setWenjianMuluId(folder); audit.setBumenId("dept"); audit.setShenqingRen("applicant"); audit.setShenqingRiqi(approved); when(externalMapper.selectByFlowForUpdate("tenant", "flow", "task")).thenReturn(List.of(audit)); DmsApprovedBatch batch = source.load("tenant", "flow", "task", "wailai_wenjian_xinzeng_shenhe", "approver", approved).batch(); assertEquals(1, batch.getItems().size()); assertEquals("D(01)", batch.getItems().get(0).getVersionLabel()); assertEquals("yes", batch.getItems().get(0).getShifouShoukong()); assertNull(batch.getEffectiveTo()); String digest = batch.getSourceDigest(); audit.setBanbenhao("D(02)"); assertNotEquals(digest, source.load("tenant", "flow", "task", "wailai_wenjian_xinzeng_shenhe", "approver", approved).batch().getSourceDigest()); } @Test void approvalDigestIsIndependentOfApplicationJsonFormatting() { DmsWenjianXinzengShenheEntity audit = arrangeNew(List.of(record("a"))); audit.setShengxiaoRiqi(new Date(1789660800000L)); ObjectMapper baseline = new ObjectMapper().disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); String expected = sourceWithJson(baseline).load("tenant", "flow", "task", "xinzeng_shenhe", "approver", approved).batch().getSourceDigest(); List variants = Arrays.asList( baseline.copy().enable(SerializationFeature.INDENT_OUTPUT), baseline.copy().enable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS), baseline.copy().setTimeZone(TimeZone.getTimeZone("Asia/Shanghai")), baseline.copy().setSerializationInclusion(JsonInclude.Include.NON_NULL)); for (ObjectMapper variant : variants) { String actual = sourceWithJson(variant).load("tenant", "flow", "task", "xinzeng_shenhe", "approver", approved).batch().getSourceDigest(); assertEquals(expected, actual, "Application JSON settings must not change a frozen approval digest"); } } @Test void nativeApprovalFieldsIgnoreApplicationPropertyNaming() { arrangeNew(List.of(record("a"))); ObjectMapper mapper = new ObjectMapper().setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); DmsApprovedAuditSource.Source result = sourceWithJson(mapper).load("tenant", "flow", "task", "xinzeng_shenhe", "approver", approved); assertEquals(2, result.batch().getItems().size()); assertEquals("SOP", result.batch().getItems().get(0).getDocumentName()); } @Test void preservesNativePreparationMetadataWhenApproverAndApprovalDateDiffer() { DmsWenjianXinzengShenheEntity audit = arrangeNew(List.of()); Date preparationDate = new Date(approved.getTime() - 86400000L); audit.setCreatorTime(preparationDate); DmsApprovedAuditSource.Source result = load(); assertEquals("author", result.preparedBy()); assertEquals(preparationDate, result.preparedOn()); assertEquals("approver", result.batch().getActorId()); assertEquals(approved, result.batch().getApprovedAt()); } @Test void presetDateNeverSubstitutesForRequiredFormalDate() { DmsWenjianXinzengShenheEntity audit = arrangeNew(List.of()); audit.setShengxiaoRiqi(null); audit.setYusheShengxiaoRiqi(approved); assertThrows(DataException.class, this::load); } @Test void includesAllNativeRowsWithStableDistinctSourceKeys() { DmsWenjianXinzengShenheJiluMingxiEntity a = record("a"); DmsWenjianXinzengShenheJiluMingxiEntity b = record("b"); arrangeNew(List.of(b, a)); DmsApprovedAuditSource.Source first = load(); assertEquals(List.of("xinzeng_shenhe:audit:zhuwenjian", "xinzeng_shenhe:audit:jilu:a", "xinzeng_shenhe:audit:jilu:b"), first.batch().getItems().stream().map(i -> i.getSourceKey()).toList()); when(newRecords.selectByAuditForUpdate("tenant", "audit")).thenReturn(List.of(a, b)); assertEquals(first.batch().getSourceDigest(), load().batch().getSourceDigest()); } @Test void detectsMetadataOrRowSetChangesButIgnoresSettlementOutputs() { DmsWenjianXinzengShenheJiluMingxiEntity row = record("a"); DmsWenjianXinzengShenheEntity audit = arrangeNew(List.of(row)); String before = load().batch().getSourceDigest(); audit.setWenjianId("resource"); audit.setShengxiaoWenjianBanbenId("business-version"); row.setJiluWenjianId("record-resource"); row.setShengxiaoWenjianBanbenId("record-version"); row.setBanbenhao("D(00)"); assertEquals(before, load().batch().getSourceDigest()); assertTrue(load().settled()); row.setFujianMingcheng("changed"); assertNotEquals(before, load().batch().getSourceDigest()); when(newRecords.selectByAuditForUpdate("tenant", "audit")).thenReturn(List.of()); assertNotEquals(before, load().batch().getSourceDigest()); } @Test void transportsFrozenSignaturePageOnlyOnMainFileAndPreservesUploadText() throws Exception { arrangeNew(List.of(record("a"))); DmsWenjianBianhaoEntity number = numbers.selectActiveByIdForUpdate("tenant", "number"); String oldDigest = load().batch().getSourceDigest(); String files = "[ {\"name\":\"签批页.docx\",\"url\":\"/api/file/Image/annex/sign.docx\"} ]"; number.setGuizeKuaizhao(json.writeValueAsString(Map.of("qianpiye_id", "signature-id", "qianpiye_files", files))); jnpf.dmsEntity.storage.approval.DmsApprovedBatch batch = load().batch(); com.fasterxml.jackson.databind.JsonNode wire = json.readTree(json.writeValueAsString(jnpf.dmsEntity.storage.form.DmsApprovedRegistrationForm.from(batch))); assertEquals("signature-id", wire.path("files").get(0).path("qianpiyeId").asText()); assertEquals(files, wire.path("files").get(0).path("qianpiyeFiles").asText()); assertNull(batch.getItems().get(1).getQianpiyeId()); // The upstream approval-page input participates in the frozen approval digest. assertNotEquals(oldDigest, batch.getSourceDigest()); } @Test void acceptsHistoricalSnapshotsWithoutSignaturePage() { arrangeNew(List.of()); DmsWenjianBianhaoEntity number = numbers.selectActiveByIdForUpdate("tenant", "number"); for (String snapshot : Arrays.asList(null, "", "{}", "{\"qianpiye_id\":null,\"qianpiye_files\":null}")) { number.setGuizeKuaizhao(snapshot); jnpf.dmsEntity.storage.approval.DmsApprovedItem main = load().batch().getItems().get(0); assertNull(main.getQianpiyeId()); assertNull(main.getQianpiyeFiles()); } } @Test void rejectsMalformedSignaturePageSnapshotInsteadOfLookingUpCurrentRule() { arrangeNew(List.of()); numbers.selectActiveByIdForUpdate("tenant", "number").setGuizeKuaizhao("{\"qianpiye_files\":[]}"); assertThrows(DataException.class, this::load); } @Test void rejectsIncompleteRowBeforeAnyFileUpload() { DmsWenjianXinzengShenheJiluMingxiEntity row = record("a"); row.setJiluWenjian(null); arrangeNew(List.of(row)); assertThrows(DataException.class, this::load); } @Test void rejectsUnapprovedNativeSource() { arrangeNew(List.of()).setFlowState(1); assertThrows(DataException.class, this::load); } @Test void sendsOnlyTheNewMainFilesFrozenApprovalPageAndIncludesItInTheDigest() { arrangeNew(List.of(record("a"))); DmsWenjianBianhaoEntity number = number( "{\"qianpiye_files\":\"[{\\\"url\\\":\\\"/api/file/Image/annex/approval.pdf\\\"}]\"}"); when(numbers.selectActiveByIdForUpdate("tenant", "number")).thenReturn(number); DmsApprovedAuditSource.Source first = load(); assertEquals("[{\"url\":\"/api/file/Image/annex/approval.pdf\"}]", first.batch().getItems().get(0).getApprovalPageFileJson()); assertNull(first.batch().getItems().get(1).getApprovalPageFileJson()); String firstDigest = first.batch().getSourceDigest(); number.setGuizeKuaizhao( "{\"qianpiye_files\":\"[{\\\"url\\\":\\\"/api/file/Image/annex/changed.pdf\\\"}]\"}"); assertNotEquals(firstDigest, load().batch().getSourceDigest()); } @Test void missingOrEmptyApprovalPageDoesNotAffectNewFileRegistration() { arrangeNew(List.of()); DmsWenjianBianhaoEntity number = number("{\"qianpiye_files\":\"[]\"}"); when(numbers.selectActiveByIdForUpdate("tenant", "number")).thenReturn(number); assertNull(load().batch().getItems().get(0).getApprovalPageFileJson()); number.setGuizeKuaizhao(null); assertNull(load().batch().getItems().get(0).getApprovalPageFileJson()); } @Test void revisionUsesTheOriginalBusinessVersionsNumberRuleApprovalPage() { arrangeRevision("shengji"); DmsWenjianBianhaoEntity number = number( "{\"qianpiye_files\":\"[{\\\"url\\\":\\\"/api/file/Image/annex/approval.docx\\\"}]\"}"); when(numbers.selectActiveByIdForUpdate("tenant", "number")).thenReturn(number); DmsApprovedItem main = loadRevision().batch().getItems().get(0); assertEquals("REVISE", main.getChangeType()); assertEquals("[{\"url\":\"/api/file/Image/annex/approval.docx\"}]", main.getApprovalPageFileJson()); assertEquals(main.getApprovalPageFileJson(), main.getQianpiyeFiles()); } @Test void revisionWithoutConfiguredApprovalPageContinuesNormally() { arrangeRevision("shengji"); when(numbers.selectActiveByIdForUpdate("tenant", "number")) .thenReturn(number("{\"qianpiye_files\":\"[]\"}")); DmsApprovedItem main = loadRevision().batch().getItems().get(0); assertNull(main.getApprovalPageFileJson()); assertNull(main.getQianpiyeFiles()); } @Test void legacyRevisionWithoutResolvableNumberContinuesNormally() { DmsWenjianBanbenEntity base = arrangeRevision("shengji"); base.setWenjianBianhaoId(null); assertNull(loadRevision().batch().getItems().get(0).getApprovalPageFileJson()); verify(numbers, never()).selectActiveByIdForUpdate(eq("tenant"), any()); } private DmsWenjianBianhaoEntity number(String snapshot) { DmsWenjianBianhaoEntity number = new DmsWenjianBianhaoEntity(); number.setWenjianBianhao("SOP-001"); number.setBizStatus("using"); number.setDangqianWenjianMingcheng("SOP"); number.setGuizeKuaizhao(snapshot); return number; } DmsWenjianXiudingShenheEntity arrangeRevision() { DmsWenjianXiudingShenheEntity audit = new DmsWenjianXiudingShenheEntity(); audit.setId("revision"); audit.setFlowState(2); audit.setCreatorUserId("author"); audit.setCreatorTime(approved); audit.setShengxiaoRiqi(approved); audit.setWenjianMuluId(folder); audit.setGuishuBumenId("dept"); audit.setWenjianMingcheng("main"); audit.setBiangengFangshi("wubianhua"); audit.setXiudingShenqingZixiangId("detail"); DmsWenjianXiudingShenqingZixiangEntity detail = new DmsWenjianXiudingShenqingZixiangEntity(); detail.setId("detail"); detail.setForeignId("request"); detail.setWenjianId("main"); detail.setYuanWenjianBanbenId("main-v1"); DmsWenjianXiudingShenqingEntity request = new DmsWenjianXiudingShenqingEntity(); request.setFlowState(2); when(revisionMapper.selectByFlowForUpdate("tenant", "flow", "task")).thenReturn(List.of(audit)); when(revisionDetails.selectByIdForUpdate("tenant", "detail")).thenReturn(detail); when(revisionRequests.selectByIdForUpdate("tenant", "request")).thenReturn(request); DmsWenjianBanbenEntity main = businessVersion("main", "main-v1"); when(businessVersions.selectById("tenant", "main-v1")).thenReturn(main); return audit; } DmsWenjianBanbenEntity businessVersion(String fileId, String versionId) { DmsWenjianBanbenEntity value = new DmsWenjianBanbenEntity(); value.setId(versionId); value.setWenjianId(fileId); value.setWenjianLeixing("main".equals(fileId) ? "zhuwenjian" : "jilu"); value.setBizStatus("effective"); UUID centerId = UUID.randomUUID(), documentId = UUID.randomUUID(); value.setWenjianZhongxinBanbenId(centerId.toString()); when(centerVersions.version("tenant", centerId)).thenReturn(new jnpf.dmsEntity.center.DmsCenterVersionSnapshot( documentId, centerId, "D(00)", "a".repeat(64), "author", null, UUID.randomUUID(), "EFFECTIVE", approved, null)); return value; } @Test void freezesRemovedRecordsAndRejectsChangedRemovalBaselineByDigest() { arrangeRevision(); DmsWenjianGuanxiEntity relation = new DmsWenjianGuanxiEntity(); relation.setGuanlianWenjianId("record"); when(existingRelations.selectEffectiveByMainAndType("tenant", "main", "jilu")).thenReturn(List.of(relation)); DmsWenjianBanbenEntity record = businessVersion("record", "record-v1"); when(businessVersions.selectCurrentByFileId("tenant", "record")).thenReturn(record); DmsApprovedBatch first = source.load("tenant", "flow", "task", "xiuding_shenhe", "approver", approved).batch(); assertEquals(2, first.getItems().size()); DmsApprovedItem removed = first.getItems().get(1); assertEquals("RETIRE", removed.getChangeType()); assertEquals("record", removed.getFileId()); assertEquals("record-v1", removed.getBusinessVersionId()); assertEquals(removed.getFileVersionId(), removed.getBaseFileVersionId()); assertFalse(removed.requiresContentRegistration()); record.setId("record-v2"); assertNotEquals(first.getSourceDigest(), source.load("tenant", "flow", "task", "xiuding_shenhe", "approver", approved).batch().getSourceDigest()); } @Test void absentUnsavedRecordCreatesNoRetirementTarget() { arrangeRevision(); DmsApprovedBatch result = source.load("tenant", "flow", "task", "xiuding_shenhe", "approver", approved).batch(); assertEquals(1, result.getItems().size()); assertEquals("REUSE", result.getItems().get(0).getChangeType()); } }