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<SaSameUtil> 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
|
.<Function<MultipartFile, DmsFileUploadRegisterForm>>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
|
.<Function<MultipartFile, DmsFileUploadRegisterForm>>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);
|
}
|
}
|