package com.ruoyi.shadow.config; import com.ruoyi.shadow.startup.ShadowDatabaseIdentity; import com.zaxxer.hikari.HikariDataSource; import org.junit.jupiter.api.Test; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import javax.sql.DataSource; import java.sql.Connection; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import org.springframework.transaction.support.TransactionTemplate; class ShadowDataSourceIsolationTest { @Test void sourcePoolIsReadOnlyBoundedAndFailsFast() { ShadowProperties properties = properties(); ShadowSourceConfiguration configuration = new ShadowSourceConfiguration(); HikariDataSource source = (HikariDataSource) configuration.shadowSourceDataSource(properties); try { assertTrue(source.isReadOnly()); assertEquals(2, source.getMaximumPoolSize()); assertEquals(0, source.getMinimumIdle()); assertEquals(2_000L, source.getConnectionTimeout()); assertEquals("SET TRANSACTION ISOLATION LEVEL READ COMMITTED", source.getConnectionInitSql()); JdbcTemplate jdbc = configuration.shadowSourceJdbc(source); assertEquals(3, jdbc.getQueryTimeout()); DataSourceTransactionManager tx = configuration.shadowSourceTx(source); assertTrue(tx.isEnforceReadOnly()); } finally { source.close(); } } @Test void sourceAndEvidenceUseDifferentPoolsAndTransactionManagers() { ShadowProperties properties = properties(); ShadowSourceConfiguration sourceConfiguration = new ShadowSourceConfiguration(); ShadowEvidenceConfiguration evidenceConfiguration = new ShadowEvidenceConfiguration(); HikariDataSource source = (HikariDataSource) sourceConfiguration.shadowSourceDataSource(properties); HikariDataSource evidence = (HikariDataSource) evidenceConfiguration.shadowEvidenceDataSource(properties); try { assertNotSame(source, evidence); assertTrue(source.isReadOnly()); assertFalse(evidence.isReadOnly()); assertNotSame(sourceConfiguration.shadowSourceTx(source), evidenceConfiguration.shadowEvidenceTx(evidence)); } finally { source.close(); evidence.close(); } } @Test void sourceReadOnlyTransactionDoesNotEmitUnsupportedSqlServerSyntax() throws Exception { DataSource source = mock(DataSource.class); Connection connection = mock(Connection.class); when(source.getConnection()).thenReturn(connection); when(connection.getAutoCommit()).thenReturn(true); DataSourceTransactionManager tx = new ShadowSourceConfiguration().shadowSourceTx(source); TransactionTemplate transaction = new TransactionTemplate(tx); transaction.setReadOnly(true); transaction.execute(status -> null); verify(connection).setReadOnly(true); verify(connection, never()).createStatement(); } @Test void sourceAndEvidenceCannotResolveToTheSamePhysicalDatabase() { ShadowDatabaseIdentity source = ShadowDatabaseIdentity.fromJdbcUrl( "jdbc:sqlserver://prod-db:1433;databaseName=FT546-wcs", "ft546_shadow_ro"); ShadowDatabaseIdentity evidence = ShadowDatabaseIdentity.fromJdbcUrl( "jdbc:sqlserver://PROD-DB:1433;databaseName=ft546-WCS", "ft546_shadow_evidence"); IllegalStateException failure = assertThrows( IllegalStateException.class, () -> ShadowDatabaseIdentity.requirePhysicallyDistinct( source, evidence)); assertEquals("SOURCE_AND_EVIDENCE_DATABASE_MUST_DIFFER", failure.getMessage()); } @Test void distinctDatabasesOnTheSameServerAreAccepted() { ShadowDatabaseIdentity source = ShadowDatabaseIdentity.fromJdbcUrl( "jdbc:sqlserver://prod-db:1433;databaseName=FT546-wcs", "ft546_shadow_ro"); ShadowDatabaseIdentity evidence = ShadowDatabaseIdentity.fromJdbcUrl( "jdbc:sqlserver://prod-db:1433;databaseName=FT546-shadow", "ft546_shadow_evidence"); ShadowDatabaseIdentity.requirePhysicallyDistinct(source, evidence); } @Test void jdbcIdentityRequiresAnExplicitServerName() { IllegalStateException failure = assertThrows( IllegalStateException.class, () -> ShadowDatabaseIdentity.fromJdbcUrl( "jdbc:sqlserver://;databaseName=FT546-wcs", "ft546_shadow_ro")); assertEquals("JDBC_SERVER_NAME_REQUIRED", failure.getMessage()); } private static ShadowProperties properties() { ShadowProperties properties = new ShadowProperties(); properties.getSource().setJdbcUrl( "jdbc:h2:mem:source;DB_CLOSE_DELAY=-1"); properties.getSource().setUsername("source_user"); properties.getSource().setPassword("source_password"); properties.getEvidence().setJdbcUrl( "jdbc:h2:mem:evidence;DB_CLOSE_DELAY=-1"); properties.getEvidence().setUsername("evidence_user"); properties.getEvidence().setPassword("evidence_password"); return properties; } }