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<String> 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<byte[]> 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<byte[]> 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<byte[]> 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;
|
}
|
}
|