package jnpf.dmsService; import com.fasterxml.jackson.databind.ObjectMapper; import jnpf.base.ActionResult; import jnpf.dmsEntity.storage.approval.*; import jnpf.dmsMapper.DmsApprovedBatchMapper; import jnpf.exception.DataException; import jnpf.file.FileContentApi; import jnpf.model.filecontent.*; import org.junit.jupiter.api.Test; import java.util.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; class DmsApprovedContentServiceTest { final FileContentApi files = mock(FileContentApi.class); final DmsApprovedBatchMapper progress = mock(DmsApprovedBatchMapper.class); final DmsApprovedFileAcceptanceService acceptance = mock(DmsApprovedFileAcceptanceService.class); final ObjectMapper json = new ObjectMapper(); final DmsApprovedContentService service = new DmsApprovedContentService(files, progress, acceptance, json); final DmsApprovedBatch batch = new DmsApprovedBatch(); final DmsApprovedItem item = new DmsApprovedItem(); final FileContentReceipt receipt = new FileContentReceipt(); DmsApprovedContentServiceTest() { batch.setId(UUID.randomUUID()); batch.setTenantId("tenant"); batch.setLeaseToken(UUID.randomUUID().toString()); batch.setSourceDigest("digest"); batch.setItems(List.of(item)); item.setSourceKey("audit:zhuwenjian"); item.setChangeType("CREATE"); item.setDocumentId(UUID.randomUUID()); item.setFileVersionId(UUID.randomUUID()); item.setFileJson("[{\"url\":\"/api/file/Image/annex/main.txt\"}]"); item.setDocumentName("main.txt"); receipt.setFileObjectId(UUID.randomUUID()); receipt.setChecksumSha256("a".repeat(64)); receipt.setSizeBytes(12L); receipt.setFileName("main.txt"); receipt.setStoragePlatform("seaweedfs-1"); receipt.setBucketName("jnpf-dms"); receipt.setObjectPath("2026/09/18/object/"); receipt.setObjectName("object.txt"); } void physicalResponse() { ActionResult response = new ActionResult<>(); response.setCode(200); response.setData(receipt); when(files.importContent(eq("jnpf-dms"), any())).thenReturn(response); } @Test void retryAfterCenterFailureReusesDurablyStoredPhysicalResult() throws Exception { physicalResponse(); when(progress.storePhysicalReceipt(eq("tenant"), eq(batch.getId()), eq(item.getSourceKey()), eq(batch.getLeaseToken()), anyString())).thenReturn(1); when(acceptance.acceptStoredContent(any())).thenThrow(new DataException("center failed")).thenReturn(batch); assertThrows(DataException.class, () -> service.register(batch)); assertNotNull(item.getStorageReceipt()); // A recovered batch reads this serialized receipt from the unified progress row. item.setStorageReceipt(json.writeValueAsString(receipt)); assertSame(batch, service.register(batch)); verify(files, times(1)).importContent(eq("jnpf-dms"), any()); org.mockito.InOrder order = inOrder(progress, acceptance); order.verify(progress).storePhysicalReceipt(eq("tenant"), eq(batch.getId()), eq(item.getSourceKey()), eq(batch.getLeaseToken()), anyString()); order.verify(acceptance, times(2)).acceptStoredContent(any()); } @Test void onlyNewAndRevisionAuditsRequestCommentRemoval() { physicalResponse(); when(progress.storePhysicalReceipt(anyString(), any(), anyString(), anyString(), anyString())).thenReturn(1); when(acceptance.acceptStoredContent(any())).thenReturn(batch); for (String type : new String[]{"xinzeng_shenhe", "xiuding_shenhe", "other", null}) { batch.setAuditType(type); item.setStorageReceipt(null); service.register(batch); } org.mockito.ArgumentCaptor request = org.mockito.ArgumentCaptor.forClass(FileContentImportForm.class); verify(files, times(4)).importContent(eq("jnpf-dms"), request.capture()); assertEquals(List.of(true, true, false, false), request.getAllValues().stream() .map(FileContentImportForm::isRemoveDocxComments).toList()); } @Test void leaseLossAfterPhysicalCopyStopsCenterRegistration() { physicalResponse(); assertThrows(DataException.class, () -> service.register(batch)); assertNull(item.getStorageReceipt()); verifyNoInteractions(acceptance); } @Test void malformedReceiptCannotAdvanceProgressOrCenter() { physicalResponse(); receipt.setChecksumSha256(null); assertThrows(DataException.class, () -> service.register(batch)); verifyNoInteractions(progress, acceptance); } @Test void removedAndUnchangedRecordsNeverCopyContent() { DmsApprovedItem unchanged = new DmsApprovedItem(); unchanged.setChangeType("REUSE"); item.setChangeType("RETIRE"); batch.setItems(List.of(item, unchanged)); when(acceptance.acceptStoredContent(any())).thenReturn(batch); assertSame(batch, service.register(batch)); verifyNoInteractions(files, progress); } @Test void sendsReservedIdsAndSignatureMetadataToPhysicalImport() throws Exception { physicalResponse(); item.setQianpiyeId("signature-page"); item.setQianpiyeFiles("[{\"url\":\"signature-url\"}]"); item.setApprovalValuesByTag(Map.of("dms.qianpiye.flow.approve-1.approver", "审核人")); item.setApprovalFlowVersionId("flow-version"); when(progress.storePhysicalReceipt(anyString(), any(), anyString(), anyString(), anyString())).thenReturn(1); when(acceptance.acceptStoredContent(any())).thenReturn(batch); assertSame(batch, service.register(batch)); org.mockito.ArgumentCaptor request = org.mockito.ArgumentCaptor.forClass(FileContentImportForm.class); verify(files).importContent(eq("jnpf-dms"), request.capture()); com.fasterxml.jackson.databind.JsonNode wire = json.readTree(json.writeValueAsString(request.getValue())); assertEquals(item.getDocumentId().toString(), wire.path("documentId").asText()); assertEquals(item.getFileVersionId().toString(), wire.path("fileVersionId").asText()); assertEquals(item.getFileJson(), wire.path("fileJson").asText()); assertEquals(item.getDocumentName(), wire.path("fileName").asText()); assertEquals(item.getQianpiyeId(), wire.path("signaturePageId").asText()); assertEquals(item.getQianpiyeFiles(), wire.path("signaturePageFiles").asText()); assertEquals("flow-version", wire.path("approvalFlowVersionId").asText()); assertEquals("审核人", wire.path("signaturePageValues") .path("dms.qianpiye.flow.approve-1.approver").asText()); assertFalse(wire.has("taskId")); assertFalse(wire.has("changeType")); } @Test void revisionApprovalPageFallsBackToTheValidatedLegacySource() { physicalResponse(); item.setChangeType("REVISE"); item.setApprovalPageFileJson("[{\"url\":\"revision-approval.docx\"}]"); item.setApprovalValuesByTag(Map.of("dms.qianpiye.flow.approve-1.approver", "修订审核人")); when(progress.storePhysicalReceipt(anyString(), any(), anyString(), anyString(), anyString())).thenReturn(1); when(acceptance.acceptStoredContent(any())).thenReturn(batch); assertSame(batch, service.register(batch)); org.mockito.ArgumentCaptor request = org.mockito.ArgumentCaptor.forClass(FileContentImportForm.class); verify(files).importContent(eq("jnpf-dms"), request.capture()); assertEquals(item.getApprovalPageFileJson(), request.getValue().getSignaturePageFiles()); assertEquals(item.getApprovalValuesByTag(), request.getValue().getSignaturePageValues()); } @Test void secondCopyFailureKeepsFirstReceiptForTheNextAttempt() throws Exception { DmsApprovedItem record = new DmsApprovedItem(); record.setSourceKey("audit:jilu:1"); record.setChangeType("REVISE"); record.setDocumentId(UUID.randomUUID()); record.setFileVersionId(UUID.randomUUID()); record.setFileJson("[{\"url\":\"/api/file/Image/annex/record.txt\"}]"); record.setDocumentName("record.txt"); batch.setItems(List.of(item, record)); when(progress.storePhysicalReceipt(anyString(), any(), anyString(), anyString(), anyString())).thenReturn(1); ActionResult first = new ActionResult<>(); first.setCode(200); first.setData(receipt); FileContentReceipt recordReceipt = json.readValue(json.writeValueAsString(receipt), FileContentReceipt.class); recordReceipt.setFileObjectId(UUID.randomUUID()); recordReceipt.setObjectName("record.txt"); ActionResult second = new ActionResult<>(); second.setCode(200); second.setData(recordReceipt); when(files.importContent(eq("jnpf-dms"), argThat(r -> r != null && item.getDocumentId().equals(r.getDocumentId())))).thenReturn(first); when(files.importContent(eq("jnpf-dms"), argThat(r -> r != null && record.getDocumentId().equals(r.getDocumentId())))) .thenThrow(new DataException("copy failed")).thenReturn(second); assertThrows(DataException.class, () -> service.register(batch)); assertNotNull(item.getStorageReceipt()); assertNull(record.getStorageReceipt()); verifyNoInteractions(acceptance); when(acceptance.acceptStoredContent(any())).thenReturn(batch); assertSame(batch, service.register(batch)); verify(files, times(1)).importContent(eq("jnpf-dms"), argThat(r -> r != null && item.getDocumentId().equals(r.getDocumentId()))); verify(files, times(2)).importContent(eq("jnpf-dms"), argThat(r -> r != null && record.getDocumentId().equals(r.getDocumentId()))); org.mockito.ArgumentCaptor callback = org.mockito.ArgumentCaptor.forClass(jnpf.dmsEntity.storage.form.DmsApprovedRegistrationCallback.class); verify(acceptance).acceptStoredContent(callback.capture()); assertEquals(2, callback.getValue().getFiles().size()); assertEquals(receipt.getFileObjectId(), callback.getValue().getFiles().get(0).getContent().getFileObjectId()); assertEquals(recordReceipt.getFileObjectId(), callback.getValue().getFiles().get(1).getContent().getFileObjectId()); } }