package jnpf.dmsService; import jnpf.base.UserInfo; import jnpf.dmsEntity.download.form.DmsFileDownloadTicketRequest; import jnpf.dmsEntity.download.model.DmsFileDownloadTicket; import jnpf.dmsEntity.download.vo.DmsFileDownloadTicketVO; import jnpf.dmsEntity.permission.entity.DmsFileVersionEntity; import jnpf.dmsEntity.permission.enums.DmsPermissionAction; import jnpf.dmsEntity.permission.enums.DmsResourceType; import jnpf.dmsEntity.preview.model.DmsWatermarkSnapshot; 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.mockito.ArgumentCaptor; import java.util.Collections; import java.util.UUID; import static org.junit.jupiter.api.Assertions.assertEquals; 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 DmsFileDownloadServiceTest { 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 DmsFileDownloadTicketIssuer ticketIssuer = mock(DmsFileDownloadTicketIssuer.class); private final DmsFileOperationLogService operationLogService = mock(DmsFileOperationLogService.class); private final DmsWatermarkPreviewResolver watermarkResolver = mock(DmsWatermarkPreviewResolver.class); private final DmsPdfGenerationTaskService pdfGenerationTaskService = mock(DmsPdfGenerationTaskService.class); private final DmsFileDownloadService service = new DmsFileDownloadService(currentIdentity, permissionGuard, versionMapper, objectMapper, ticketIssuer, operationLogService, watermarkResolver, 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"); private UserInfo user; @BeforeEach void setUp() { 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(DmsFileDownloadTicket.class), org.mockito.ArgumentMatchers.eq(DmsFileDownloadService.TICKET_TTL_SECONDS))) .thenReturn("download-ticket"); } @Test void uncontrolledVersionStillRequiresDownloadPermissionCheck() { DmsFileVersionEntity version = version(); version.setShifouShoukong("no"); DmsWatermarkSnapshot watermark = new DmsWatermarkSnapshot(); when(versionMapper.selectCurrentEffective("tenant-1", documentId)).thenReturn(version); when(versionMapper.selectReadableByTenantAndId("tenant-1", versionId)).thenReturn(version); when(objectMapper.selectByTenantAndFileVersionId("tenant-1", versionId)).thenReturn(fileObject()); when(watermarkResolver.resolveDownload(org.mockito.ArgumentMatchers.eq(user), org.mockito.ArgumentMatchers.eq("tenant-1"), org.mockito.ArgumentMatchers.eq(version), any())) .thenReturn(Collections.singletonList(watermark)); DmsFileDownloadTicketVO result = service.createTicket( request(DmsResourceType.DOCUMENT, documentId)); assertEquals(versionId, result.getFileVersionId()); assertEquals("download-ticket", result.getAccessTicket()); assertTrue(result.getExpiresAt().getTime() > System.currentTimeMillis()); ArgumentCaptor contextCaptor = ArgumentCaptor.forClass(DmsPermissionExecutionContext.class); verify(permissionGuard).execute(contextCaptor.capture(), any()); assertEquals(DmsPermissionAction.DOWNLOAD, contextCaptor.getValue().getActionCode()); assertEquals(DmsResourceType.FILE_VERSION, contextCaptor.getValue().getResourceType()); assertEquals(versionId, contextCaptor.getValue().getResourceId()); assertEquals(operationId, contextCaptor.getValue().getOperationId()); ArgumentCaptor payloadCaptor = ArgumentCaptor.forClass(DmsFileDownloadTicket.class); verify(ticketIssuer).issue(payloadCaptor.capture(), org.mockito.ArgumentMatchers.eq(DmsFileDownloadService.TICKET_TTL_SECONDS)); DmsFileDownloadTicket payload = payloadCaptor.getValue(); assertEquals(DmsFileDownloadTicket.SCOPE, payload.getScope()); assertEquals(Collections.singletonList(watermark), payload.getWatermarks()); verify(operationLogService).recordDownload(user, "tenant-1", documentId, versionId, "quality-manual.docx", operationId); verify(pdfGenerationTaskService).enqueueMissingEligible("tenant-1", versionId, "quality-manual.docx", "a".repeat(64)); } @Test void uncontrolledVersionWithoutDownloadPermissionIsRejected() { DmsFileVersionEntity version = version(); version.setShifouShoukong("no"); when(versionMapper.selectReadableByTenantAndId("tenant-1", versionId)).thenReturn(version); doThrow(DmsPermissionException.of(DmsPermissionError.PERMISSION_DENIED, "no download 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 rejectsUnreadableHistoricalVersionBeforePermissionCheck() { 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 DmsFileDownloadTicketRequest request(DmsResourceType type, UUID id) { DmsFileDownloadTicketRequest request = new DmsFileDownloadTicketRequest(); request.setResourceType(type); request.setResourceId(id); request.setOperationId(operationId); 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)); 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; } }