package jnpf.fileStorage.dms; import jakarta.servlet.http.HttpServletResponse; import jnpf.dmsEntity.preview.model.DmsFilePreviewTicket; import jnpf.dmsEntity.preview.model.DmsWatermarkSnapshot; import jnpf.dmsEntity.storage.model.DmsPersistentPdfSource; import jnpf.exception.DataException; import jnpf.util.JsonUtil; import org.dromara.x.file.storage.core.FileInfo; import org.dromara.x.file.storage.core.FileStorageService; import org.dromara.x.file.storage.core.platform.MinioFileStorage; import org.junit.Test; import java.io.OutputStream; import java.util.Collections; import java.util.UUID; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.any; import static org.junit.Assert.fail; import static org.mockito.Mockito.RETURNS_DEEP_STUBS; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class DmsOnlyOfficeFileServiceTest { private static final UUID VERSION_ID = UUID.fromString("018f0000-0000-7000-8000-000000000702"); @Test public void parsesValidBoundPreviewPayload() { DmsFilePreviewTicket payload = payload(60_000L); DmsOnlyOfficeFileService service = service(payload); assertEquals(VERSION_ID, service.parse("ticket").getFileVersionId()); } @Test public void parsesValidPrintScopedPayload() { DmsFilePreviewTicket payload = payload(60_000L); payload.setScope(DmsFilePreviewTicket.PRINT_SCOPE); assertEquals(DmsFilePreviewTicket.PRINT_SCOPE, service(payload).parse("ticket").getScope()); } @Test(expected = DataException.class) public void rejectsExpiredPreviewPayload() { DmsOnlyOfficeFileService service = service(payload(-1L)); service.parse("ticket"); } @Test public void rejectsVersionMismatchBeforeAccessingStorage() { FileStorageService storageService = mock(FileStorageService.class); DmsFilePreviewTicket payload = payload(60_000L); String json = JsonUtil.getObjectToString(payload); DmsOnlyOfficeFileService service = new DmsOnlyOfficeFileService(storageService, ticket -> json, mock(DmsFileWatermarkService.class)); try { service.stream(UUID.fromString("018f0000-0000-7000-8000-000000000799"), "ticket", mock(HttpServletResponse.class)); fail("expected version mismatch"); } catch (DataException expected) { assertEquals("DMS 预览票据与文件版本不匹配", expected.getMessage()); } verify(storageService, never()).getFileStorageVerify("seaweedfs-1"); } @Test public void rejectsClientPrintTicketForInternalPrintBaseStream() { FileStorageService storageService = mock(FileStorageService.class); DmsFilePreviewTicket payload = payload(60_000L); payload.setScope(DmsFilePreviewTicket.PRINT_SCOPE); DmsOnlyOfficeFileService service = new DmsOnlyOfficeFileService(storageService, ticket -> JsonUtil.getObjectToString(payload), mock(DmsFileWatermarkService.class)); try { service.stream(VERSION_ID, "ticket", true, mock(HttpServletResponse.class)); fail("expected internal print-base scope rejection"); } catch (DataException expected) { assertEquals("只有内部打印基础票据可以读取基础源文件", expected.getMessage()); } verify(storageService, never()).getFileStorageVerify("seaweedfs-1"); } @Test public void dynamicallyWatermarksPngAndReturnsInlinePreviewHeaders() throws Exception { byte[] sourceBytes = new byte[]{(byte) 0x89, 0x50, 0x4e, 0x47, 1}; byte[] watermarkedBytes = new byte[]{(byte) 0x89, 0x50, 0x4e, 0x47, 2, 3}; DmsFilePreviewTicket payload = imagePayload(sourceBytes.length); FileStorageService storageService = storage(payload, sourceBytes); DmsFileWatermarkService watermarkService = mock(DmsFileWatermarkService.class); when(watermarkService.loadAndApply(any(), any(DmsFilePreviewTicket.class))) .thenAnswer(invocation -> { DmsImageWatermarkService.SourceReader reader = invocation.getArgument(0); DmsFilePreviewTicket parsed = invocation.getArgument(1); assertEquals(VERSION_ID, parsed.getFileVersionId()); assertArrayEquals(sourceBytes, reader.read()); return watermarkedBytes; }); DmsOnlyOfficeFileService service = new DmsOnlyOfficeFileService(storageService, ticket -> JsonUtil.getObjectToString(payload), watermarkService); org.springframework.mock.web.MockHttpServletResponse response = new org.springframework.mock.web.MockHttpServletResponse(); service.stream(VERSION_ID, "ticket", response); assertArrayEquals(watermarkedBytes, response.getContentAsByteArray()); assertEquals("image/png", response.getContentType()); assertEquals(watermarkedBytes.length, response.getContentLength()); assertEquals("private, no-store", response.getHeader("Cache-Control")); assertEquals("nosniff", response.getHeader("X-Content-Type-Options")); org.springframework.http.ContentDisposition disposition = org.springframework.http.ContentDisposition .parse(response.getHeader("Content-Disposition")); assertEquals("inline", disposition.getType()); assertEquals("diagram.png", disposition.getFilename()); verify(watermarkService).loadAndApply(any(), any(DmsFilePreviewTicket.class)); } @Test public void watermarkFailureDoesNotWriteOriginalPngToPreviewResponse() throws Exception { byte[] sourceBytes = new byte[]{(byte) 0x89, 0x50, 0x4e, 0x47, 1}; DmsFilePreviewTicket payload = imagePayload(sourceBytes.length); FileStorageService storageService = storage(payload, sourceBytes); DmsFileWatermarkService watermarkService = mock(DmsFileWatermarkService.class); when(watermarkService.loadAndApply(any(), any(DmsFilePreviewTicket.class))) .thenAnswer(invocation -> { DmsImageWatermarkService.SourceReader reader = invocation.getArgument(0); DmsFilePreviewTicket parsed = invocation.getArgument(1); assertEquals(VERSION_ID, parsed.getFileVersionId()); assertArrayEquals(sourceBytes, reader.read()); throw new DataException("图片水印生成失败"); }); DmsOnlyOfficeFileService service = new DmsOnlyOfficeFileService(storageService, ticket -> JsonUtil.getObjectToString(payload), watermarkService); org.springframework.mock.web.MockHttpServletResponse response = new org.springframework.mock.web.MockHttpServletResponse(); try { service.stream(VERSION_ID, "ticket", response); fail("expected image watermark failure"); } catch (DataException expected) { assertEquals("图片水印生成失败", expected.getMessage()); } assertEquals(0, response.getContentAsByteArray().length); assertNull(response.getContentType()); assertNull(response.getHeader("Content-Disposition")); verify(watermarkService).loadAndApply(any(), any(DmsFilePreviewTicket.class)); } @Test public void missingPersistentPdfFallsBackToOriginalWordForPrintConversion() throws Exception { DmsFilePreviewTicket payload = payload(60_000L); payload.setScope(DmsFilePreviewTicket.PRINT_BASE_SCOPE); DmsPersistentPdfSource pdf = new DmsPersistentPdfSource(); pdf.setObjectKey("dms-derived/pdf/version/missing.pdf"); pdf.setSizeBytes(128L); pdf.setSourceChecksumSha256("a".repeat(64)); payload.setPdfSource(pdf); FileStorageService storageService = mock(FileStorageService.class, RETURNS_DEEP_STUBS); MinioFileStorage storage = mock(MinioFileStorage.class); when(storage.getBucketName()).thenReturn("jnpf-dms"); when(storageService.getFileStorageVerify("seaweedfs-1")).thenReturn(storage); when(storageService.exists(any(FileInfo.class))).thenReturn(false); Class downloaderType = FileStorageService.class .getMethod("download", FileInfo.class).getReturnType(); Object downloader = mock(downloaderType, invocation -> null); when(storageService.download(any(FileInfo.class))).thenAnswer(invocation -> downloader); DmsPersistentPdfRecoveryReporter reporter = mock(DmsPersistentPdfRecoveryReporter.class); DmsOnlyOfficeFileService service = new DmsOnlyOfficeFileService(storageService, ticket -> JsonUtil.getObjectToString(payload), mock(DmsFileWatermarkService.class), reporter); service.stream(VERSION_ID, "ticket", true, new org.springframework.mock.web.MockHttpServletResponse()); org.mockito.ArgumentCaptor source = org.mockito.ArgumentCaptor.forClass(FileInfo.class); verify(storageService).download(source.capture()); assertEquals("2026/08/24/document/", source.getValue().getPath()); assertEquals("quality-manual-v1.docx", source.getValue().getFilename()); verify(reporter).report(payload, "持久化 PDF 对象不存在或检查失败"); } private DmsOnlyOfficeFileService service(DmsFilePreviewTicket payload) { String json = JsonUtil.getObjectToString(payload); return new DmsOnlyOfficeFileService(mock(FileStorageService.class), ticket -> json, mock(DmsFileWatermarkService.class)); } private FileStorageService storage(DmsFilePreviewTicket payload, byte[] sourceBytes) throws Exception { FileStorageService storageService = mock(FileStorageService.class); MinioFileStorage storage = mock(MinioFileStorage.class); when(storage.getBucketName()).thenReturn(payload.getBucketName()); when(storageService.getFileStorageVerify(payload.getStoragePlatform())).thenReturn(storage); Class downloaderType = FileStorageService.class .getMethod("download", FileInfo.class).getReturnType(); Object downloader = mock(downloaderType, invocation -> { if ("outputStream".equals(invocation.getMethod().getName())) { ((OutputStream) invocation.getArgument(0)).write(sourceBytes); } return null; }); when(storageService.download(any(FileInfo.class))).thenAnswer(invocation -> downloader); return storageService; } private DmsFilePreviewTicket imagePayload(long sizeBytes) { DmsFilePreviewTicket payload = payload(60_000L); payload.setFileName("diagram.png"); payload.setMimeType("image/png"); payload.setSizeBytes(sizeBytes); payload.setObjectPath("2026/08/24/image/"); payload.setObjectName("diagram-v1.png"); payload.setWatermarks(Collections.singletonList(new DmsWatermarkSnapshot())); return payload; } private DmsFilePreviewTicket payload(long expiresInMillis) { DmsFilePreviewTicket payload = new DmsFilePreviewTicket(); payload.setScope(DmsFilePreviewTicket.SCOPE); payload.setTenantId("tenant-1"); payload.setUserId("user-1"); payload.setFileVersionId(VERSION_ID); payload.setFileName("quality-manual.docx"); payload.setMimeType("application/vnd.openxmlformats-officedocument.wordprocessingml.document"); payload.setSizeBytes(2048L); payload.setStoragePlatform("seaweedfs-1"); payload.setBucketName("jnpf-dms"); payload.setBasePath("dms/"); payload.setObjectPath("2026/08/24/document/"); payload.setObjectName("quality-manual-v1.docx"); payload.setExpiresAt(System.currentTimeMillis() + expiresInMillis); return payload; } }