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<Void> 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;
|
}
|
}
|