package jnpf.dmsService; import jnpf.dmsEntity.permission.entity.DmsDocumentEntity; import jnpf.dmsEntity.permission.entity.DmsFileVersionEntity; import jnpf.dmsEntity.storage.form.DmsFileLifecycleUpdateForm; import jnpf.dmsEntity.storage.vo.DmsFileLifecycleUpdateVO; import jnpf.dmsIntegration.audit.DmsVersionStateAuditRow; import jnpf.dmsMapper.DmsDocumentMapper; import jnpf.dmsMapper.DmsFileVersionMapper; import jnpf.dmsMapper.DmsVersionStateMapper; import jnpf.exception.DataException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.Date; 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.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 DmsFileLifecycleServiceTest { private static final String TENANT = "tenant-1"; private static final String SERVICE_ID = "jnpf-file"; private static final UUID VERSION = UUID.fromString("018f0000-0000-7000-8000-000000000901"); private static final UUID DOCUMENT = UUID.fromString("018f0000-0000-7000-8000-000000000902"); private static final Date EFFECTIVE_FROM = new Date(1726329600000L); private static final Date EFFECTIVE_TO = new Date(1757865600000L); private final DmsFileVersionMapper versions = mock(DmsFileVersionMapper.class); private final DmsDocumentMapper documents = mock(DmsDocumentMapper.class); private final DmsVersionStateMapper states = mock(DmsVersionStateMapper.class); private final jnpf.dmsService.expiry.DmsFileExpiryProjection projection = mock(jnpf.dmsService.expiry.DmsFileExpiryProjection.class); private final DmsBusinessLifecycleGuard businessOccupancy = mock(DmsBusinessLifecycleGuard.class); private final DmsFileLifecycleService service = new DmsFileLifecycleService( versions, documents, states, projection, businessOccupancy); private DmsFileLifecycleUpdateForm form; private DmsVersionStateAuditRow state; @BeforeEach void setUp() { when(businessOccupancy.tryEnterExpiry(TENANT, VERSION)).thenReturn(true); form = new DmsFileLifecycleUpdateForm(); form.setTenantId(" tenant-1 "); form.setFileVersionId(VERSION); form.setLifecycleStatus("EXPIRED"); form.setEffectiveFrom(EFFECTIVE_FROM); form.setEffectiveTo(EFFECTIVE_TO); form.setStatusReason(" 到期检查确认 "); DmsFileVersionEntity version = new DmsFileVersionEntity(); version.setDocumentId(DOCUMENT); when(versions.selectByTenantAndId(TENANT, VERSION)).thenReturn(version); when(documents.selectByTenantAndIdForUpdate(TENANT, DOCUMENT)) .thenReturn(new DmsDocumentEntity()); state = new DmsVersionStateAuditRow(); state.setLifecycleStatus("EFFECTIVE"); state.setEffectiveFrom(EFFECTIVE_FROM); state.setEffectiveTo(EFFECTIVE_TO); when(states.selectAuditState(TENANT, VERSION)).thenReturn(state); when(states.expire(TENANT, VERSION, "到期检查确认", SERVICE_ID)).thenReturn(1); } @Test void occupiedFileWaitsWithoutTakingDocumentLockOrChangingDates() { when(businessOccupancy.tryEnterExpiry(TENANT, VERSION)).thenReturn(false); assertThrows(DataException.class, () -> service.updateExpired(form, SERVICE_ID)); verifyNoInteractions(versions, documents, states, projection); } @Test void expiresEffectiveFileAndReturnsPersistedValues() { DmsFileLifecycleUpdateVO result = service.updateExpired(form, SERVICE_ID); assertEquals(VERSION, result.getFileVersionId()); assertEquals("EXPIRED", result.getLifecycleStatus()); assertSame(EFFECTIVE_FROM, result.getEffectiveFrom()); assertSame(EFFECTIVE_TO, result.getEffectiveTo()); verify(documents).selectByTenantAndIdForUpdate(TENANT, DOCUMENT); verify(states).expire(TENANT, VERSION, "到期检查确认", SERVICE_ID); verify(projection).apply(TENANT, VERSION, EFFECTIVE_TO, SERVICE_ID); } @Test void sameExpiredRequestIsIdempotent() { state.setLifecycleStatus("EXPIRED"); state.setEffectiveTo(EFFECTIVE_TO); DmsFileLifecycleUpdateVO result = service.updateExpired(form, SERVICE_ID); assertEquals("EXPIRED", result.getLifecycleStatus()); verify(projection).apply(TENANT, VERSION, EFFECTIVE_TO, SERVICE_ID); verify(states, never()).expire(TENANT, VERSION, "到期检查确认", SERVICE_ID); } @Test void rejectsRetryWithDifferentLifecycleTimes() { state.setLifecycleStatus("EXPIRED"); state.setEffectiveTo(new Date(EFFECTIVE_TO.getTime() - 1000L)); assertThrows(DataException.class, () -> service.updateExpired(form, SERVICE_ID)); verify(states, never()).expire(TENANT, VERSION, "到期检查确认", SERVICE_ID); } @Test void databaseControlsDueBoundaryAndPreventsProjectionWhenUpdateLosesRace() { when(states.expire(TENANT, VERSION, "到期检查确认", SERVICE_ID)).thenReturn(0); assertThrows(DataException.class, () -> service.updateExpired(form, SERVICE_ID)); verifyNoInteractions(projection); } @Test void rejectsUnlimitedVersions() { state.setEffectiveTo(null); assertThrows(DataException.class, () -> service.updateExpired(form, SERVICE_ID)); verifyNoInteractions(projection); } @Test void rejectsNonEffectiveLifecycleState() { state.setLifecycleStatus("SUPERSEDED"); assertThrows(DataException.class, () -> service.updateExpired(form, SERVICE_ID)); verify(states, never()).expire(TENANT, VERSION, "到期检查确认", SERVICE_ID); } @Test void acceptsMinimalCommandAndNeverOverwritesStartFromLegacyCaller() { form.setEffectiveFrom(new Date(0)); form.setEffectiveTo(null); assertEquals(EFFECTIVE_FROM, service.updateExpired(form, SERVICE_ID).getEffectiveFrom()); verify(projection).apply(TENANT, VERSION, EFFECTIVE_TO, SERVICE_ID); } }