package jnpf.dmsService; import jnpf.base.UserInfo; import jnpf.dmsEntity.permission.entity.DmsDocumentEntity; import jnpf.dmsEntity.permission.entity.DmsFileVersionEntity; import jnpf.dmsEntity.permission.entity.DmsPermissionResourceEntity; import jnpf.dmsEntity.storage.entity.DmsFileObjectEntity; import jnpf.dmsEntity.storage.form.DmsFileUploadRegisterForm; import jnpf.dmsEntity.storage.vo.DmsFileUploadVO; import jnpf.dmsIntegration.audit.DmsAuditAdapter; import jnpf.dmsMapper.DmsDocumentMapper; import jnpf.dmsMapper.DmsFolderMapper; import jnpf.dmsMapper.DmsFileObjectMapper; import jnpf.dmsMapper.DmsFileVersionMapper; import jnpf.dmsMapper.DmsPermissionResourceMapper; import jnpf.dmsMapper.DmsVersionStateMapper; import jnpf.dmsPermission.DmsCurrentIdentity; import jnpf.dmsPermission.DmsPermissionException; import jnpf.dmsService.search.DmsSearchIndexJobService; import jnpf.dmsService.pdf.DmsPdfGenerationTaskService; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import java.time.LocalDate; import java.util.UUID; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; 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.verifyNoInteractions; import static org.mockito.Mockito.when; class DmsFileUploadServiceTest { @Test void registersDocumentVersionResourcesAndObjectInOneServiceCall() { Fixture fixture = new Fixture(); DmsFileUploadRegisterForm form = fixture.form(); form.setExtendedProperties(new com.fasterxml.jackson.databind.ObjectMapper().createObjectNode().put("prepared_by_name", "管理员")); DmsFileUploadVO created = fixture.view(form); when(fixture.fileObjectMapper.selectUploadBySource("tenant-1", "jnpf-file", "source-1")) .thenReturn(null, created); DmsFileUploadVO result = fixture.service.register(form, "jnpf-file"); assertSame(created, result); verify(fixture.decisionService).requireAllowed(any(), any(), any()); ArgumentCaptor documentCaptor = ArgumentCaptor.forClass(DmsDocumentEntity.class); verify(fixture.documentMapper).insert(documentCaptor.capture()); assertEquals("质量手册", documentCaptor.getValue().getName()); assertEquals("MM-R02", documentCaptor.getValue().getDocumentNo()); assertEquals("QUALITY_MANUAL", documentCaptor.getValue().getDocumentType()); assertEquals("dept-quality", documentCaptor.getValue().getResponsibleDeptId()); ArgumentCaptor versionCaptor = ArgumentCaptor.forClass(DmsFileVersionEntity.class); verify(fixture.propertiesMapper).insertVersion(versionCaptor.capture(), org.mockito.ArgumentMatchers.eq(form.getExtendedProperties() == null ? null : form.getExtendedProperties().toString())); assertEquals("D(00)", versionCaptor.getValue().getVersionLabel()); assertEquals("author-1", versionCaptor.getValue().getPreparedBy()); assertEquals(LocalDate.of(2026, 8, 21), versionCaptor.getValue().getPreparedOn()); assertEquals("yes", versionCaptor.getValue().getShifouShoukong()); verify(fixture.resourceMapper, org.mockito.Mockito.times(2)).insert(any(DmsPermissionResourceEntity.class)); verify(fixture.versionStateMapper).insertDirectEffective("tenant-1", form.getDocumentId(), form.getFileVersionId(), "user-1"); verify(fixture.fileObjectMapper).insert(any(DmsFileObjectEntity.class)); verify(fixture.searchIndexJobService).enqueueContentUpsert("tenant-1", form.getDocumentId(), form.getFileVersionId(), form.getChecksumSha256()); verify(fixture.operationLogService).recordUpload(any(), org.mockito.ArgumentMatchers.eq("tenant-1"), org.mockito.ArgumentMatchers.eq(form.getDocumentId()), org.mockito.ArgumentMatchers.eq(form.getFileVersionId()), org.mockito.ArgumentMatchers.eq("report.txt"), org.mockito.ArgumentMatchers.eq("jnpf-file"), org.mockito.ArgumentMatchers.eq("source-1")); verify(fixture.auditAdapter).fileChange(any()); } @Test void ordinaryUploadIgnoresForgedUncontrolledFlag() { Fixture fixture = new Fixture(); DmsFileUploadRegisterForm form = fixture.form(); form.setShifouShoukong("no"); DmsFileUploadVO created = fixture.view(form); created.setShifouShoukong("yes"); when(fixture.fileObjectMapper.selectUploadBySource("tenant-1", "jnpf-file", "source-1")) .thenReturn(null, created); fixture.service.register(form, "jnpf-file"); ArgumentCaptor versionCaptor = ArgumentCaptor.forClass(DmsFileVersionEntity.class); verify(fixture.propertiesMapper).insertVersion(versionCaptor.capture(), org.mockito.ArgumentMatchers.eq(form.getExtendedProperties() == null ? null : form.getExtendedProperties().toString())); assertEquals("yes", versionCaptor.getValue().getShifouShoukong()); } @Test void approvedUploadPreservesTrustedUncontrolledFlag() { Fixture fixture = new Fixture(); DmsFileUploadRegisterForm form = fixture.form(); form.setShifouShoukong("no"); DmsFileUploadVO created = fixture.view(form); when(fixture.fileObjectMapper.selectUploadBySource("tenant-1", "jnpf-file", "source-1")) .thenReturn(null, created); fixture.service.registerApproved(form, "jnpf-file", "flow-1", "task-1", fixture.approvedActor()); ArgumentCaptor versionCaptor = ArgumentCaptor.forClass(DmsFileVersionEntity.class); verify(fixture.propertiesMapper).insertVersion(versionCaptor.capture(), org.mockito.ArgumentMatchers.eq(form.getExtendedProperties() == null ? null : form.getExtendedProperties().toString())); assertEquals("no", versionCaptor.getValue().getShifouShoukong()); verify(fixture.decisionService, never()).requireAllowed(any(), any(), any()); } @Test void returnsExistingUploadWithoutDuplicateWrites() { Fixture fixture = new Fixture(); DmsFileUploadRegisterForm form = fixture.form(); DmsFileUploadVO existing = fixture.view(form); when(fixture.fileObjectMapper.selectUploadBySource("tenant-1", "jnpf-file", "source-1")) .thenReturn(existing); DmsFileUploadVO result = fixture.service.register(form, "jnpf-file"); assertSame(existing, result); verify(fixture.documentMapper, never()).insert(any(DmsDocumentEntity.class)); verify(fixture.propertiesMapper, never()).insertVersion(any(), org.mockito.ArgumentMatchers.nullable(String.class)); verify(fixture.versionStateMapper, never()).insertDirectEffective(any(), any(), any(), any()); verify(fixture.fileObjectMapper, never()).insert(any(DmsFileObjectEntity.class)); verify(fixture.searchIndexJobService).enqueueContentUpsert("tenant-1", form.getDocumentId(), form.getFileVersionId(), form.getChecksumSha256()); verify(fixture.auditAdapter, never()).fileChange(any()); } @Test void ordinaryUploadRetryAlsoIgnoresForgedUncontrolledFlag() { Fixture fixture = new Fixture(); DmsFileUploadRegisterForm form = fixture.form(); form.setShifouShoukong("no"); DmsFileUploadVO existing = fixture.view(form); existing.setShifouShoukong("yes"); when(fixture.fileObjectMapper.selectUploadBySource("tenant-1", "jnpf-file", "source-1")) .thenReturn(existing); assertSame(existing, fixture.service.register(form, "jnpf-file")); verify(fixture.propertiesMapper, never()).insertVersion(any(), org.mockito.ArgumentMatchers.nullable(String.class)); } @Test void approvedUploadRetryUsesTrustedUncontrolledFlag() { Fixture fixture = new Fixture(); DmsFileUploadRegisterForm form = fixture.form(); form.setShifouShoukong("no"); DmsFileUploadVO existing = fixture.view(form); when(fixture.fileObjectMapper.selectUploadBySource("tenant-1", "jnpf-file", "source-1")) .thenReturn(existing); DmsFileUploadVO result = fixture.service.registerApproved(form, "jnpf-file", "flow-1", "task-1", fixture.approvedActor()); assertSame(existing, result); verify(fixture.propertiesMapper, never()).insertVersion(any(), org.mockito.ArgumentMatchers.nullable(String.class)); } @Test void rejectsIdempotentRetryWhenDocumentNumberChanges() { Fixture fixture = new Fixture(); DmsFileUploadRegisterForm form = fixture.form(); DmsFileUploadVO existing = fixture.view(form); form.setDocumentNo("MM-R03"); when(fixture.fileObjectMapper.selectUploadBySource("tenant-1", "jnpf-file", "source-1")) .thenReturn(existing); assertThrows(DmsPermissionException.class, () -> fixture.service.register(form, "jnpf-file")); verify(fixture.documentMapper, never()).insert(any(DmsDocumentEntity.class)); verify(fixture.propertiesMapper, never()).insertVersion(any(), org.mockito.ArgumentMatchers.nullable(String.class)); verify(fixture.versionStateMapper, never()).insertDirectEffective(any(), any(), any(), any()); verify(fixture.fileObjectMapper, never()).insert(any(DmsFileObjectEntity.class)); } @Test void workflowUploadIsStoredPendingWithoutSearchIndex() { Fixture fixture = new Fixture(); DmsFileUploadRegisterForm form = fixture.form(); DmsFileUploadVO created = fixture.view(form); created.setLifecycleStatus("PENDING_APPROVAL"); when(fixture.fileObjectMapper.selectUploadBySource("tenant-1", "jnpf-file", "source-1")) .thenReturn(null, created); DmsFileUploadVO result = fixture.service.registerWorkflowPending( form, "jnpf-file", "flow-1", "task-1"); assertSame(created, result); verify(fixture.versionStateMapper).insertWorkflowPending("tenant-1", form.getDocumentId(), form.getFileVersionId(), "source-1", "flow-1", "task-1", "CREATE", "user-1"); verify(fixture.versionStateMapper, never()).insertDirectEffective(any(), any(), any(), any()); verifyNoInteractions(fixture.searchIndexJobService); } @Test void genericUploadCannotClaimBusinessOccupiedNumber() { Fixture f=new Fixture(); var form=f.form();form.setDocumentNo("BUSINESS-001"); org.mockito.Mockito.doThrow(new jnpf.exception.DataException("business number occupied")) .when(f.businessOccupancy).guardStandaloneRegistration("tenant-1","BUSINESS-001"); assertThrows(jnpf.exception.DataException.class,()->f.service.register(form,"jnpf-file")); org.mockito.Mockito.verifyNoInteractions(f.fileObjectMapper,f.documentMapper,f.versionStateMapper); } private static class Fixture { private final DmsCurrentIdentity currentIdentity = mock(DmsCurrentIdentity.class); private final DmsPermissionDecisionService decisionService = mock(DmsPermissionDecisionService.class); private final DmsDocumentMapper documentMapper = mock(DmsDocumentMapper.class); private final DmsFolderMapper folderMapper = mock(DmsFolderMapper.class); private final DmsFileVersionMapper fileVersionMapper = mock(DmsFileVersionMapper.class); private final jnpf.dmsMapper.DmsVersionPropertiesMapper propertiesMapper = mock(jnpf.dmsMapper.DmsVersionPropertiesMapper.class); private final DmsPermissionResourceMapper resourceMapper = mock(DmsPermissionResourceMapper.class); private final DmsFileObjectMapper fileObjectMapper = mock(DmsFileObjectMapper.class); private final DmsVersionStateMapper versionStateMapper = mock(DmsVersionStateMapper.class); private final DmsAuditAdapter auditAdapter = mock(DmsAuditAdapter.class); private final DmsFileOperationLogService operationLogService = mock(DmsFileOperationLogService.class); private final DmsSearchIndexJobService searchIndexJobService = mock(DmsSearchIndexJobService.class); private final DmsBusinessLifecycleGuard businessOccupancy = mock(DmsBusinessLifecycleGuard.class); private final DmsPdfGenerationTaskService pdfGenerationTaskService = mock(DmsPdfGenerationTaskService.class); private final DmsFileUploadService service = new DmsFileUploadService(new DmsVersionPropertiesService(propertiesMapper), currentIdentity, decisionService, documentMapper, folderMapper, fileVersionMapper, resourceMapper, fileObjectMapper, versionStateMapper, auditAdapter, operationLogService, searchIndexJobService, pdfGenerationTaskService, businessOccupancy); private Fixture() { when(propertiesMapper.insertVersion(any(), org.mockito.ArgumentMatchers.nullable(String.class))).thenReturn(1); UserInfo user = new UserInfo(); user.setUserId("user-1"); user.setTenantId("tenant-1"); when(currentIdentity.requireUser()).thenReturn(user); when(currentIdentity.tenantId(user)).thenReturn("tenant-1"); } private DmsFileUploadRegisterForm form() { DmsFileUploadRegisterForm form = new DmsFileUploadRegisterForm(); form.setDocumentId(UUID.fromString("018f0000-0000-7000-8000-000000000301")); form.setFileVersionId(UUID.fromString("018f0000-0000-7000-8000-000000000302")); form.setFileObjectId(UUID.fromString("018f0000-0000-7000-8000-000000000303")); form.setFolderId(UUID.fromString("018f0000-0000-7000-8000-000000000304")); form.setDocumentNo("MM-R02"); form.setDocumentName("质量手册"); form.setDocumentType("QUALITY_MANUAL"); form.setResponsibleDeptId("dept-quality"); form.setVersionLabel("D(00)"); form.setFileName("report.txt"); form.setMimeType("text/plain"); form.setSizeBytes(12L); form.setChecksumSha256("a".repeat(64)); form.setPreparedBy("author-1"); form.setPreparedOn(LocalDate.of(2026, 8, 21)); form.setStoragePlatform("seaweedfs-1"); form.setBucketName("jnpf-dms"); form.setBasePath("dms/"); form.setObjectPath("2026/08/17/document/"); form.setObjectName("version.txt"); form.setSourceId("source-1"); return form; } private DmsFileUploadVO view(DmsFileUploadRegisterForm form) { DmsFileUploadVO view = new DmsFileUploadVO(); view.setDocumentId(form.getDocumentId()); view.setFileVersionId(form.getFileVersionId()); view.setFileObjectId(form.getFileObjectId()); view.setFolderId(form.getFolderId()); view.setDocumentNo(form.getDocumentNo()); view.setDocumentName(form.getDocumentName()); view.setDocumentType(form.getDocumentType()); view.setResponsibleDeptId(form.getResponsibleDeptId()); view.setVersionNo(1L); view.setVersionLabel(form.getVersionLabel()); view.setFileName(form.getFileName()); view.setMimeType(form.getMimeType()); view.setSizeBytes(form.getSizeBytes()); view.setChecksumSha256(form.getChecksumSha256()); view.setPreparedBy(form.getPreparedBy()); view.setPreparedOn(form.getPreparedOn()); view.setShifouShoukong(form.getShifouShoukong() == null ? "yes" : form.getShifouShoukong()); view.setStoragePlatform(form.getStoragePlatform()); view.setBucketName(form.getBucketName()); view.setBasePath(form.getBasePath()); view.setObjectPath(form.getObjectPath()); view.setObjectName(form.getObjectName()); view.setSourceId(form.getSourceId()); return view; } private UserInfo approvedActor() { UserInfo actor = new UserInfo(); actor.setUserId("approver-1"); actor.setTenantId("tenant-1"); when(currentIdentity.tenantId(actor)).thenReturn("tenant-1"); return actor; } } }