diff --git a/src/test/java/net/hostsharing/hsadminng/hs/migration/LiquibaseCompatibilityIntegrationTest.java b/src/test/java/net/hostsharing/hsadminng/hs/migration/LiquibaseCompatibilityIntegrationTest.java index 64841f6e..12335e90 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/migration/LiquibaseCompatibilityIntegrationTest.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/migration/LiquibaseCompatibilityIntegrationTest.java @@ -1,7 +1,5 @@ package net.hostsharing.hsadminng.hs.migration; -import liquibase.Liquibase; -import lombok.SneakyThrows; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -10,24 +8,9 @@ import org.springframework.context.annotation.Import; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.jdbc.Sql; -import org.testcontainers.containers.JdbcDatabaseContainer; -import org.testcontainers.jdbc.ContainerDatabaseDriver; -import jakarta.persistence.EntityManager; -import jakarta.persistence.PersistenceContext; -import javax.sql.DataSource; -import java.io.BufferedReader; import java.io.File; -import java.io.InputStreamReader; -import java.util.List; -import java.util.Objects; -import java.util.stream.Collectors; -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.apache.commons.io.FileUtils.readFileToString; -import static org.apache.commons.io.FileUtils.write; -import static org.apache.commons.io.FileUtils.writeStringToFile; -import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.BEFORE_TEST_CLASS; // BLOG: Liquibase-migration-test (not before the reference-SQL-dump-generation is simplified) @@ -58,123 +41,28 @@ import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.BEFORE_TE @DirtiesContext @ActiveProfiles("liquibase-migration-test") @Import(LiquibaseConfig.class) -@Sql(value = "/db/prod-only-office-schema-with-test-data.sql", executionPhase = BEFORE_TEST_CLASS) +@Sql(value = "/db/prod-only-office-schema-with-test-data.sql", executionPhase = BEFORE_TEST_CLASS) // reads prod-schema-dump public class LiquibaseCompatibilityIntegrationTest { private static final String EXPECTED_CHANGESET_ONLY_AFTER_NEW_MIGRATION = "hs-global-liquibase-migration-test"; + public static final int EXPECTED_LIQUIBASE_CHANGELOGS_IN_PROD_SCHEMA_DUMP = 287; @Autowired - private DataSource dataSource; - - @Autowired - private Liquibase liquibase; - - @PersistenceContext - private EntityManager em; + private LiquibaseMigration liquibase; @Test void migrationWorksBasedOnAPreviouslyPopulatedSchema() { // check the initial status from the @Sql-annotation - final var initialChangeSetCount = assertProdReferenceStatusAfterRestore(); + final var initialChangeSetCount = liquibase.assertReferenceStatusAfterRestore( + EXPECTED_LIQUIBASE_CHANGELOGS_IN_PROD_SCHEMA_DUMP, EXPECTED_CHANGESET_ONLY_AFTER_NEW_MIGRATION); // run the current migrations and dump the result to the build-directory - runLiquibaseMigrationsWithContexts("only-office", "with-test-data"); - dumpTo(new File("build/db/prod-only-office-schema-with-test-data.sql")); + liquibase.runWithContexts("only-office", "with-test-data"); + PostgresTestcontainer.dumpTo(new File("build/db/prod-only-office-schema-with-test-data.sql")); // then add another migration and assert if it was applied - runLiquibaseMigrationsWithContexts("liquibase-migration-test"); - assertThatCurrentMigrationsGotApplied(initialChangeSetCount); - } - - private int assertProdReferenceStatusAfterRestore() { - final var schemas = singleColumnSqlQuery("SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname='public'"); - assertThat(schemas).containsExactly("databasechangelog", "databasechangeloglock"); - - final var liquibaseScripts1 = singleColumnSqlQuery("SELECT * FROM public.databasechangelog"); - assertThat(liquibaseScripts1).hasSizeGreaterThan(285); - assertThat(liquibaseScripts1).doesNotContain(EXPECTED_CHANGESET_ONLY_AFTER_NEW_MIGRATION); - final var initialChangeSetCount = liquibaseScripts1.size(); - return initialChangeSetCount; - } - - private void assertThatCurrentMigrationsGotApplied(final int initialChangeSetCount) { - final var liquibaseScripts = singleColumnSqlQuery("SELECT id FROM public.databasechangelog"); - assertThat(liquibaseScripts).hasSizeGreaterThan(initialChangeSetCount); - assertThat(liquibaseScripts).contains(EXPECTED_CHANGESET_ONLY_AFTER_NEW_MIGRATION); - } - - @SneakyThrows - private void dumpTo(final File targetFileName) { - makeDir(targetFileName.getParentFile()); - - final var jdbcDatabaseContainer = getJdbcDatabaseContainer(); - - final var sqlDumpFile = new File(targetFileName.getParent(), "." + targetFileName.getName()); - final var pb = new ProcessBuilder( - "pg_dump", "--column-inserts", "--disable-dollar-quoting", - "--host=" + jdbcDatabaseContainer.getHost(), - "--port=" + jdbcDatabaseContainer.getFirstMappedPort(), - "--username=" + jdbcDatabaseContainer.getUsername() , - "--dbname=" + jdbcDatabaseContainer.getDatabaseName(), - "--file=" + sqlDumpFile.getCanonicalPath() - ); - pb.environment().put("PGPASSWORD", jdbcDatabaseContainer.getPassword()); - - final var process = pb.start(); - int exitCode = process.waitFor(); - final var stderr = new BufferedReader(new InputStreamReader(process.getErrorStream())) - .lines().collect(Collectors.joining("\n")); - assertThat(exitCode).describedAs(stderr).isEqualTo(0); - - final var header = """ - -- ================================================================================= - -- Generated reference-SQL-dump (hopefully of latest prod-release). - -- See: net.hostsharing.hsadminng.hs.migration.LiquibaseCompatibilityIntegrationTest - -- --------------------------------------------------------------------------------- - - -- - -- Explicit pre-initialization because we cannot use `pg_dump --create ...` - -- because the database is already created by Testcontainers. - -- - - CREATE ROLE postgres; - CREATE ROLE admin; - CREATE ROLE restricted; - - """; - writeStringToFile(targetFileName, header, UTF_8, false); // false = overwrite - - write(targetFileName, readFileToString(sqlDumpFile, UTF_8), UTF_8, true); - - assertThat(sqlDumpFile.delete()).describedAs(sqlDumpFile + " cannot be deleted"); - } - - private void makeDir(final File dir) { - assertThat(!dir.exists() || dir.isDirectory()).describedAs(dir + " does exist, but is not a directory").isTrue(); - assertThat(dir.isDirectory() || dir.mkdirs()).describedAs(dir + " cannot be created").isTrue(); - } - - @SneakyThrows - private void runLiquibaseMigrationsWithContexts(final String... contexts) { - liquibase.update( - new liquibase.Contexts(contexts), - new liquibase.LabelExpression()); - } - - private List singleColumnSqlQuery(final String sql) { - //noinspection unchecked - final var rows = (List) em.createNativeQuery(sql).getResultList(); - return rows.stream().map(Objects::toString).toList(); - } - - @SneakyThrows - private static JdbcDatabaseContainer getJdbcDatabaseContainer() { - final var getContainerMethod = ContainerDatabaseDriver.class.getDeclaredMethod("getContainer", String.class); - getContainerMethod.setAccessible(true); - - @SuppressWarnings("rawtypes") - final var container = (JdbcDatabaseContainer) getContainerMethod.invoke(null, - "jdbc:tc:postgresql:15.5-bookworm:///liquibaseMigrationTestTC"); - return container; + liquibase.runWithContexts("liquibase-migration-test"); + liquibase.assertThatCurrentMigrationsGotApplied( + initialChangeSetCount, EXPECTED_CHANGESET_ONLY_AFTER_NEW_MIGRATION); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/migration/LiquibaseConfig.java b/src/test/java/net/hostsharing/hsadminng/hs/migration/LiquibaseConfig.java index d788f644..8d8ec6fe 100644 --- a/src/test/java/net/hostsharing/hsadminng/hs/migration/LiquibaseConfig.java +++ b/src/test/java/net/hostsharing/hsadminng/hs/migration/LiquibaseConfig.java @@ -1,28 +1,27 @@ package net.hostsharing.hsadminng.hs.migration; -import liquibase.Liquibase; import liquibase.database.DatabaseFactory; import liquibase.database.jvm.JdbcConnection; -import liquibase.resource.ClassLoaderResourceAccessor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; import javax.sql.DataSource; @Configuration @Profile("liquibase-migration-test") public class LiquibaseConfig { + @PersistenceContext + private EntityManager em; + @Bean - public Liquibase liquibase(DataSource dataSource) throws Exception { + public LiquibaseMigration liquibase(DataSource dataSource) throws Exception { final var connection = dataSource.getConnection(); final var database = DatabaseFactory.getInstance() .findCorrectDatabaseImplementation(new JdbcConnection(connection)); - return new Liquibase( - "db/changelog/db.changelog-master.yaml", // Path to your Liquibase changelog - new ClassLoaderResourceAccessor(), - database - ); + return new LiquibaseMigration(em, "db/changelog/db.changelog-master.yaml", database); } } diff --git a/src/test/java/net/hostsharing/hsadminng/hs/migration/LiquibaseMigration.java b/src/test/java/net/hostsharing/hsadminng/hs/migration/LiquibaseMigration.java new file mode 100644 index 00000000..93a01d47 --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/migration/LiquibaseMigration.java @@ -0,0 +1,55 @@ +package net.hostsharing.hsadminng.hs.migration; + +import liquibase.Liquibase; +import liquibase.database.Database; +import liquibase.resource.ClassLoaderResourceAccessor; +import lombok.SneakyThrows; + +import jakarta.persistence.EntityManager; +import java.util.List; +import java.util.Objects; + +import static org.assertj.core.api.Assertions.assertThat; + +public class LiquibaseMigration extends Liquibase { + + private final EntityManager em; + + public LiquibaseMigration(final EntityManager em, final String changeLogFile, final Database db) { + super(changeLogFile, new ClassLoaderResourceAccessor(), db); + this.em = em; + } + + @SneakyThrows + public void runWithContexts(final String... contexts) { + update( + new liquibase.Contexts(contexts), + new liquibase.LabelExpression()); + } + + public int assertReferenceStatusAfterRestore( + final int minExpectedLiquibaseChangelogs, + final String expectedChangesetOnlyAfterNewMigration) { + final var schemas = singleColumnSqlQuery("SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname='public'"); + assertThat(schemas).containsExactly("databasechangelog", "databasechangeloglock"); + + final var liquibaseScripts = singleColumnSqlQuery("SELECT id FROM public.databasechangelog"); + assertThat(liquibaseScripts).hasSize(minExpectedLiquibaseChangelogs); + assertThat(liquibaseScripts).doesNotContain(expectedChangesetOnlyAfterNewMigration); + return liquibaseScripts.size(); + } + + public void assertThatCurrentMigrationsGotApplied( + final int initialChangeSetCount, + final String expectedChangesetOnlyAfterNewMigration) { + final var liquibaseScripts = singleColumnSqlQuery("SELECT id FROM public.databasechangelog"); + assertThat(liquibaseScripts).hasSizeGreaterThan(initialChangeSetCount); + assertThat(liquibaseScripts).contains(expectedChangesetOnlyAfterNewMigration); + } + + private List singleColumnSqlQuery(final String sql) { + //noinspection unchecked + final var rows = (List) em.createNativeQuery(sql).getResultList(); + return rows.stream().map(Objects::toString).toList(); + } +} diff --git a/src/test/java/net/hostsharing/hsadminng/hs/migration/PostgresTestcontainer.java b/src/test/java/net/hostsharing/hsadminng/hs/migration/PostgresTestcontainer.java new file mode 100644 index 00000000..7990232b --- /dev/null +++ b/src/test/java/net/hostsharing/hsadminng/hs/migration/PostgresTestcontainer.java @@ -0,0 +1,82 @@ +package net.hostsharing.hsadminng.hs.migration; + +import lombok.SneakyThrows; +import org.testcontainers.containers.JdbcDatabaseContainer; +import org.testcontainers.jdbc.ContainerDatabaseDriver; + +import java.io.BufferedReader; +import java.io.File; +import java.io.InputStreamReader; +import java.util.stream.Collectors; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.apache.commons.io.FileUtils.readFileToString; +import static org.apache.commons.io.FileUtils.write; +import static org.apache.commons.io.FileUtils.writeStringToFile; +import static org.assertj.core.api.Assertions.assertThat; + +public class PostgresTestcontainer { + + @SneakyThrows + public static void dumpTo(final File targetFileName) { + makeDir(targetFileName.getParentFile()); + + final var jdbcDatabaseContainer = getJdbcDatabaseContainer(); + + final var sqlDumpFile = new File(targetFileName.getParent(), "." + targetFileName.getName()); + final var pb = new ProcessBuilder( + "pg_dump", "--column-inserts", "--disable-dollar-quoting", + "--host=" + jdbcDatabaseContainer.getHost(), + "--port=" + jdbcDatabaseContainer.getFirstMappedPort(), + "--username=" + jdbcDatabaseContainer.getUsername() , + "--dbname=" + jdbcDatabaseContainer.getDatabaseName(), + "--file=" + sqlDumpFile.getCanonicalPath() + ); + pb.environment().put("PGPASSWORD", jdbcDatabaseContainer.getPassword()); + + final var process = pb.start(); + int exitCode = process.waitFor(); + final var stderr = new BufferedReader(new InputStreamReader(process.getErrorStream())) + .lines().collect(Collectors.joining("\n")); + assertThat(exitCode).describedAs(stderr).isEqualTo(0); + + final var header = """ + -- ================================================================================= + -- Generated reference-SQL-dump (hopefully of latest prod-release). + -- See: net.hostsharing.hsadminng.hs.migration.LiquibaseCompatibilityIntegrationTest + -- --------------------------------------------------------------------------------- + + -- + -- Explicit pre-initialization because we cannot use `pg_dump --create ...` + -- because the database is already created by Testcontainers. + -- + + CREATE ROLE postgres; + CREATE ROLE admin; + CREATE ROLE restricted; + + """; + writeStringToFile(targetFileName, header, UTF_8, false); // false = overwrite + + write(targetFileName, readFileToString(sqlDumpFile, UTF_8), UTF_8, true); + + assertThat(sqlDumpFile.delete()).describedAs(sqlDumpFile + " cannot be deleted"); + } + + private static void makeDir(final File dir) { + assertThat(!dir.exists() || dir.isDirectory()).describedAs(dir + " does exist, but is not a directory").isTrue(); + assertThat(dir.isDirectory() || dir.mkdirs()).describedAs(dir + " cannot be created").isTrue(); + } + + @SneakyThrows + private static JdbcDatabaseContainer getJdbcDatabaseContainer() { + // TODO.test: check if, in the future, there is a better way to access auto-created Testcontainers + final var getContainerMethod = ContainerDatabaseDriver.class.getDeclaredMethod("getContainer", String.class); + getContainerMethod.setAccessible(true); + + @SuppressWarnings("rawtypes") + final var container = (JdbcDatabaseContainer) getContainerMethod.invoke(null, + "jdbc:tc:postgresql:15.5-bookworm:///liquibaseMigrationTestTC"); + return container; + } +}