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.DmsFileRevisionRegisterForm;
|
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 DmsFileRevisionServiceTest {
|
|
@Test
|
void createsNewVersionAndAtomicallySwitchesEffectiveState() {
|
Fixture fixture = new Fixture();
|
DmsFileRevisionRegisterForm 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", "revision-1"))
|
.thenReturn(null, created);
|
|
DmsFileUploadVO result = fixture.service.register(form, "jnpf-file");
|
|
assertSame(created, result);
|
verify(fixture.fileObjectMapper).lockSource("tenant-1", "jnpf-file", "revision-1");
|
verify(fixture.decisionService).requireAllowed(any(), any(), any());
|
ArgumentCaptor<DmsFileVersionEntity> versionCaptor =
|
ArgumentCaptor.forClass(DmsFileVersionEntity.class);
|
verify(fixture.propertiesMapper).insertVersion(versionCaptor.capture(), org.mockito.ArgumentMatchers.eq(form.getExtendedProperties() == null ? null : form.getExtendedProperties().toString()));
|
assertEquals(2L, versionCaptor.getValue().getVersionNo());
|
assertEquals("D(01)", versionCaptor.getValue().getVersionLabel());
|
assertEquals("no", versionCaptor.getValue().getShifouShoukong());
|
verify(fixture.resourceMapper).insert(any(DmsPermissionResourceEntity.class));
|
verify(fixture.fileObjectMapper).insert(any(DmsFileObjectEntity.class));
|
verify(fixture.pdfGenerationTaskService).enqueueEligible("tenant-1", form.getFileVersionId(),
|
form.getFileName(), form.getChecksumSha256());
|
verify(fixture.versionStateMapper).supersedeCurrent("tenant-1", form.getDocumentId(),
|
form.getBaseFileVersionId(), "D(01)", "user-1");
|
verify(fixture.versionStateMapper).insertRevisionEffective("tenant-1", form.getDocumentId(),
|
form.getBaseFileVersionId(), form.getFileVersionId(), "revision-1",
|
"更新质量职责", "user-1");
|
verify(fixture.searchIndexJobService).enqueueContentUpsert("tenant-1", form.getDocumentId(),
|
form.getFileVersionId(), form.getChecksumSha256());
|
verify(fixture.operationLogService).recordRevision(any(),
|
org.mockito.ArgumentMatchers.eq("tenant-1"),
|
org.mockito.ArgumentMatchers.eq(form.getDocumentId()),
|
org.mockito.ArgumentMatchers.eq(form.getFileVersionId()),
|
org.mockito.ArgumentMatchers.eq("quality-manual-v2.docx"),
|
org.mockito.ArgumentMatchers.eq("jnpf-file"),
|
org.mockito.ArgumentMatchers.eq("revision-1"));
|
verify(fixture.auditAdapter).fileChange(any());
|
}
|
|
@Test
|
void ordinaryRevisionIgnoresForgedUncontrolledFlagAndInheritsBase() {
|
Fixture fixture = new Fixture();
|
fixture.current.setShifouShoukong("yes");
|
DmsFileRevisionRegisterForm form = fixture.form();
|
form.setShifouShoukong("no");
|
DmsFileUploadVO created = fixture.view(form);
|
created.setShifouShoukong("yes");
|
when(fixture.fileObjectMapper.selectUploadBySource("tenant-1", "jnpf-file", "revision-1"))
|
.thenReturn(null, created);
|
|
fixture.service.register(form, "jnpf-file");
|
|
ArgumentCaptor<DmsFileVersionEntity> 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 approvedRevisionPreservesTrustedUncontrolledFlag() {
|
Fixture fixture = new Fixture();
|
fixture.current.setShifouShoukong("yes");
|
DmsFileRevisionRegisterForm form = fixture.form();
|
form.setShifouShoukong("no");
|
DmsFileUploadVO created = fixture.view(form);
|
when(fixture.fileObjectMapper.selectUploadBySource("tenant-1", "jnpf-file", "revision-1"))
|
.thenReturn(null, created);
|
|
fixture.service.registerApproved(form, "jnpf-file", "flow-1", "task-1",
|
fixture.approvedActor());
|
|
ArgumentCaptor<DmsFileVersionEntity> 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 returnsExactIdempotentRetryWithoutWritingAgain() {
|
Fixture fixture = new Fixture();
|
DmsFileRevisionRegisterForm form = fixture.form();
|
DmsFileUploadVO existing = fixture.view(form);
|
when(fixture.fileObjectMapper.selectUploadBySource("tenant-1", "jnpf-file", "revision-1"))
|
.thenReturn(existing);
|
|
DmsFileUploadVO result = fixture.service.register(form, "jnpf-file");
|
|
assertSame(existing, result);
|
verify(fixture.documentMapper, never()).selectByTenantAndIdForUpdate(any(), any());
|
verify(fixture.propertiesMapper, never()).insertVersion(any(), org.mockito.ArgumentMatchers.nullable(String.class));
|
verify(fixture.versionStateMapper, never()).supersedeCurrent(any(), any(), any(), any(), any());
|
verify(fixture.searchIndexJobService).enqueueContentUpsert("tenant-1", form.getDocumentId(),
|
form.getFileVersionId(), form.getChecksumSha256());
|
verify(fixture.auditAdapter, never()).fileChange(any());
|
}
|
|
@Test
|
void ordinaryRevisionRetryAlsoIgnoresForgedFlagAndUsesBase() {
|
Fixture fixture = new Fixture();
|
fixture.current.setShifouShoukong("yes");
|
DmsFileRevisionRegisterForm form = fixture.form();
|
form.setShifouShoukong("no");
|
DmsFileUploadVO existing = fixture.view(form);
|
existing.setShifouShoukong("yes");
|
when(fixture.fileObjectMapper.selectUploadBySource("tenant-1", "jnpf-file", "revision-1"))
|
.thenReturn(existing);
|
|
assertSame(existing, fixture.service.register(form, "jnpf-file"));
|
|
verify(fixture.fileVersionMapper).selectByTenantAndId("tenant-1", form.getBaseFileVersionId());
|
verify(fixture.propertiesMapper, never()).insertVersion(any(), org.mockito.ArgumentMatchers.nullable(String.class));
|
}
|
|
@Test
|
void approvedRevisionRetryUsesTrustedUncontrolledFlag() {
|
Fixture fixture = new Fixture();
|
fixture.current.setShifouShoukong("yes");
|
DmsFileRevisionRegisterForm form = fixture.form();
|
form.setShifouShoukong("no");
|
DmsFileUploadVO existing = fixture.view(form);
|
when(fixture.fileObjectMapper.selectUploadBySource("tenant-1", "jnpf-file", "revision-1"))
|
.thenReturn(existing);
|
|
DmsFileUploadVO result = fixture.service.registerApproved(form, "jnpf-file", "flow-1", "task-1",
|
fixture.approvedActor());
|
|
assertSame(existing, result);
|
verify(fixture.fileVersionMapper, never()).selectByTenantAndId(any(), any());
|
verify(fixture.propertiesMapper, never()).insertVersion(any(), org.mockito.ArgumentMatchers.nullable(String.class));
|
}
|
|
@Test
|
void rejectsRetryWhenRevisionReasonChanges() {
|
Fixture fixture = new Fixture();
|
DmsFileRevisionRegisterForm form = fixture.form();
|
DmsFileUploadVO existing = fixture.view(form);
|
form.setRevisionReason("另一个原因");
|
when(fixture.fileObjectMapper.selectUploadBySource("tenant-1", "jnpf-file", "revision-1"))
|
.thenReturn(existing);
|
|
assertThrows(DmsPermissionException.class,
|
() -> fixture.service.register(form, "jnpf-file"));
|
|
verify(fixture.propertiesMapper, never()).insertVersion(any(), org.mockito.ArgumentMatchers.nullable(String.class));
|
}
|
|
@Test
|
void rejectsWhenSelectedVersionIsNoLongerCurrent() {
|
Fixture fixture = new Fixture();
|
DmsFileRevisionRegisterForm form = fixture.form();
|
DmsFileVersionEntity anotherCurrent = new DmsFileVersionEntity();
|
anotherCurrent.setId(UUID.fromString("018f0000-0000-7000-8000-000000000599"));
|
when(fixture.fileVersionMapper.selectCurrentEffective("tenant-1", form.getDocumentId()))
|
.thenReturn(anotherCurrent);
|
|
assertThrows(DmsPermissionException.class,
|
() -> fixture.service.register(form, "jnpf-file"));
|
|
verify(fixture.propertiesMapper, never()).insertVersion(any(), org.mockito.ArgumentMatchers.nullable(String.class));
|
verify(fixture.versionStateMapper, never()).supersedeCurrent(any(), any(), any(), any(), any());
|
}
|
|
@Test
|
void workflowRevisionKeepsBaseEffectiveUntilApproval() {
|
Fixture fixture = new Fixture();
|
DmsFileRevisionRegisterForm form = fixture.form();
|
DmsFileUploadVO created = fixture.view(form);
|
created.setLifecycleStatus("PENDING_APPROVAL");
|
when(fixture.fileObjectMapper.selectUploadBySource("tenant-1", "jnpf-file", "revision-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(), "revision:" + form.getBaseFileVersionId() + ":revision-1",
|
"flow-1", "task-1", "REVISE", "user-1");
|
verify(fixture.versionStateMapper, never()).supersedeCurrent(any(), any(), any(), any(), any());
|
verifyNoInteractions(fixture.searchIndexJobService);
|
}
|
|
@Test
|
void genericRevisionCannotBypassBusinessAuditChain() {
|
Fixture f=new Fixture();
|
var form=f.form();
|
org.mockito.Mockito.doThrow(new jnpf.exception.DataException("business chain required"))
|
.when(f.businessOccupancy).guardStandaloneActivation("tenant-1",form.getBaseFileVersionId());
|
assertThrows(jnpf.exception.DataException.class,()->f.service.register(form,"jnpf-file"));
|
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 DmsFileRevisionService service = new DmsFileRevisionService(new DmsVersionPropertiesService(propertiesMapper), currentIdentity,
|
decisionService, documentMapper, folderMapper, fileVersionMapper, resourceMapper,
|
fileObjectMapper, versionStateMapper, auditAdapter, operationLogService,
|
searchIndexJobService, pdfGenerationTaskService, businessOccupancy);
|
private final UUID documentId = UUID.fromString("018f0000-0000-7000-8000-000000000501");
|
private final UUID baseVersionId = UUID.fromString("018f0000-0000-7000-8000-000000000502");
|
private final DmsFileVersionEntity current = new DmsFileVersionEntity();
|
|
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");
|
DmsDocumentEntity document = new DmsDocumentEntity();
|
document.setId(documentId);
|
when(documentMapper.selectByTenantAndIdForUpdate("tenant-1", documentId))
|
.thenReturn(document);
|
current.setId(baseVersionId);
|
current.setDocumentId(documentId);
|
current.setShifouShoukong("no");
|
when(fileVersionMapper.selectCurrentEffective("tenant-1", documentId)).thenReturn(current);
|
when(fileVersionMapper.selectByTenantAndId("tenant-1", baseVersionId)).thenReturn(current);
|
when(fileVersionMapper.selectNextVersionNo("tenant-1", documentId)).thenReturn(2L);
|
when(versionStateMapper.supersedeCurrent(any(), any(), any(), any(), any())).thenReturn(1);
|
}
|
|
private DmsFileRevisionRegisterForm form() {
|
DmsFileRevisionRegisterForm form = new DmsFileRevisionRegisterForm();
|
form.setDocumentId(documentId);
|
form.setBaseFileVersionId(baseVersionId);
|
form.setFileVersionId(UUID.fromString("018f0000-0000-7000-8000-000000000503"));
|
form.setFileObjectId(UUID.fromString("018f0000-0000-7000-8000-000000000504"));
|
form.setVersionLabel("D(01)");
|
form.setFileName("quality-manual-v2.docx");
|
form.setMimeType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
|
form.setSizeBytes(1024L);
|
form.setChecksumSha256("b".repeat(64));
|
form.setPreparedBy("author-1");
|
form.setPreparedOn(LocalDate.of(2026, 8, 22));
|
form.setRevisionReason("更新质量职责");
|
form.setStoragePlatform("seaweedfs-1");
|
form.setBucketName("jnpf-dms");
|
form.setBasePath("dms/");
|
form.setObjectPath("2026/08/22/document/");
|
form.setObjectName("version-v2.docx");
|
form.setSourceId("revision-1");
|
return form;
|
}
|
|
private DmsFileUploadVO view(DmsFileRevisionRegisterForm form) {
|
DmsFileUploadVO view = new DmsFileUploadVO();
|
view.setDocumentId(form.getDocumentId());
|
view.setBaseFileVersionId(form.getBaseFileVersionId());
|
view.setFileVersionId(form.getFileVersionId());
|
view.setFileObjectId(form.getFileObjectId());
|
view.setVersionNo(2L);
|
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 ? "no" : form.getShifouShoukong());
|
view.setRevisionReason(form.getRevisionReason());
|
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;
|
}
|
}
|
}
|