刘光辉
4 小时以前 06f314545ac3514d7ce28939920866ae8188d31c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
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;
    }
}