package jnpf.dmsService; import jnpf.base.UserInfo; import jnpf.dmsEntity.permission.entity.DmsDocumentEntity; import jnpf.dmsEntity.permission.entity.DmsFileVersionEntity; import jnpf.dmsEntity.permission.enums.DmsPermissionAction; import jnpf.dmsEntity.permission.enums.DmsResourceType; import jnpf.dmsEntity.preview.model.DmsFilePreviewTicket; import jnpf.dmsEntity.preview.model.DmsWatermarkSnapshot; import jnpf.dmsEntity.print.form.DmsFilePrintTicketRequest; import jnpf.dmsEntity.print.form.DmsPrintOperationCreateForm; import jnpf.dmsEntity.print.vo.DmsFilePrintTicketVO; import jnpf.dmsEntity.storage.entity.DmsFileObjectEntity; import jnpf.dmsEntity.watermark.DmsWatermarkParameters; import jnpf.dmsMapper.DmsDocumentMapper; 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.List; import java.util.UUID; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; 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 DmsFilePrintServiceTest { 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 DmsDocumentMapper documentMapper = mock(DmsDocumentMapper.class); private final DmsFilePreviewTicketIssuer ticketIssuer = mock(DmsFilePreviewTicketIssuer.class); private final DmsPrintOperationLogService logService = mock(DmsPrintOperationLogService.class); private final DmsWatermarkPreviewResolver watermarkResolver = mock(DmsWatermarkPreviewResolver.class); private final DmsPdfGenerationTaskService pdfGenerationTaskService = mock(DmsPdfGenerationTaskService.class); private final DmsFilePrintService service = new DmsFilePrintService(currentIdentity, permissionGuard, versionMapper, objectMapper, documentMapper, ticketIssuer, logService, watermarkResolver, pdfGenerationTaskService); private final UUID documentId = UUID.fromString("018f0000-0000-7000-8000-000000000821"); private final UUID versionId = UUID.fromString("018f0000-0000-7000-8000-000000000822"); private final UUID operationId = UUID.fromString("018f0000-0000-7000-8000-000000000823"); @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(), org.mockito.ArgumentMatchers.eq( DmsFilePrintService.TICKET_TTL_SECONDS))).thenReturn("print-ticket"); when(watermarkResolver.resolvePrint(any(), any(), any(), any())).thenReturn(Collections.emptyList()); } @Test void issuesPrintScopedTicketAfterPrintPermissionAndRecordsInitiation() { DmsFileVersionEntity version = version(); when(versionMapper.selectReadableByTenantAndId("tenant-1", versionId)).thenReturn(version); when(objectMapper.selectByTenantAndFileVersionId("tenant-1", versionId)).thenReturn(object()); when(documentMapper.selectByTenantAndId("tenant-1", documentId)).thenReturn(document()); DmsFilePrintTicketVO result = service.createTicket(request()); assertEquals("print-ticket", result.getAccessTicket()); assertEquals(2, result.getCopies()); assertEquals("1-3", result.getPrintPages()); ArgumentCaptor context = ArgumentCaptor.forClass(DmsPermissionExecutionContext.class); verify(permissionGuard).execute(context.capture(), any()); assertEquals(DmsPermissionAction.PRINT, context.getValue().getActionCode()); assertEquals(DmsResourceType.FILE_VERSION, context.getValue().getResourceType()); assertEquals(versionId, context.getValue().getResourceId()); assertEquals(operationId, context.getValue().getOperationId()); assertNull(context.getValue().getSessionExpiresAt()); ArgumentCaptor ticket = ArgumentCaptor.forClass(DmsFilePreviewTicket.class); verify(ticketIssuer).issue(ticket.capture(), org.mockito.ArgumentMatchers.eq( DmsFilePrintService.TICKET_TTL_SECONDS)); assertEquals(DmsFilePreviewTicket.PRINT_SCOPE, ticket.getValue().getScope()); ArgumentCaptor log = ArgumentCaptor.forClass(DmsPrintOperationCreateForm.class); verify(logService).create(log.capture()); assertEquals(operationId.toString(), log.getValue().getClientEventId()); assertEquals("CONTROLLED_DOCUMENT", log.getValue().getPrintType()); assertEquals("FILE_PREVIEW", log.getValue().getSourceType()); assertEquals("打印页码:1-3", log.getValue().getTargets().get(0).getRemark()); verify(pdfGenerationTaskService).enqueueMissingEligible("tenant-1", versionId, "quality-manual.docx", "a".repeat(64)); } @Test void managedPrintCannotIssueTicketWithoutSerialWatermark() { var request = request(); request.setRequireSerialWatermark(true); var barcode = new DmsWatermarkParameters.CustomBarcode(); barcode.setBarValue("unique-1"); barcode.setBarText("打印流水号 unique-1 计数流水号 20260001"); request.setCustomBarcode(List.of(barcode)); when(versionMapper.selectReadableByTenantAndId("tenant-1", versionId)).thenReturn(version()); when(objectMapper.selectByTenantAndFileVersionId("tenant-1", versionId)).thenReturn(object()); when(documentMapper.selectByTenantAndId("tenant-1", documentId)).thenReturn(document()); assertThrows(jnpf.exception.DataException.class, () -> service.createTicket(request)); verify(ticketIssuer, never()).issue(any(), anyLong()); } @Test void requiredSerialAcceptsMatchingExistingCustomBarcodeSnapshot() { var request = request(); request.setRequireSerialWatermark(true); var barcode = new DmsWatermarkParameters.CustomBarcode(); barcode.setBarValue("20261003000001"); barcode.setBarText("打印流水号: 20261003000001 计数流水号: 20260001"); request.setCustomBarcode(List.of(barcode)); var snapshot = new DmsWatermarkSnapshot(); snapshot.setWatermarkType("BARCODE"); snapshot.setCustomBarcode(true); snapshot.setContent(barcode.getBarValue()); snapshot.setBarcodeText(barcode.getBarText()); snapshot.setPositions(List.of(new jnpf.dmsEntity.preview.model.DmsWatermarkPositionSnapshot())); when(versionMapper.selectReadableByTenantAndId("tenant-1", versionId)).thenReturn(version()); when(objectMapper.selectByTenantAndFileVersionId("tenant-1", versionId)).thenReturn(object()); when(documentMapper.selectByTenantAndId("tenant-1", documentId)).thenReturn(document()); when(watermarkResolver.resolvePrint(any(), org.mockito.ArgumentMatchers.eq("tenant-1"), any(), org.mockito.ArgumentMatchers.eq(request))).thenReturn(List.of(snapshot)); service.createTicket(request); ArgumentCaptor ticket = ArgumentCaptor.forClass(DmsFilePreviewTicket.class); verify(ticketIssuer).issue(ticket.capture(), org.mockito.ArgumentMatchers.eq( DmsFilePrintService.TICKET_TTL_SECONDS)); assertEquals(List.of(snapshot), ticket.getValue().getWatermarks()); } @Test void resolvesRuntimeParametersAgainstPrintConfigurationIntoTicketSnapshot() { DmsFileVersionEntity version = version(); DmsFilePrintTicketRequest request = request(); DmsWatermarkParameters.CustomBarcode barcode = new DmsWatermarkParameters.CustomBarcode(); barcode.setPage(2); barcode.setBarValue("COPY-002"); barcode.setBarText("第二份"); request.setCustomBarcode(List.of(barcode)); DmsWatermarkSnapshot snapshot = new DmsWatermarkSnapshot(); snapshot.setWatermarkType("BARCODE"); snapshot.setContent("COPY-002"); snapshot.setBarcodeText("第二份"); snapshot.setRenderPages(List.of(2)); when(versionMapper.selectReadableByTenantAndId("tenant-1", versionId)).thenReturn(version); when(objectMapper.selectByTenantAndFileVersionId("tenant-1", versionId)).thenReturn(object()); when(documentMapper.selectByTenantAndId("tenant-1", documentId)).thenReturn(document()); when(watermarkResolver.resolvePrint(any(), org.mockito.ArgumentMatchers.eq("tenant-1"), org.mockito.ArgumentMatchers.eq(version), org.mockito.ArgumentMatchers.eq(request))) .thenReturn(List.of(snapshot)); service.createTicket(request); verify(watermarkResolver).resolvePrint(any(), org.mockito.ArgumentMatchers.eq("tenant-1"), org.mockito.ArgumentMatchers.eq(version), org.mockito.ArgumentMatchers.eq(request)); ArgumentCaptor ticket = ArgumentCaptor.forClass(DmsFilePreviewTicket.class); verify(ticketIssuer).issue(ticket.capture(), org.mockito.ArgumentMatchers.eq( DmsFilePrintService.TICKET_TTL_SECONDS)); assertEquals(List.of(snapshot), ticket.getValue().getWatermarks()); assertEquals("1-3", ticket.getValue().getPrintPages()); } @Test void uncontrolledVersionSkipsPrintPermissionButKeepsPrintPipeline() { DmsFileVersionEntity version = version(); version.setShifouShoukong("no"); when(versionMapper.selectReadableByTenantAndId("tenant-1", versionId)).thenReturn(version); when(objectMapper.selectByTenantAndFileVersionId("tenant-1", versionId)).thenReturn(object()); when(documentMapper.selectByTenantAndId("tenant-1", documentId)).thenReturn(document()); doThrow(DmsPermissionException.of(DmsPermissionError.PERMISSION_DENIED, "no print permission")).when(permissionGuard).execute(any(), any()); DmsFilePrintTicketVO result = service.createTicket(request()); assertEquals("print-ticket", result.getAccessTicket()); verify(permissionGuard, never()).execute(any(), any()); verify(watermarkResolver).resolvePrint(any(), org.mockito.ArgumentMatchers.eq("tenant-1"), org.mockito.ArgumentMatchers.eq(version), any()); verify(logService).create(any()); } @Test void controlledVersionWithoutPrintPermissionIsRejected() { DmsFileVersionEntity version = version(); when(versionMapper.selectReadableByTenantAndId("tenant-1", versionId)).thenReturn(version); doThrow(DmsPermissionException.of(DmsPermissionError.PERMISSION_DENIED, "no print permission")).when(permissionGuard).execute(any(), any()); DmsPermissionException error = assertThrows( DmsPermissionException.class, () -> service.createTicket(request())); assertEquals(DmsPermissionError.PERMISSION_DENIED, error.getError()); verify(ticketIssuer, never()).issue(any(), anyLong()); } private DmsFilePrintTicketRequest request() { DmsFilePrintTicketRequest request = new DmsFilePrintTicketRequest(); request.setResourceType(DmsResourceType.FILE_VERSION); request.setResourceId(versionId); request.setOperationId(operationId); request.setCopies(2); request.setPrintPages("1-3"); return request; } private DmsFileVersionEntity version() { DmsFileVersionEntity version = new DmsFileVersionEntity(); version.setId(versionId); version.setDocumentId(documentId); version.setVersionLabel("V1.0"); version.setFileName("quality-manual.docx"); version.setSizeBytes(2048L); version.setChecksumSha256("a".repeat(64)); version.setShifouShoukong("yes"); return version; } private DmsFileObjectEntity object() { DmsFileObjectEntity object = new DmsFileObjectEntity(); object.setStoragePlatform("seaweedfs-1"); object.setBucketName("jnpf-dms"); object.setObjectPath("2026/09/document/"); object.setObjectName("manual-v1.docx"); return object; } private DmsDocumentEntity document() { DmsDocumentEntity document = new DmsDocumentEntity(); document.setId(documentId); document.setDocumentNo("QM-001"); document.setName("质量手册"); return document; } }