package jnpf.dmsService; import jnpf.base.UserInfo; import jnpf.dmsEntity.permission.entity.DmsFileVersionEntity; import jnpf.dmsEntity.permission.enums.DmsPermissionAction; import jnpf.dmsEntity.permission.enums.DmsResourceType; import jnpf.dmsEntity.preview.enums.DmsPreviewFormat; import jnpf.dmsEntity.preview.form.DmsFilePreviewTicketRequest; import jnpf.dmsEntity.preview.model.DmsFilePreviewTicket; import jnpf.dmsEntity.preview.vo.DmsFilePreviewTicketVO; import jnpf.dmsEntity.storage.entity.DmsFileObjectEntity; import jnpf.dmsMapper.DmsFileObjectMapper; import jnpf.dmsMapper.DmsFileVersionMapper; import jnpf.dmsPermission.DmsCurrentIdentity; import jnpf.dmsPermission.DmsPermissionError; import jnpf.dmsPermission.DmsPermissionException; import jnpf.dmsPermission.DmsPermissionExecutionContext; import jnpf.dmsPermission.DmsPermissionGuard; import jnpf.dmsPermission.DmsPermissionOperation; import jnpf.dmsService.pdf.DmsPdfGenerationTaskService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.ArgumentCaptor; import java.util.UUID; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class DmsFilePreviewServiceTest { private final DmsCurrentIdentity currentIdentity = mock(DmsCurrentIdentity.class); private final DmsPermissionGuard permissionGuard = mock(DmsPermissionGuard.class); private final DmsFileVersionMapper versionMapper = mock(DmsFileVersionMapper.class); private final DmsFileObjectMapper objectMapper = mock(DmsFileObjectMapper.class); private final DmsFilePreviewTicketIssuer ticketIssuer = mock(DmsFilePreviewTicketIssuer.class); private final DmsFileOperationLogService operationLogService = mock(DmsFileOperationLogService.class); private final DmsWatermarkPreviewResolver watermarkPreviewResolver = mock(DmsWatermarkPreviewResolver.class); private final DmsPreviewPreparationClient previewPreparationClient = mock(DmsPreviewPreparationClient.class); private final DmsPdfGenerationTaskService pdfGenerationTaskService = mock(DmsPdfGenerationTaskService.class); private final DmsFilePreviewService service = new DmsFilePreviewService(currentIdentity, permissionGuard, versionMapper, objectMapper, ticketIssuer, operationLogService, watermarkPreviewResolver, previewPreparationClient, pdfGenerationTaskService); private final UUID documentId = UUID.fromString("018f0000-0000-7000-8000-000000000701"); private final UUID versionId = UUID.fromString("018f0000-0000-7000-8000-000000000702"); private final UUID operationId = UUID.fromString("018f0000-0000-7000-8000-000000000703"); @BeforeEach void setUp() { UserInfo user = new UserInfo(); user.setTenantId("tenant-1"); user.setUserId("user-1"); when(currentIdentity.requireUser()).thenReturn(user); when(currentIdentity.tenantId(user)).thenReturn("tenant-1"); when(permissionGuard.execute(any(), any())).thenAnswer(invocation -> { DmsPermissionOperation operation = invocation.getArgument(1); return operation.execute(); }); when(ticketIssuer.issue(any(DmsFilePreviewTicket.class), org.mockito.ArgumentMatchers.eq(DmsFilePreviewService.TICKET_TTL_SECONDS))) .thenReturn("preview-ticket"); when(watermarkPreviewResolver.resolve(any(), any(), any(), any())) .thenReturn(java.util.Collections.emptyList()); when(previewPreparationClient.prepare(any())).thenAnswer(invocation -> invocation.getArgument(0)); } @Test void documentPreviewResolvesCurrentVersionAndGuardsExactVersion() { DmsFileVersionEntity version = version(); when(versionMapper.selectCurrentEffective("tenant-1", documentId)).thenReturn(version); when(versionMapper.selectReadableByTenantAndId("tenant-1", versionId)).thenReturn(version); when(objectMapper.selectByTenantAndFileVersionId("tenant-1", versionId)).thenReturn(fileObject()); long before = System.currentTimeMillis(); DmsFilePreviewTicketVO result = service.createTicket(request(DmsResourceType.DOCUMENT, documentId)); assertEquals(versionId, result.getFileVersionId()); assertEquals("quality-manual.docx", result.getFileName()); assertEquals("preview-ticket", result.getAccessTicket()); assertTrue(result.getExpiresAt().getTime() >= before + 299_000L); ArgumentCaptor contextCaptor = ArgumentCaptor.forClass(DmsPermissionExecutionContext.class); verify(permissionGuard).execute(contextCaptor.capture(), any()); DmsPermissionExecutionContext context = contextCaptor.getValue(); assertEquals(DmsResourceType.FILE_VERSION, context.getResourceType()); assertEquals(versionId, context.getResourceId()); assertEquals(DmsPermissionAction.READ, context.getActionCode()); assertEquals(operationId, context.getOperationId()); assertNotNull(context.getSessionExpiresAt()); ArgumentCaptor payloadCaptor = ArgumentCaptor.forClass(DmsFilePreviewTicket.class); verify(ticketIssuer).issue(payloadCaptor.capture(), org.mockito.ArgumentMatchers.eq(DmsFilePreviewService.TICKET_TTL_SECONDS)); DmsFilePreviewTicket payload = payloadCaptor.getValue(); assertEquals("tenant-1", payload.getTenantId()); assertEquals("user-1", payload.getUserId()); assertEquals(versionId, payload.getFileVersionId()); assertEquals("seaweedfs-1", payload.getStoragePlatform()); assertEquals(DmsPreviewFormat.PDF, payload.getPreviewFormat()); verify(operationLogService).recordView(any(), org.mockito.ArgumentMatchers.eq("tenant-1"), org.mockito.ArgumentMatchers.eq(documentId), org.mockito.ArgumentMatchers.eq(versionId), org.mockito.ArgumentMatchers.eq("quality-manual.docx"), org.mockito.ArgumentMatchers.eq(operationId)); verify(pdfGenerationTaskService).enqueueMissingEligible("tenant-1", versionId, "quality-manual.docx", "a".repeat(64)); } @ParameterizedTest @ValueSource(strings = {"download", "print", "preview", "other", ""}) void previewTicketSelectsRequestedWatermarkScene(String watermarkType) { DmsFileVersionEntity version = version(); when(versionMapper.selectReadableByTenantAndId("tenant-1", versionId)).thenReturn(version); when(objectMapper.selectByTenantAndFileVersionId("tenant-1", versionId)).thenReturn(fileObject()); DmsFilePreviewTicketRequest request = request(DmsResourceType.FILE_VERSION, versionId); request.setWatermarkType(watermarkType); service.createTicket(request); if ("download".equals(watermarkType)) { verify(watermarkPreviewResolver).resolveDownload(any(), org.mockito.ArgumentMatchers.eq("tenant-1"), org.mockito.ArgumentMatchers.eq(version), org.mockito.ArgumentMatchers.eq(request)); } else if ("print".equals(watermarkType)) { verify(watermarkPreviewResolver).resolvePrint(any(), org.mockito.ArgumentMatchers.eq("tenant-1"), org.mockito.ArgumentMatchers.eq(version), org.mockito.ArgumentMatchers.eq(request)); } else { verify(watermarkPreviewResolver).resolve(any(), org.mockito.ArgumentMatchers.eq("tenant-1"), org.mockito.ArgumentMatchers.eq(version), org.mockito.ArgumentMatchers.eq(request)); } } @Test void historicalPreviewUsesSelectedFileVersionWithoutResolvingCurrentVersion() { DmsFileVersionEntity version = version(); version.setLifecycleStatus("INVALIDATED"); when(versionMapper.selectReadableByTenantAndId("tenant-1", versionId)).thenReturn(version); when(objectMapper.selectByTenantAndFileVersionId("tenant-1", versionId)).thenReturn(fileObject()); DmsFilePreviewTicketVO result = service.createTicket( request(DmsResourceType.FILE_VERSION, versionId)); assertEquals(versionId, result.getFileVersionId()); verify(versionMapper, never()).selectCurrentEffective(any(), any()); ArgumentCaptor historical = ArgumentCaptor.forClass(DmsFilePreviewTicket.class); verify(ticketIssuer).issue(historical.capture(), anyLong()); assertTrue(historical.getValue().isHistoricalAccess()); } @Test void uncontrolledVersionSkipsReadPermissionButKeepsPreviewPipeline() { DmsFileVersionEntity version = version(); version.setShifouShoukong("no"); when(versionMapper.selectReadableByTenantAndId("tenant-1", versionId)).thenReturn(version); when(objectMapper.selectByTenantAndFileVersionId("tenant-1", versionId)).thenReturn(fileObject()); doThrow(DmsPermissionException.of(DmsPermissionError.PERMISSION_DENIED, "no read permission")).when(permissionGuard).execute(any(), any()); DmsFilePreviewTicketVO result = service.createTicket( request(DmsResourceType.FILE_VERSION, versionId)); assertEquals("preview-ticket", result.getAccessTicket()); verify(permissionGuard, never()).execute(any(), any()); verify(previewPreparationClient).prepare(any()); verify(operationLogService).recordView(any(), org.mockito.ArgumentMatchers.eq("tenant-1"), org.mockito.ArgumentMatchers.eq(documentId), org.mockito.ArgumentMatchers.eq(versionId), org.mockito.ArgumentMatchers.eq("quality-manual.docx"), org.mockito.ArgumentMatchers.eq(operationId)); } @Test void controlledVersionWithoutReadPermissionIsRejected() { DmsFileVersionEntity version = version(); when(versionMapper.selectReadableByTenantAndId("tenant-1", versionId)).thenReturn(version); doThrow(DmsPermissionException.of(DmsPermissionError.PERMISSION_DENIED, "no read permission")).when(permissionGuard).execute(any(), any()); DmsPermissionException error = assertThrows(DmsPermissionException.class, () -> service.createTicket(request(DmsResourceType.FILE_VERSION, versionId))); assertEquals(DmsPermissionError.PERMISSION_DENIED, error.getError()); verify(ticketIssuer, never()).issue(any(), anyLong()); } @Test void returnsPreparedPdfMetadataInsteadOfOriginalDocxMetadata() { DmsFileVersionEntity version = version(); when(versionMapper.selectReadableByTenantAndId("tenant-1", versionId)).thenReturn(version); when(objectMapper.selectByTenantAndFileVersionId("tenant-1", versionId)).thenReturn(fileObject()); when(previewPreparationClient.prepare(any())).thenAnswer(invocation -> { DmsFilePreviewTicket prepared = invocation.getArgument(0); prepared.setFileName("quality-manual.pdf"); prepared.setMimeType("application/pdf"); prepared.setSizeBytes(4096L); return prepared; }); DmsFilePreviewTicketVO result = service.createTicket( request(DmsResourceType.FILE_VERSION, versionId)); assertEquals("quality-manual.pdf", result.getFileName()); assertEquals("application/pdf", result.getMimeType()); assertEquals(Long.valueOf(4096L), result.getSizeBytes()); } @Test void folderCannotBeUsedAsPreviewResource() { DmsPermissionException error = assertThrows(DmsPermissionException.class, () -> service.createTicket(request(DmsResourceType.FOLDER, documentId))); assertEquals(DmsPermissionError.INVALID_REQUEST, error.getError()); verify(permissionGuard, never()).execute(any(), any()); verify(ticketIssuer, never()).issue(any(), anyLong()); } @Test void missingStoredObjectDoesNotIssueTicket() { DmsFileVersionEntity version = version(); when(versionMapper.selectReadableByTenantAndId("tenant-1", versionId)).thenReturn(version); DmsPermissionException error = assertThrows(DmsPermissionException.class, () -> service.createTicket(request(DmsResourceType.FILE_VERSION, versionId))); assertEquals(DmsPermissionError.RESOURCE_NOT_FOUND, error.getError()); verify(ticketIssuer, never()).issue(any(), anyLong()); } @Test void pendingVersionCannotIssuePreviewTicket() { DmsPermissionException error = assertThrows(DmsPermissionException.class, () -> service.createTicket(request(DmsResourceType.FILE_VERSION, versionId))); assertEquals(DmsPermissionError.RESOURCE_NOT_FOUND, error.getError()); verify(permissionGuard, never()).execute(any(), any()); verify(ticketIssuer, never()).issue(any(), anyLong()); } private DmsFilePreviewTicketRequest request(DmsResourceType type, UUID id) { DmsFilePreviewTicketRequest request = new DmsFilePreviewTicketRequest(); request.setResourceType(type); request.setResourceId(id); request.setOperationId(operationId); request.setPreviewFormat(DmsPreviewFormat.PDF); return request; } private DmsFileVersionEntity version() { DmsFileVersionEntity version = new DmsFileVersionEntity(); version.setId(versionId); version.setDocumentId(documentId); version.setFileName("quality-manual.docx"); version.setMimeType("application/vnd.openxmlformats-officedocument.wordprocessingml.document"); version.setSizeBytes(2048L); version.setChecksumSha256("a".repeat(64)); version.setShifouShoukong("yes"); return version; } private DmsFileObjectEntity fileObject() { DmsFileObjectEntity object = new DmsFileObjectEntity(); object.setFileVersionId(versionId); object.setStoragePlatform("seaweedfs-1"); object.setBucketName("jnpf-dms"); object.setBasePath("dms/"); object.setObjectPath("2026/08/24/document/"); object.setObjectName("quality-manual-v1.docx"); return object; } }