package jnpf.dmsMapper; import jnpf.dmsDatabase.DmsUuidTypeHandler; import jnpf.dmsEntity.storage.approval.DmsDisableRequest; import jnpf.dmsEntity.center.DmsCenterDisableCommand; import jnpf.dmsService.*; import jnpf.dmsService.center.DmsCenterDisableService; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import static org.mockito.Mockito.*; import org.apache.ibatis.builder.xml.XMLMapperBuilder; import org.apache.ibatis.datasource.unpooled.UnpooledDataSource; import org.apache.ibatis.mapping.Environment; import org.apache.ibatis.session.*; import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; import org.junit.jupiter.api.*; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import java.io.StringReader; import java.nio.charset.StandardCharsets; import java.util.*; import static org.junit.jupiter.api.Assertions.*; /** Real production SQL and column types; only connection-local TEMP data, always rolled back. */ @EnabledIfEnvironmentVariable(named="DMS_READ_TEST_URL", matches=".+") class DmsAlreadyIneffectivePostgresTest { SqlSession session; DmsInvalidationMapper mapper; DmsDisableRequest row; Date at; final UUID document=UUID.fromString("10000000-0000-0000-0000-000000000001"); final UUID version=UUID.fromString("20000000-0000-0000-0000-000000000001"); void sql(String text) throws Exception { try(var s=session.getConnection().createStatement()){s.execute(text);} } @BeforeEach void setup() throws Exception { var ds=new UnpooledDataSource("org.postgresql.Driver",System.getenv("DMS_READ_TEST_URL"), System.getenv("DMS_READ_TEST_USER"),System.getenv("DMS_READ_TEST_PASSWORD")); var config=new Configuration(new Environment("ineffective-test",new JdbcTransactionFactory(),ds)); config.setDefaultStatementTimeout(15); config.getTypeHandlerRegistry().register(UUID.class,DmsUuidTypeHandler.class); for(String name:List.of("DmsApprovedDisableMapper","DmsInvalidationMapper","DmsRevocationMapper","DmsDisableApplicationMapper")) { String path="mapper/"+name+".xml"; try(var input=getClass().getResourceAsStream("/"+path)) { String xml=new String(Objects.requireNonNull(input).readAllBytes(),StandardCharsets.UTF_8).replace("public.","pg_temp."); new XMLMapperBuilder(new StringReader(xml),config,path,config.getSqlFragments()).parse(); } } session=new SqlSessionFactoryBuilder().build(config).openSession(false); for(String table:List.of("dms_wenjian_ziyuan","dms_wenjian_banben","dms_documents","dms_file_versions","dms_version_states","dms_approved_file_items","dms_approved_file_batches")) sql("CREATE TEMP TABLE "+table+" ON COMMIT DROP AS SELECT * FROM public."+table+" WITH NO DATA"); sql("INSERT INTO pg_temp.dms_documents(tenant_id,id,access_status) VALUES('test','"+document+"','enabled')"); sql("INSERT INTO pg_temp.dms_version_states(tenant_id,document_id,file_version_id,lifecycle_status,approved_at,effective_from,effective_to) VALUES('test','"+document+"','"+version+"','EFFECTIVE',CURRENT_TIMESTAMP-interval '2 days',CURRENT_TIMESTAMP-interval '1 day',CURRENT_TIMESTAMP)"); sql("INSERT INTO pg_temp.dms_wenjian_ziyuan(f_tenant_id,f_id,dangqian_banben_id,wenjian_zhongxin_wenjian_id,wenjian_zhongxin_banben_id,biz_status) VALUES('test','file','business-version','"+document+"','"+version+"','effective')"); sql("INSERT INTO pg_temp.dms_wenjian_banben(f_tenant_id,f_id,wenjian_id,wenjian_zhongxin_wenjian_id,wenjian_zhongxin_banben_id,biz_status) VALUES('test','business-version','file','"+document+"','"+version+"','effective')"); sql("INSERT INTO pg_temp.dms_file_versions(tenant_id,id,document_id,version_no) VALUES('test','"+version+"','"+document+"',1)"); mapper=session.getMapper(DmsInvalidationMapper.class); row=new DmsDisableRequest();row.setTenantId("test");row.setFileId("file");row.setBusinessVersionId("business-version");row.setDocumentId(document);row.setFileVersionId(version); at=mapper.executionTime(); // SQL timestamps may have sub-millisecond precision; align boundary to the execution timestamp. try(var stmt=session.getConnection().prepareStatement("UPDATE pg_temp.dms_version_states SET effective_to=?")) { stmt.setTimestamp(1,new java.sql.Timestamp(at.getTime()));stmt.executeUpdate(); } } @AfterEach void close() { if(session!=null){try{session.rollback(true);}finally{session.close();}} } @Test void elapsedCurrentVersionSucceedsWithoutChangingLifecycleOrValidity() throws Exception { assertTrue(mapper.alreadyIneffective(row,at)); try(var s=session.getConnection().createStatement();var r=s.executeQuery("SELECT lifecycle_status,effective_to FROM pg_temp.dms_version_states")) { assertTrue(r.next());assertEquals("EFFECTIVE",r.getString(1));assertEquals(at.getTime(),r.getTimestamp(2).getTime()); } assertFalse(mapper.alreadyIneffective(row,new Date(at.getTime()-1))); assertTrue(mapper.alreadyIneffective(row,new Date(at.getTime()+1))); } @Test void absentDeadlineAndFutureDeadlineAreNotIneffective() throws Exception { sql("UPDATE pg_temp.dms_version_states SET effective_to=NULL");assertFalse(mapper.alreadyIneffective(row,at)); sql("UPDATE pg_temp.dms_version_states SET effective_to=CURRENT_TIMESTAMP+interval '1 day'");assertFalse(mapper.alreadyIneffective(row,at)); } @Test void allThreeConsistentTerminalStatesSucceed() throws Exception { for(String[] status:List.of(new String[]{"EXPIRED","expired","yidaoqi"},new String[]{"INVALIDATED","invalidated","yishixiao"},new String[]{"REVOKED","revoked","yichexiao"})) { sql("UPDATE pg_temp.dms_version_states SET lifecycle_status='"+status[0]+"'"); sql("UPDATE pg_temp.dms_wenjian_ziyuan SET biz_status='disabled',jinyong_laiyuan='"+status[2]+"'"); sql("UPDATE pg_temp.dms_wenjian_banben SET biz_status='"+status[1]+"'"); sql("UPDATE pg_temp.dms_documents SET access_status='disable'"); assertTrue(mapper.alreadyIneffective(row,at),status[0]); } } @Test void mismatchedProjectionAndSupersededSourceAreRejected() throws Exception { sql("UPDATE pg_temp.dms_wenjian_banben SET biz_status='expired'");assertNull(mapper.alreadyIneffective(row,at)); sql("UPDATE pg_temp.dms_wenjian_banben SET biz_status='effective'"); sql("UPDATE pg_temp.dms_wenjian_ziyuan SET dangqian_banben_id='new-version'");assertNull(mapper.alreadyIneffective(row,at)); sql("UPDATE pg_temp.dms_wenjian_ziyuan SET dangqian_banben_id='business-version'"); sql("UPDATE pg_temp.dms_version_states SET lifecycle_status='SUPERSEDED'");assertNull(mapper.alreadyIneffective(row,at)); } @Test void foreignTenantMissingDeletedAndAmbiguousTargetsAreRejected() throws Exception { row.setTenantId("other");assertNull(mapper.alreadyIneffective(row,at));row.setTenantId("test"); row.setFileId("absent");assertNull(mapper.alreadyIneffective(row,at));row.setFileId("file"); sql("UPDATE pg_temp.dms_wenjian_ziyuan SET f_delete_mark=1");assertNull(mapper.alreadyIneffective(row,at)); sql("UPDATE pg_temp.dms_wenjian_ziyuan SET f_delete_mark=NULL"); sql("INSERT INTO pg_temp.dms_wenjian_ziyuan SELECT * FROM pg_temp.dms_wenjian_ziyuan");assertNull(mapper.alreadyIneffective(row,at)); } @Test void anotherEffectiveVersionCannotBeMistakenForAnInvalidFile() throws Exception { sql("INSERT INTO pg_temp.dms_version_states(tenant_id,document_id,file_version_id,lifecycle_status) VALUES('test','"+document+"','20000000-0000-0000-0000-000000000002','EFFECTIVE')"); assertNull(mapper.alreadyIneffective(row,at)); } @Test void aNewerRegisteredVersionPreventsNoOpCompletion() throws Exception { sql("INSERT INTO pg_temp.dms_file_versions(tenant_id,id,document_id,version_no) VALUES('test','20000000-0000-0000-0000-000000000002','"+document+"',2)"); assertNull(mapper.alreadyIneffective(row,at)); } @ParameterizedTest @ValueSource(booleans={false,true}) void actualExecutorsCompleteAnElapsedFileAndReplayWithoutRewritingIt(boolean revocation) throws Exception { var command=approvedTask(revocation); var guard=mock(DmsBusinessLifecycleGuard.class); var receipts=List.of(new jnpf.dmsBusinessOccupancy.BusinessFileOccupancy()); when(guard.requireExecution(any(),any(),any(),any(),any())).thenReturn(receipts); assertEquals(200,execute(command,revocation,guard).getCode()); assertEquals("APPLIED",value("SELECT status FROM pg_temp.dms_approved_file_items")); assertEquals("f",value("SELECT active FROM pg_temp.dms_file_operation_occupancies")); assertEquals("EFFECTIVE",value("SELECT lifecycle_status FROM pg_temp.dms_version_states")); assertEquals("effective",value("SELECT biz_status FROM pg_temp.dms_wenjian_ziyuan")); assertEquals("t",value("SELECT "+(revocation?"chexiao":"shixiao")+"_zhixing_shijian IS NOT NULL FROM pg_temp."+(revocation?"dms_wenjian_chexiao_tiyi_zixiang":"dms_wenjian_shixiao_shenqing_zixiang"))); assertEquals(200,execute(command,revocation,guard).getCode()); verify(guard,times(1)).completeExecution(receipts); } @ParameterizedTest @ValueSource(booleans={false,true}) void completionFailureRollsBackAllTaskAndOccupancyWrites(boolean revocation) throws Exception { var command=approvedTask(revocation); var guard=mock(DmsBusinessLifecycleGuard.class); doThrow(new IllegalStateException("release failed")).when(guard).completeExecution(any()); var savepoint=session.getConnection().setSavepoint(); assertThrows(IllegalStateException.class,()->execute(command,revocation,guard)); session.getConnection().rollback(savepoint);session.clearCache(); assertEquals("WAITING",value("SELECT status FROM pg_temp.dms_approved_file_items")); assertEquals("t",value("SELECT active FROM pg_temp.dms_file_operation_occupancies")); assertEquals("t",value("SELECT "+(revocation?"chexiao":"shixiao")+"_zhixing_shijian IS NULL FROM pg_temp."+(revocation?"dms_wenjian_chexiao_tiyi_zixiang":"dms_wenjian_shixiao_shenqing_zixiang"))); } @ParameterizedTest @ValueSource(booleans={false,true}) void pendingVersionStillBlocksCompletion(boolean revocation) throws Exception { var command=approvedTask(revocation); sql("INSERT INTO pg_temp.dms_version_states(tenant_id,document_id,file_version_id,lifecycle_status) VALUES('test','"+document+"','20000000-0000-0000-0000-000000000002','PENDING_EFFECTIVE')"); assertThrows(jnpf.exception.DataException.class,()->execute(command,revocation,mock(DmsBusinessLifecycleGuard.class))); assertEquals("WAITING",value("SELECT status FROM pg_temp.dms_approved_file_items")); assertEquals("t",value("SELECT active FROM pg_temp.dms_file_operation_occupancies")); } String value(String query) throws Exception { try(var s=session.getConnection().createStatement();var r=s.executeQuery(query)){assertTrue(r.next());return r.getString(1);} } DmsCenterDisableCommand approvedTask(boolean revocation) throws Exception { String main=revocation?"dms_wenjian_chexiao_tiyi":"dms_wenjian_shixiao_shenqing"; String operation=revocation?"yichexiao":"yishixiao"; UUID batch=UUID.fromString("30000000-0000-0000-0000-000000000001"); UUID request=UUID.fromString("40000000-0000-0000-0000-000000000001"); for(String table:List.of(main,main+"_zixiang","workflow_task","dms_file_operation_occupancies")) sql("CREATE TEMP TABLE "+table+" ON COMMIT DROP AS SELECT * FROM public."+table+" WITH NO DATA"); sql("INSERT INTO pg_temp."+main+"(f_tenant_id,f_id,f_flow_task_id,f_flow_id,f_flow_state) VALUES('test','app','task','flow',2)"); sql("INSERT INTO pg_temp."+main+"_zixiang(f_tenant_id,f_id,f_foreign_id,wenjian_id,wenjian_banben_id) VALUES('test','child','app','file','business-version')"); sql("INSERT INTO pg_temp.workflow_task(f_tenant_id,f_id,f_flow_id,f_status,f_end_time) VALUES('test','task','flow',2,'2020-01-01Z')"); sql("INSERT INTO pg_temp.dms_file_operation_occupancies(tenant_id,task_id,application_id,file_id,folder_id,operation,active) VALUES('test','task','app','file','"+document+"','"+operation+"',true)"); sql("INSERT INTO pg_temp.dms_approved_file_batches(tenant_id,id,task_id,flow_id,audit_id,audit_type,actor_id,approved_at,lease_token,lease_until,status) VALUES('test','"+batch+"','task','flow','app','"+(revocation?"chexiao_shenqing":"shixiao_shenqing")+"','actor','2020-01-01Z','lease',CURRENT_TIMESTAMP+interval '10 minutes','WAITING')"); sql("INSERT INTO pg_temp.dms_approved_file_items(tenant_id,id,batch_id,change_type,source_row_id,file_id,business_version_id,document_id,file_version_id,scheduled_at,status) VALUES('test','"+request+"','"+batch+"','"+(revocation?"REVOKE":"INVALIDATE")+"','child','file','business-version','"+document+"','"+version+"','2020-01-02Z','WAITING')"); sql("UPDATE pg_temp.workflow_task SET f_end_time='2020-01-01Z'::timestamptz"); var command=new DmsCenterDisableCommand();command.setTenantId("test");command.setRequestId(request); command.setMainDocumentId(document);command.setExpectedMainVersionId(version);command.setActorId("actor"); command.setEffectiveAt(Date.from(java.time.Instant.parse("2020-01-02T00:00:00Z")));command.setLeaseToken("lease");command.setReason(operation); return command; } jnpf.base.ActionResult execute(DmsCenterDisableCommand command,boolean revocation,DmsBusinessLifecycleGuard guard) { var revocations=session.getMapper(DmsRevocationMapper.class); var center=new DmsCenterDisableService(mapper,revocations); var resources=mock(DmsFileExpiryProjectionMapper.class);var cleanup=mock(DmsFileExpiryMapper.class); var occupancies=session.getMapper(DmsDisableApplicationMapper.class); var properties=new DmsInvalidationProperties();properties.setEnabled(true);properties.setAllBatches(true); var result=revocation ?new DmsApprovedRevocationExecutor(revocations,center,resources,cleanup,occupancies,guard).execute(command) :new DmsApprovedInvalidationExecutor(mapper,properties,center,resources,cleanup,occupancies,guard).execute(command); verifyNoInteractions(resources,cleanup); return result; } }