diff --git a/README.md b/README.md index c9a660c..2ed9eb5 100644 --- a/README.md +++ b/README.md @@ -472,6 +472,11 @@ zonky.test.database.provider=default # Provider to be used to creat zonky.test.database.refresh=never # Determines the refresh mode of the embedded database. zonky.test.database.replace=any # Determines what type of existing DataSource beans can be replaced. +zonky.test.database.init.script-locations= # Locations of the SQL scripts to apply to the database. +zonky.test.database.init.continue-on-error=false # Whether initialization should continue when an error occurs. +zonky.test.database.init.separator=; # Statement separator in the SQL scripts. +zonky.test.database.init.encoding= # Encoding of the SQL scripts. + zonky.test.database.postgres.client.properties.*= # Additional PostgreSQL options used to configure the test data source. zonky.test.database.mssql.client.properties.*= # Additional MSSQL options used to configure the test data source. zonky.test.database.mysql.client.properties.*= # Additional MySQL options used to configure the test data source. diff --git a/embedded-database-spring-test/src/main/java/io/zonky/test/db/config/EmbeddedDatabaseAutoConfiguration.java b/embedded-database-spring-test/src/main/java/io/zonky/test/db/config/EmbeddedDatabaseAutoConfiguration.java index 3c0372c..a31b385 100644 --- a/embedded-database-spring-test/src/main/java/io/zonky/test/db/config/EmbeddedDatabaseAutoConfiguration.java +++ b/embedded-database-spring-test/src/main/java/io/zonky/test/db/config/EmbeddedDatabaseAutoConfiguration.java @@ -18,6 +18,8 @@ import io.zonky.test.db.flyway.FlywayDatabaseExtension; import io.zonky.test.db.flyway.FlywayPropertiesPostProcessor; +import io.zonky.test.db.init.EmbeddedDatabaseInitializer; +import io.zonky.test.db.init.ScriptDatabasePreparer; import io.zonky.test.db.liquibase.LiquibaseDatabaseExtension; import io.zonky.test.db.liquibase.LiquibasePropertiesPostProcessor; import io.zonky.test.db.provider.DatabaseProvider; @@ -44,6 +46,9 @@ import org.springframework.core.env.Environment; import org.springframework.util.ClassUtils; +import java.nio.charset.Charset; +import java.util.Arrays; + @Configuration public class EmbeddedDatabaseAutoConfiguration implements BeanClassLoaderAware { @@ -274,6 +279,22 @@ public LiquibasePropertiesPostProcessor liquibasePropertiesPostProcessor() { return new LiquibasePropertiesPostProcessor(); } + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + @ConditionalOnMissingBean(name = "embeddedDatabaseInitializer") + public EmbeddedDatabaseInitializer embeddedDatabaseInitializer(Environment environment) { + String[] scriptLocations = environment.getProperty("zonky.test.database.init.script-locations", String[].class); + boolean continueOnError = environment.getProperty("zonky.test.database.init.continue-on-error", boolean.class, false); + String separator = environment.getProperty("zonky.test.database.init.separator", ";"); + Charset encoding = environment.getProperty("zonky.test.database.init.encoding", Charset.class); + + ScriptDatabasePreparer scriptPreparer = null; + if (scriptLocations != null) { + scriptPreparer = new ScriptDatabasePreparer(Arrays.asList(scriptLocations), continueOnError, separator, encoding); + } + return new EmbeddedDatabaseInitializer(scriptPreparer); + } + private void checkDependency(String groupId, String artifactId, String className) { if (!ClassUtils.isPresent(className, classLoader)) { String dependencyName = String.format("%s:%s", groupId, artifactId); diff --git a/embedded-database-spring-test/src/main/java/io/zonky/test/db/init/EmbeddedDatabaseInitializer.java b/embedded-database-spring-test/src/main/java/io/zonky/test/db/init/EmbeddedDatabaseInitializer.java new file mode 100644 index 0000000..f2ba477 --- /dev/null +++ b/embedded-database-spring-test/src/main/java/io/zonky/test/db/init/EmbeddedDatabaseInitializer.java @@ -0,0 +1,50 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.zonky.test.db.init; + +import io.zonky.test.db.context.DatabaseContext; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.core.Ordered; + +public class EmbeddedDatabaseInitializer implements BeanPostProcessor, Ordered { + + private final ScriptDatabasePreparer scriptPreparer; + + public EmbeddedDatabaseInitializer(ScriptDatabasePreparer scriptPreparer) { + this.scriptPreparer = scriptPreparer; + } + + @Override + public int getOrder() { + return Ordered.HIGHEST_PRECEDENCE + 10; + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof DatabaseContext && scriptPreparer != null) { + DatabaseContext databaseContext = (DatabaseContext) bean; + databaseContext.apply(scriptPreparer); + } + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + return bean; + } +} diff --git a/embedded-database-spring-test/src/main/java/io/zonky/test/db/init/ScriptDatabasePreparer.java b/embedded-database-spring-test/src/main/java/io/zonky/test/db/init/ScriptDatabasePreparer.java new file mode 100644 index 0000000..dc87e21 --- /dev/null +++ b/embedded-database-spring-test/src/main/java/io/zonky/test/db/init/ScriptDatabasePreparer.java @@ -0,0 +1,100 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.zonky.test.db.init; + +import com.google.common.base.MoreObjects; +import io.zonky.test.db.preparer.DatabasePreparer; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.jdbc.datasource.init.DatabasePopulatorUtils; +import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; + +import javax.sql.DataSource; +import java.nio.charset.Charset; +import java.sql.SQLException; +import java.util.List; +import java.util.Objects; + +public class ScriptDatabasePreparer implements DatabasePreparer { + + private final List scriptLocations; + private final boolean continueOnError; + private final String separator; + private final Charset encoding; + + public ScriptDatabasePreparer(List scriptLocations) { + this.scriptLocations = scriptLocations; + this.continueOnError = false; + this.separator = ";"; + this.encoding = null; + } + + public ScriptDatabasePreparer(List scriptLocations, boolean continueOnError, String separator, Charset encoding) { + this.scriptLocations = scriptLocations; + this.continueOnError = continueOnError; + this.separator = separator; + this.encoding = encoding; + } + + @Override + public long estimatedDuration() { + return 10; + } + + @Override + public void prepare(DataSource dataSource) throws SQLException { + ResourceDatabasePopulator populator = new ResourceDatabasePopulator(); + populator.setContinueOnError(continueOnError); + populator.setSeparator(separator); + if (encoding != null) { + populator.setSqlScriptEncoding(encoding.name()); + } + + PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); + for (String scriptLocation : scriptLocations) { + Resource resource = resolver.getResource(scriptLocation); + populator.addScript(resource); + } + DatabasePopulatorUtils.execute(populator, dataSource); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ScriptDatabasePreparer that = (ScriptDatabasePreparer) o; + return continueOnError == that.continueOnError + && Objects.equals(scriptLocations, that.scriptLocations) + && Objects.equals(separator, that.separator) + && Objects.equals(encoding, that.encoding); + } + + @Override + public int hashCode() { + return Objects.hash(scriptLocations, continueOnError, separator, encoding); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("scriptLocation", scriptLocations) + .add("continueOnError", continueOnError) + .add("separator", separator) + .add("encoding", encoding) + .toString(); + } +} diff --git a/embedded-database-spring-test/src/main/resources/META-INF/spring-configuration-metadata.json b/embedded-database-spring-test/src/main/resources/META-INF/spring-configuration-metadata.json index c3d0ead..0a57781 100644 --- a/embedded-database-spring-test/src/main/resources/META-INF/spring-configuration-metadata.json +++ b/embedded-database-spring-test/src/main/resources/META-INF/spring-configuration-metadata.json @@ -4,6 +4,10 @@ "name": "zonky.test.database", "description": "Configuration properties for Zonky Embedded Database library." }, + { + "name": "zonky.test.database.init", + "description": "Configuration properties to initialize the database." + }, { "name": "zonky.test.database.prefetching", "description": "Configuration properties to configure prefetching of prepared databases." @@ -70,6 +74,28 @@ "description": "Determines what type of existing DataSource beans can be replaced.", "defaultValue": "any" }, + { + "name": "zonky.test.database.init.script-locations", + "type": "java.util.List", + "description": "Locations of the SQL scripts to apply to the database." + }, + { + "name": "zonky.test.database.init.continue-on-error", + "type": "java.lang.Boolean", + "description": "Whether initialization should continue when an error occurs.", + "defaultValue": false + }, + { + "name": "zonky.test.database.init.separator", + "type": "java.lang.String", + "description": "Statement separator in the SQL scripts.", + "defaultValue": ";" + }, + { + "name": "zonky.test.database.init.encoding", + "type": "java.nio.charset.Charset", + "description": "Encoding of the SQL scripts." + }, { "name": "zonky.test.database.prefetching.thread-name-prefix", "type": "java.lang.String", diff --git a/embedded-database-spring-test/src/test/java/io/zonky/test/db/DatabaseInitializerIntegrationTest.java b/embedded-database-spring-test/src/test/java/io/zonky/test/db/DatabaseInitializerIntegrationTest.java new file mode 100644 index 0000000..081550a --- /dev/null +++ b/embedded-database-spring-test/src/test/java/io/zonky/test/db/DatabaseInitializerIntegrationTest.java @@ -0,0 +1,81 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.zonky.test.db; + +import io.zonky.test.category.SpringTestSuite; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.jdbc.JdbcTestUtils; + +import javax.sql.DataSource; +import java.util.List; +import java.util.Map; + +import static io.zonky.test.db.AutoConfigureEmbeddedDatabase.DatabaseType.POSTGRES; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +@RunWith(SpringRunner.class) +@Category(SpringTestSuite.class) +@AutoConfigureEmbeddedDatabase(type = POSTGRES) +@TestPropertySource(properties = { + "zonky.test.database.init.script-locations=" + + "/db/schema/init-schema.sql," + + "/db/migration/V0001_1__create_person_table.sql," + + "/db/migration/V0002_1__rename_surname_column.sql" +}) +public class DatabaseInitializerIntegrationTest { + + private static final String SQL_SELECT_PERSONS = "select * from test.person"; + + @Autowired + private DataSource dataSource; + + private JdbcTemplate jdbcTemplate; + + @Before + public void setUp() { + jdbcTemplate = new JdbcTemplate(dataSource); + jdbcTemplate.afterPropertiesSet(); + } + + @After + public void tearDown() { + JdbcTestUtils.dropTables(jdbcTemplate, "test.person"); + } + + @Test + public void testScriptLocations() { + assertThat(dataSource).isNotNull(); + + List> persons = jdbcTemplate.queryForList(SQL_SELECT_PERSONS); + assertThat(persons).isNotNull().hasSize(1); + + Map person = persons.get(0); + assertThat(person).containsExactly( + entry("id", 1L), + entry("first_name", "Dave"), + entry("last_name", "Syer")); + } +} diff --git a/embedded-database-spring-test/src/test/resources/db/schema/init-schema.sql b/embedded-database-spring-test/src/test/resources/db/schema/init-schema.sql new file mode 100644 index 0000000..5efb9a8 --- /dev/null +++ b/embedded-database-spring-test/src/test/resources/db/schema/init-schema.sql @@ -0,0 +1 @@ +create schema if not exists test \ No newline at end of file