package jnpf.controller; import cn.dev33.satoken.same.SaSameUtil; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import jnpf.audit.sdk.AuditClient; import jnpf.audit.sdk.AuditEventPublisher; import jnpf.audit.sdk.aspect.AuditLogAspect; import jnpf.dmsEntity.storage.form.DmsFileUploadRegisterForm; import jnpf.exception.DataException; import jnpf.fileStorage.dms.DmsCommonFileReader; import jnpf.fileStorage.dms.DmsApprovalPageMergeService; import jnpf.fileStorage.dms.DmsFileStorageService; import jnpf.fileStorage.dms.DmsQianpiyeDocxWriter; import jnpf.fileStorage.dms.FileContentImportService; import jnpf.i18n.config.I18nProperties; import jnpf.model.filecontent.FileContentImportForm; import jnpf.util.context.SpringContext; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mockito.MockedStatic; import org.mockito.MockMakers; import org.springframework.aop.aspectj.annotation.AspectJProxyFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.MessageSource; import org.springframework.context.support.StaticMessageSource; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.multipart.MultipartFile; import java.util.UUID; import java.util.Map; import java.util.function.Function; import static org.junit.Assert.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; public class FileContentImportHttpTest { private final ObjectMapper json = new ObjectMapper(); private final DmsCommonFileReader reader = mock(DmsCommonFileReader.class); private final DmsFileStorageService storage = mock(DmsFileStorageService.class); private final DmsApprovalPageMergeService approvalPages = mock(DmsApprovalPageMergeService.class); private final DmsQianpiyeDocxWriter qianpiyeWriter = mock(DmsQianpiyeDocxWriter.class); private final jnpf.fileStorage.dms.DmsDocxCommentCleaner cleaner = mock(jnpf.fileStorage.dms.DmsDocxCommentCleaner.class); private final FileContentImportService service = new FileContentImportService( reader, storage, approvalPages, qianpiyeWriter, cleaner, json); private final FileContentController controller = new FileContentController(service); private final MultipartFile attachment = mock(MultipartFile.class); private final FileContentImportForm form = new FileContentImportForm(); private final String url = "/api/file/Image/annex/main.txt"; private MockMvc mvc; private MockedStatic token; @Before public void setup() { token = mockStatic(SaSameUtil.class, withSettings().mockMaker(MockMakers.INLINE)); ApplicationContext context = mock(ApplicationContext.class); when(context.getBean(MessageSource.class)).thenReturn(new StaticMessageSource()); when(context.getBean(I18nProperties.class)).thenReturn(new I18nProperties()); SpringContext.setApplicationContext(context); mvc = MockMvcBuilders.standaloneSetup(controller).build(); form.setDocumentId(UUID.randomUUID()); form.setFileVersionId(UUID.randomUUID()); form.setFileName("main.txt"); when(attachment.getOriginalFilename()).thenReturn("main.txt"); form.setFileJson("[{\"url\":\"" + url + "\",\"name\":\"main.txt\"}]"); } @After public void closeToken() { token.close(); } private void readAttachment() { when(reader.withFile(eq(url), eq(form.getFileJson()), eq(form.getFileName()), any())) .thenAnswer(invocation -> invocation .>getArgument(3).apply(attachment)); } @Test public void httpImportDelegatesToOriginalStorageAndReturnsOnlyPhysicalFacts() throws Exception { readAttachment(); DmsFileUploadRegisterForm stored = new DmsFileUploadRegisterForm(); stored.setDocumentId(form.getDocumentId()); stored.setFileVersionId(form.getFileVersionId()); stored.setFileObjectId(UUID.randomUUID()); stored.setFileName("main.txt"); stored.setDocumentType("TXT"); stored.setMimeType("text/plain"); stored.setSizeBytes(12L); stored.setChecksumSha256("a".repeat(64)); stored.setStoragePlatform("seaweedfs-1"); stored.setBucketName("jnpf-dms"); stored.setBasePath(""); stored.setObjectPath("2026/09/18/" + form.getDocumentId() + "/"); stored.setObjectName(stored.getFileObjectId() + ".txt"); when(storage.storeWorkflowContent(form.getDocumentId(), form.getFileVersionId(), attachment)).thenReturn(stored); String body = mvc.perform(post("/Internal/file-content/imports") .header("X-DMS-Service-Id", "jnpf-dms").contentType(MediaType.APPLICATION_JSON) .content(json.writeValueAsBytes(form))) .andExpect(status().isOk()).andReturn().getResponse().getContentAsString(); JsonNode response = json.readTree(body); assertEquals(200, response.path("code").asInt()); JsonNode facts = response.path("data"); assertEquals(stored.getFileObjectId().toString(), facts.path("fileObjectId").asText()); assertEquals(stored.getObjectPath(), facts.path("objectPath").asText()); assertEquals(stored.getObjectName(), facts.path("objectName").asText()); assertEquals(stored.getChecksumSha256(), facts.path("checksumSha256").asText()); assertEquals(stored.getBucketName(), facts.path("bucketName").asText()); assertEquals(stored.getStoragePlatform(), facts.path("storagePlatform").asText()); assertEquals(stored.getBasePath(), facts.path("basePath").asText()); assertEquals(stored.getFileName(), facts.path("fileName").asText()); assertEquals(stored.getDocumentType(), facts.path("documentType").asText()); assertEquals(stored.getMimeType(), facts.path("mimeType").asText()); assertEquals(12L, facts.path("sizeBytes").asLong()); assertEquals(11, facts.size()); assertFalse(facts.has("documentId")); assertFalse(facts.has("fileVersionId")); assertFalse(facts.has("sourceId")); assertFalse(facts.has("status")); token.verify(SaSameUtil::checkCurrentRequestToken); verify(storage).storeWorkflowContent(form.getDocumentId(), form.getFileVersionId(), attachment); verifyNoMoreInteractions(storage); verify(reader).withFile(eq(url), eq(form.getFileJson()), eq("main.txt"), any()); verifyNoMoreInteractions(reader); verifyNoInteractions(approvalPages, qianpiyeWriter); } @Test public void approvalPageIsRenderedMergedAndStoredBeforeReturningReceipt() { form.setFileName("录入的文件名称"); String pageJson = "[{\"url\":\"/api/file/Image/annex/approval.docx\"}]"; form.setSignaturePageId("signature-page"); form.setSignaturePageFiles(pageJson); form.setSignaturePageValues(Map.of("dms.qianpiye.flow.approve-1.approver", "审核人")); form.setApprovalFlowVersionId("flow-version"); MultipartFile page = mock(MultipartFile.class); MultipartFile rendered = mock(MultipartFile.class); MultipartFile merged = mock(MultipartFile.class); MultipartFile cleaned = mock(MultipartFile.class); when(cleaned.getOriginalFilename()).thenReturn("main.docx"); form.setRemoveDocxComments(true); when(merged.getOriginalFilename()).thenReturn("main.docx"); when(cleaner.clean(merged)).thenReturn(cleaned); when(attachment.getOriginalFilename()).thenReturn("main.docx"); when(page.getOriginalFilename()).thenReturn("approval.docx"); when(approvalPages.withoutApprovalPages(attachment)).thenReturn(attachment); readAttachment(); when(reader.withFile(eq("/api/file/Image/annex/approval.docx"), eq(pageJson), eq("签批页"), any())) .thenAnswer(invocation -> invocation .>getArgument(3).apply(page)); when(qianpiyeWriter.render(page, form.getSignaturePageValues(), form.getApprovalFlowVersionId())).thenReturn(rendered); when(qianpiyeWriter.render(attachment, form.getSignaturePageValues(), form.getApprovalFlowVersionId())).thenReturn(attachment); when(approvalPages.replaceOrPrepend(attachment, rendered)).thenReturn(merged); DmsFileUploadRegisterForm stored = new DmsFileUploadRegisterForm(); stored.setFileObjectId(UUID.randomUUID()); when(storage.storeWorkflowContent(eq(form.getDocumentId()), eq(form.getFileVersionId()), any())) .thenAnswer(invocation -> { MultipartFile actual = invocation.getArgument(2); assertEquals("录入的文件名称.docx", actual.getOriginalFilename()); return stored; }); assertEquals(stored.getFileObjectId(), service.importContent(form).getFileObjectId()); verify(qianpiyeWriter).render(page, form.getSignaturePageValues(), form.getApprovalFlowVersionId()); verify(qianpiyeWriter).render(attachment, form.getSignaturePageValues(), form.getApprovalFlowVersionId()); verify(approvalPages).replaceOrPrepend(attachment, rendered); org.mockito.InOrder order = inOrder(approvalPages, cleaner, storage); order.verify(approvalPages).replaceOrPrepend(attachment, rendered); order.verify(cleaner).clean(merged); order.verify(storage).storeWorkflowContent(eq(form.getDocumentId()), eq(form.getFileVersionId()), argThat(file -> "录入的文件名称.docx".equals(file.getOriginalFilename()))); } @Test public void cleanupRunsBeforeStorageWithoutApprovalPageAndFailureStopsStorage() { form.setFileName("main.docx"); form.setRemoveDocxComments(true); when(attachment.getOriginalFilename()).thenReturn("main.docx"); readAttachment(); when(cleaner.clean(attachment)).thenThrow(new DataException("invalid docx")); assertThrows(DataException.class, () -> service.importContent(form)); verifyNoInteractions(storage); MultipartFile cleaned = mock(MultipartFile.class); when(cleaned.getOriginalFilename()).thenReturn("main.docx"); doReturn(cleaned).when(cleaner).clean(attachment); when(storage.storeWorkflowContent(form.getDocumentId(), form.getFileVersionId(), cleaned)) .thenReturn(new DmsFileUploadRegisterForm()); service.importContent(form); verify(storage).storeWorkflowContent(form.getDocumentId(), form.getFileVersionId(), cleaned); } @Test public void approvalPageConfigurationDoesNotBlockImageImport() { form.setRemoveDocxComments(true); form.setFileName("图片测试PIC-SMP100-01000.png"); String pageJson = "[{\"url\":\"/api/file/Image/annex/approval.docx\"}]"; form.setSignaturePageId("signature-page"); form.setSignaturePageFiles(pageJson); when(attachment.getOriginalFilename()).thenReturn("图片测试PIC-SMP100-01000.png"); readAttachment(); DmsFileUploadRegisterForm stored = new DmsFileUploadRegisterForm(); stored.setFileObjectId(UUID.randomUUID()); when(storage.storeWorkflowContent(form.getDocumentId(), form.getFileVersionId(), attachment)) .thenReturn(stored); assertEquals(stored.getFileObjectId(), service.importContent(form).getFileObjectId()); verify(reader).withFile(eq(url), eq(form.getFileJson()), eq(form.getFileName()), any()); verifyNoMoreInteractions(reader); verifyNoInteractions(approvalPages, qianpiyeWriter); verify(storage).storeWorkflowContent(form.getDocumentId(), form.getFileVersionId(), attachment); } @Test public void formalImportUsesEnteredTitleAndPreservesBytesAndSourceExtension() throws Exception { for (String title : new String[]{"设备清洁操作规程", "设备清洁操作规程.DOCX", "设备清洁操作规程.v2"}) { form.setFileName(title); byte[] bytes = "original content".getBytes(java.nio.charset.StandardCharsets.UTF_8); when(attachment.getOriginalFilename()).thenReturn("错误上传名.docx"); when(attachment.getBytes()).thenReturn(bytes); when(attachment.getSize()).thenReturn((long) bytes.length); when(attachment.getInputStream()).thenAnswer(i -> new java.io.ByteArrayInputStream(bytes)); readAttachment(); when(storage.storeWorkflowContent(eq(form.getDocumentId()), eq(form.getFileVersionId()), any())) .thenAnswer(invocation -> { MultipartFile actual = invocation.getArgument(2); String expected = title.endsWith(".DOCX") ? title : title + ".docx"; assertEquals(expected, actual.getOriginalFilename()); assertArrayEquals(bytes, actual.getBytes()); assertArrayEquals(bytes, actual.getInputStream().readAllBytes()); assertEquals(bytes.length, actual.getSize()); DmsFileUploadRegisterForm stored = new DmsFileUploadRegisterForm(); stored.setFileName(actual.getOriginalFilename()); return stored; }); assertEquals(title.endsWith(".DOCX") ? title : title + ".docx", service.importContent(form).getFileName()); } verifyNoInteractions(approvalPages, qianpiyeWriter, cleaner); } @Test public void httpRequiresBothReservedIdsAndAttachmentBeforeStorage() throws Exception { for (String field : new String[]{"documentId", "fileVersionId", "fileJson", "fileName"}) { com.fasterxml.jackson.databind.node.ObjectNode request = json.valueToTree(form); request.remove(field); mvc.perform(post("/Internal/file-content/imports").header("X-DMS-Service-Id", "jnpf-dms") .contentType(MediaType.APPLICATION_JSON).content(json.writeValueAsBytes(request))) .andExpect(status().isBadRequest()); } mvc.perform(post("/Internal/file-content/imports").contentType(MediaType.APPLICATION_JSON) .content(json.writeValueAsBytes(form))).andExpect(status().isBadRequest()); verifyNoInteractions(reader, storage); } @Test public void invalidServiceCannotReadOrCopyAttachments() { assertThrows(DataException.class, () -> controller.importContent("other-service", form)); token.verify(SaSameUtil::checkCurrentRequestToken); verifyNoInteractions(reader, storage); } @Test public void invalidInternalTokenStopsBeforeStorage() { IllegalStateException failure = new IllegalStateException("invalid internal token"); token.when(SaSameUtil::checkCurrentRequestToken).thenThrow(failure); assertSame(failure, assertThrows(IllegalStateException.class, () -> controller.importContent("jnpf-dms", form))); verifyNoInteractions(reader, storage); } @Test public void rejectsMultipleOrMalformedAttachmentsBeforeReading() { for (String value : new String[]{"[]", "[{\"url\":\"a\"},{\"url\":\"b\"}]", "{}", "null", "not-json", "{\"url\":1}", "{\"url\":\" \"}"}) { form.setFileJson(value); assertThrows(DataException.class, () -> service.importContent(form)); } verifyNoInteractions(reader, storage); } @Test public void originalStorageFailureIsNotConvertedIntoASuccessReceipt() { readAttachment(); DataException failure = new DataException("SeaweedFS 上传失败或未生成 SHA-256"); when(storage.storeWorkflowContent(form.getDocumentId(), form.getFileVersionId(), attachment)).thenThrow(failure); assertSame(failure, assertThrows(DataException.class, () -> service.importContent(form))); verify(storage).storeWorkflowContent(form.getDocumentId(), form.getFileVersionId(), attachment); verifyNoMoreInteractions(storage); } @Test public void successfulImportReturnsPhysicalObjectWithoutTechnicalAudit() { readAttachment(); DmsFileUploadRegisterForm stored = new DmsFileUploadRegisterForm(); stored.setFileObjectId(UUID.randomUUID()); when(storage.storeWorkflowContent(form.getDocumentId(), form.getFileVersionId(), attachment)) .thenReturn(stored); AuditClient audit = mock(AuditClient.class); AspectJProxyFactory factory = new AspectJProxyFactory(service); factory.addAspect(new AuditLogAspect(audit, mock(AuditEventPublisher.class), mock(ApplicationContext.class))); FileContentImportService audited = factory.getProxy(); assertEquals(stored.getFileObjectId(), audited.importContent(form).getFileObjectId()); verifyNoInteractions(audit); } }