package jnpf.fileStorage.dms; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.ServletOutputStream; import jakarta.servlet.WriteListener; import jnpf.dmsEntity.download.model.DmsFileDownloadTicket; import jnpf.dmsEntity.storage.model.DmsPersistentPdfSource; import jnpf.dmsEntity.preview.model.DmsWatermarkSnapshot; import jnpf.exception.DataException; import jnpf.base.ActionResult; import jnpf.onlyoffice.DmsDownloadPdfApi; import jnpf.onlyoffice.DmsDownloadPdfRequest; import jnpf.util.JsonUtil; import org.dromara.x.file.storage.core.FileStorageService; import org.dromara.x.file.storage.core.FileInfo; import org.dromara.x.file.storage.core.platform.MinioFileStorage; import org.junit.Test; import java.io.ByteArrayOutputStream; import java.io.OutputStream; import java.util.Collections; import java.util.UUID; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; 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 DmsFileDownloadStorageServiceTest { private static final UUID VERSION_ID = UUID.fromString("018f0000-0000-7000-8000-000000000702"); @Test public void parsesValidDownloadPayloadWithFrozenWatermarks() { DmsFileDownloadTicket payload = payload(60_000L); payload.setWatermarks(Collections.singletonList(new DmsWatermarkSnapshot())); DmsFileDownloadStorageService service = service(payload); DmsFileDownloadTicket parsed = service.parse("ticket"); assertEquals(VERSION_ID, parsed.getFileVersionId()); assertEquals(1, parsed.getWatermarks().size()); } @Test(expected = DataException.class) public void rejectsExpiredDownloadPayload() { service(payload(-1L)).parse("ticket"); } @Test public void rejectsPreviewScopeBeforeAccessingStorage() { FileStorageService storageService = mock(FileStorageService.class); DmsFileDownloadTicket payload = payload(60_000L); payload.setScope("dms:onlyoffice:preview"); DmsFileDownloadStorageService service = new DmsFileDownloadStorageService(storageService, ticket -> JsonUtil.getObjectToString(payload), mock(DmsFileWatermarkService.class), mock(DmsDownloadPdfApi.class)); try { service.stream(VERSION_ID, "ticket", mock(HttpServletResponse.class)); fail("expected invalid ticket scope"); } catch (DataException expected) { assertEquals("DMS 下载票据无效或已过期", expected.getMessage()); } verify(storageService, never()).getFileStorageVerify("seaweedfs-1"); } @Test public void dynamicallyWatermarksDocxAndReturnsItAsAttachment() throws Exception { byte[] sourceBytes = new byte[]{1, 2, 3}; byte[] watermarkedBytes = new byte[]{4, 5, 6, 7}; DmsFileDownloadTicket payload = payload(60_000L); payload.setWatermarks(Collections.singletonList(new DmsWatermarkSnapshot())); FileStorageService storageService = mock(FileStorageService.class, org.mockito.Mockito.RETURNS_DEEP_STUBS); MinioFileStorage storage = mock(MinioFileStorage.class); when(storage.getBucketName()).thenReturn("jnpf-dms"); when(storageService.getFileStorageVerify("seaweedfs-1")).thenReturn(storage); Class downloaderType = FileStorageService.class .getMethod("download", FileInfo.class).getReturnType(); Object downloader = mock(downloaderType, invocation -> { if ("outputStream".equals(invocation.getMethod().getName())) { OutputStream output = invocation.getArgument(0); output.write(sourceBytes); } return null; }); when(storageService.download(any(FileInfo.class))).thenAnswer(invocation -> downloader); DmsFileWatermarkService watermarkService = mock(DmsFileWatermarkService.class); when(watermarkService.loadAndApply(any(), any(DmsFileDownloadTicket.class))) .thenAnswer(invocation -> { DmsImageWatermarkService.SourceReader reader = invocation.getArgument(0); DmsFileDownloadTicket parsed = invocation.getArgument(1); assertEquals(VERSION_ID, parsed.getFileVersionId()); assertArrayEquals(sourceBytes, reader.read()); return watermarkedBytes; }); DmsFileDownloadStorageService service = new DmsFileDownloadStorageService(storageService, ticket -> JsonUtil.getObjectToString(payload), watermarkService, mock(DmsDownloadPdfApi.class)); HttpServletResponse response = mock(HttpServletResponse.class); ByteArrayOutputStream responseBody = new ByteArrayOutputStream(); when(response.getOutputStream()).thenReturn(new ServletOutputStream() { @Override public boolean isReady() { return true; } @Override public void setWriteListener(WriteListener writeListener) { } @Override public void write(int value) { responseBody.write(value); } }); service.stream(VERSION_ID, "ticket", response); assertArrayEquals(watermarkedBytes, responseBody.toByteArray()); verify(response).setContentLengthLong(watermarkedBytes.length); org.mockito.ArgumentCaptor disposition = org.mockito.ArgumentCaptor.forClass(String.class); verify(response).setHeader(org.mockito.ArgumentMatchers.eq( org.springframework.http.HttpHeaders.CONTENT_DISPOSITION), disposition.capture()); assertTrue(disposition.getValue().startsWith("attachment;")); verify(watermarkService).loadAndApply(any(), any(DmsFileDownloadTicket.class)); } @Test public void dynamicallyWatermarksPngAndReturnsImageAttachmentHeaders() throws Exception { byte[] sourceBytes = new byte[]{(byte) 0x89, 0x50, 0x4e, 0x47, 1}; byte[] watermarkedBytes = new byte[]{(byte) 0x89, 0x50, 0x4e, 0x47, 2, 3}; DmsFileDownloadTicket payload = payload(60_000L); payload.setFileName("diagram.png"); payload.setMimeType("image/png"); payload.setSizeBytes((long) sourceBytes.length); payload.setObjectPath("2026/08/24/image/"); payload.setObjectName("diagram-v1.png"); payload.setWatermarks(Collections.singletonList(new DmsWatermarkSnapshot())); FileStorageService storageService = storage(payload); 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); DmsFileWatermarkService watermarkService = mock(DmsFileWatermarkService.class); when(watermarkService.loadAndApply(any(), any(DmsFileDownloadTicket.class))) .thenAnswer(invocation -> { DmsImageWatermarkService.SourceReader reader = invocation.getArgument(0); DmsFileDownloadTicket parsed = invocation.getArgument(1); assertEquals(VERSION_ID, parsed.getFileVersionId()); assertArrayEquals(sourceBytes, reader.read()); return watermarkedBytes; }); DmsFileDownloadStorageService service = new DmsFileDownloadStorageService(storageService, ticket -> JsonUtil.getObjectToString(payload), watermarkService, mock(DmsDownloadPdfApi.class)); 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("no-store", response.getHeader("Cache-Control")); org.springframework.http.ContentDisposition disposition = org.springframework.http.ContentDisposition .parse(response.getHeader("Content-Disposition")); assertEquals("attachment", disposition.getType()); assertEquals("diagram.png", disposition.getFilename()); verify(watermarkService).loadAndApply(any(), any(DmsFileDownloadTicket.class)); } @Test public void watermarkFailureDoesNotWriteOriginalPngToDownloadResponse() throws Exception { byte[] sourceBytes = new byte[]{(byte) 0x89, 0x50, 0x4e, 0x47, 1}; DmsFileDownloadTicket payload = payload(60_000L); payload.setFileName("diagram.png"); payload.setMimeType("image/png"); payload.setSizeBytes((long) sourceBytes.length); payload.setWatermarks(Collections.singletonList(new DmsWatermarkSnapshot())); FileStorageService storageService = storage(payload); 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); DmsFileWatermarkService watermarkService = mock(DmsFileWatermarkService.class); when(watermarkService.loadAndApply(any(), any(DmsFileDownloadTicket.class))) .thenAnswer(invocation -> { DmsImageWatermarkService.SourceReader reader = invocation.getArgument(0); assertArrayEquals(sourceBytes, reader.read()); throw new DataException("图片水印生成失败"); }); DmsFileDownloadStorageService service = new DmsFileDownloadStorageService(storageService, ticket -> JsonUtil.getObjectToString(payload), watermarkService, mock(DmsDownloadPdfApi.class)); 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")); } private DmsFileDownloadStorageService service(DmsFileDownloadTicket payload) { String json = JsonUtil.getObjectToString(payload); return new DmsFileDownloadStorageService(mock(FileStorageService.class), ticket -> json, mock(DmsFileWatermarkService.class), mock(DmsDownloadPdfApi.class)); } @Test public void pdfDownloadUsesSameTicketAndReturnsPdfAttachmentMetadata() throws Exception { DmsFileDownloadTicket payload = payload(60_000L); payload.setFileName("质量手册.V2.DOCX"); byte[] pdfBytes = "%PDF-1.7 converted".getBytes(java.nio.charset.StandardCharsets.UTF_8); DmsDownloadPdfApi converter = mock(DmsDownloadPdfApi.class); ActionResult converted = new ActionResult<>(); converted.setCode(200); converted.setData(pdfBytes); when(converter.render(new DmsDownloadPdfRequest(VERSION_ID, "download-ticket"))) .thenReturn(converted); FileStorageService storage = storage(payload); DmsFileWatermarkService watermarks = mock(DmsFileWatermarkService.class); DmsFileDownloadStorageService service = new DmsFileDownloadStorageService(storage, ticket -> JsonUtil.getObjectToString(payload), watermarks, converter); org.springframework.mock.web.MockHttpServletResponse response = new org.springframework.mock.web.MockHttpServletResponse(); service.stream(VERSION_ID, "download-ticket", "pdf", response); assertArrayEquals(pdfBytes, response.getContentAsByteArray()); assertEquals("application/pdf", response.getContentType()); assertEquals(pdfBytes.length, response.getContentLength()); assertEquals("no-store", response.getHeader("Cache-Control")); org.springframework.http.ContentDisposition disposition = org.springframework.http.ContentDisposition .parse(response.getHeader("Content-Disposition")); assertEquals("attachment", disposition.getType()); assertEquals("质量手册.V2.pdf", disposition.getFilename()); verify(storage, never()).download(any(FileInfo.class)); org.mockito.Mockito.verifyNoInteractions(watermarks); } @Test public void pdfDownloadUsesExistingPersistentPdfBeforeConverter() throws Exception { byte[] pdfBytes = "%PDF-1.7 persisted".getBytes(java.nio.charset.StandardCharsets.UTF_8); DmsFileDownloadTicket payload = payload(60_000L); DmsPersistentPdfSource pdf = new DmsPersistentPdfSource(); pdf.setObjectKey("dms-derived/pdf/018f0000/persisted.pdf"); pdf.setSizeBytes((long) pdfBytes.length); payload.setPdfSource(pdf); FileStorageService storage = storage(payload); when(storage.exists(any(FileInfo.class))).thenReturn(true); 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(pdfBytes); } return null; }); when(storage.download(any(FileInfo.class))).thenAnswer(invocation -> downloader); DmsDownloadPdfApi converter = mock(DmsDownloadPdfApi.class); DmsFileDownloadStorageService service = new DmsFileDownloadStorageService(storage, ticket -> JsonUtil.getObjectToString(payload), mock(DmsFileWatermarkService.class), converter); org.springframework.mock.web.MockHttpServletResponse response = new org.springframework.mock.web.MockHttpServletResponse(); service.stream(VERSION_ID, "ticket", "pdf", response); assertArrayEquals(pdfBytes, response.getContentAsByteArray()); org.mockito.Mockito.verifyNoInteractions(converter); } @Test public void missingPersistentPdfFallsBackToConverter() throws Exception { byte[] convertedBytes = "%PDF-1.7 converted".getBytes(java.nio.charset.StandardCharsets.UTF_8); DmsFileDownloadTicket payload = payload(60_000L); DmsPersistentPdfSource pdf = new DmsPersistentPdfSource(); pdf.setObjectKey("dms-derived/pdf/018f0000/missing.pdf"); pdf.setSizeBytes(123L); pdf.setSourceChecksumSha256("a".repeat(64)); payload.setPdfSource(pdf); FileStorageService storage = storage(payload); when(storage.exists(any(FileInfo.class))).thenReturn(false); DmsDownloadPdfApi converter = mock(DmsDownloadPdfApi.class); ActionResult result = new ActionResult<>(); result.setCode(200); result.setData(convertedBytes); when(converter.render(any())).thenReturn(result); DmsPersistentPdfRecoveryReporter reporter = mock(DmsPersistentPdfRecoveryReporter.class); DmsFileDownloadStorageService service = new DmsFileDownloadStorageService(storage, ticket -> JsonUtil.getObjectToString(payload), mock(DmsFileWatermarkService.class), converter, reporter); org.springframework.mock.web.MockHttpServletResponse response = new org.springframework.mock.web.MockHttpServletResponse(); service.stream(VERSION_ID, "ticket", "pdf", response); assertArrayEquals(convertedBytes, response.getContentAsByteArray()); verify(converter).render(new DmsDownloadPdfRequest(VERSION_ID, "ticket")); verify(reporter).report(payload, "持久化 PDF 对象不存在"); } @Test public void conversionFailureDoesNotReturnOriginalOrCommitPdfHeaders() { DmsFileDownloadTicket payload = payload(60_000L); FileStorageService storage = storage(payload); DmsDownloadPdfApi converter = mock(DmsDownloadPdfApi.class); ActionResult failed = new ActionResult<>(); failed.setCode(400); failed.setMsg("指定页水印生成失败"); when(converter.render(any())).thenReturn(failed); DmsFileDownloadStorageService service = new DmsFileDownloadStorageService(storage, ticket -> JsonUtil.getObjectToString(payload), mock(DmsFileWatermarkService.class), converter); org.springframework.mock.web.MockHttpServletResponse response = new org.springframework.mock.web.MockHttpServletResponse(); try { service.stream(VERSION_ID, "ticket", "pdf", response); fail("expected conversion error"); } catch (DataException expected) { assertEquals("指定页水印生成失败", expected.getMessage()); } assertEquals(0, response.getContentAsByteArray().length); assertEquals(null, response.getHeader("Content-Disposition")); verify(storage, never()).download(any(FileInfo.class)); } @Test public void rejectsUnsupportedFormatAndSourcesBeforeConversion() { DmsDownloadPdfApi converter = mock(DmsDownloadPdfApi.class); FileStorageService storage = mock(FileStorageService.class); DmsFileDownloadTicket payload = payload(60_000L); for (String[] input : new String[][]{{"quality.docx", "xlsx"}, {"quality.txt", "pdf"}}) { payload.setFileName(input[0]); DmsFileDownloadStorageService service = new DmsFileDownloadStorageService(storage, ticket -> JsonUtil.getObjectToString(payload), mock(DmsFileWatermarkService.class), converter); try { service.stream(VERSION_ID, "ticket", input[1], mock(HttpServletResponse.class)); fail("expected unsupported format"); } catch (DataException expected) { assertTrue(expected.getMessage().contains("支持")); } } org.mockito.Mockito.verifyNoInteractions(storage, converter); } @Test public void omittedFormatStreamsOriginalBytesWithoutConversion() throws Exception { DmsFileDownloadTicket payload = payload(60_000L); byte[] original = new byte[]{80, 75, 3, 4}; payload.setSizeBytes((long) original.length); assertOriginalStream(payload, original, null); payload.setFileName("existing.pdf"); payload.setMimeType("application/pdf"); assertOriginalStream(payload, original, "pdf"); } private void assertOriginalStream(DmsFileDownloadTicket payload, byte[] original, String format) throws Exception { FileStorageService storage = storage(payload); 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(original); } return null; }); when(storage.download(any(FileInfo.class))).thenAnswer(invocation -> downloader); DmsDownloadPdfApi converter = mock(DmsDownloadPdfApi.class); DmsFileDownloadStorageService service = new DmsFileDownloadStorageService(storage, ticket -> JsonUtil.getObjectToString(payload), mock(DmsFileWatermarkService.class), converter); org.springframework.mock.web.MockHttpServletResponse response = new org.springframework.mock.web.MockHttpServletResponse(); service.stream(VERSION_ID, "ticket", format, response); assertArrayEquals(original, response.getContentAsByteArray()); assertEquals(payload.getMimeType(), response.getContentType()); assertEquals(payload.getFileName(), org.springframework.http.ContentDisposition .parse(response.getHeader("Content-Disposition")).getFilename()); org.mockito.Mockito.verifyNoInteractions(converter); } private FileStorageService storage(DmsFileDownloadTicket payload) { FileStorageService service = mock(FileStorageService.class); MinioFileStorage storage = mock(MinioFileStorage.class); when(storage.getBucketName()).thenReturn(payload.getBucketName()); when(service.getFileStorageVerify(payload.getStoragePlatform())).thenReturn(storage); return service; } private DmsFileDownloadTicket payload(long expiresInMillis) { DmsFileDownloadTicket payload = new DmsFileDownloadTicket(); payload.setScope(DmsFileDownloadTicket.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; } }