feature/test-liquibase-migration-from-a-prod-dump (#152)

Co-authored-by: Michael Hoennig <michael@hoennig.de>
Reviewed-on: #152
Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
This commit is contained in:
Michael Hoennig 2025-01-28 12:27:54 +01:00
parent 2a61686918
commit ce7e3741bd
8 changed files with 17337 additions and 12 deletions

View File

@ -620,8 +620,8 @@ This way we would get rid of all explicit grants within the same DB-row
and would not need the `rbac.role` table anymore. and would not need the `rbac.role` table anymore.
We would also reduce the depth of the expensive recursive CTE-query. We would also reduce the depth of the expensive recursive CTE-query.
This has to be explored further. This has to be explored further. For now, we just keep it in mind and avoid roles+grants
For now, we just keep it in mind and FIXME which would not fit into a simplified system with a fixed role-type-system.
### The Mapper is Error-Prone ### The Mapper is Error-Prone

View File

@ -335,7 +335,7 @@ jacocoTestCoverageVerification {
} }
} }
// HOWTO: run all unit-tests which don't need a database: gw unitTest // HOWTO: run all unit-tests which don't need a database: gw-test unitTest
tasks.register('unitTest', Test) { tasks.register('unitTest', Test) {
useJUnitPlatform { useJUnitPlatform {
excludeTags 'importOfficeData', 'importHostingAssets', 'scenarioTest', 'generalIntegrationTest', excludeTags 'importOfficeData', 'importHostingAssets', 'scenarioTest', 'generalIntegrationTest',
@ -360,7 +360,7 @@ tasks.register('generalIntegrationTest', Test) {
mustRunAfter spotlessJava mustRunAfter spotlessJava
} }
// HOWTO: run all integration tests of the office module: gw officeIntegrationTest // HOWTO: run all integration tests of the office module: gw-test officeIntegrationTest
tasks.register('officeIntegrationTest', Test) { tasks.register('officeIntegrationTest', Test) {
useJUnitPlatform { useJUnitPlatform {
includeTags 'officeIntegrationTest' includeTags 'officeIntegrationTest'
@ -372,26 +372,26 @@ tasks.register('officeIntegrationTest', Test) {
mustRunAfter spotlessJava mustRunAfter spotlessJava
} }
// HOWTO: run all integration tests of the booking module: gw bookingIntegrationTest // HOWTO: run all integration tests of the booking module: gw-test bookingIntegrationTest
tasks.register('bookingIntegrationTest', Test) { tasks.register('bookingIntegrationTest', Test) {
useJUnitPlatform { useJUnitPlatform {
includeTags 'bookingIntegrationTest' includeTags 'bookingIntegrationTest'
} }
group 'verification' group 'verification'
description 'runs integration tests of the office module' description 'runs integration tests of the booking module'
mustRunAfter spotlessJava mustRunAfter spotlessJava
} }
// HOWTO: run all integration tests of the hosting module: gw hostingIntegrationTest // HOWTO: run all integration tests of the hosting module: gw-test hostingIntegrationTest
tasks.register('hostingIntegrationTest', Test) { tasks.register('hostingIntegrationTest', Test) {
useJUnitPlatform { useJUnitPlatform {
includeTags 'hostingIntegrationTest' includeTags 'hostingIntegrationTest'
} }
group 'verification' group 'verification'
description 'runs integration tests of the office module' description 'runs integration tests of the hosting module'
mustRunAfter spotlessJava mustRunAfter spotlessJava
} }

View File

@ -3,7 +3,7 @@
-- ============================================================================ -- ============================================================================
-- NUMERIC-HASH-FUNCTIONS -- NUMERIC-HASH-FUNCTIONS
--changeset michael.hoennig:hash endDelimiter:--// --changeset michael.hoennig:hash runOnChange:true validCheckSum:ANY endDelimiter:--//
-- ---------------------------------------------------------------------------- -- ----------------------------------------------------------------------------
do $$ do $$

View File

@ -870,18 +870,23 @@ $$;
-- ============================================================================ -- ============================================================================
--changeset michael.hoennig:rbac-base-PGSQL-ROLES context:!external-db endDelimiter:--// --changeset michael.hoennig:rbac-base-PGSQL-ROLES runOnChange:true validCheckSum:ANY context:!external-db endDelimiter:--//
-- ---------------------------------------------------------------------------- -- ----------------------------------------------------------------------------
do $$ do $$
begin begin
if '${HSADMINNG_POSTGRES_ADMIN_USERNAME}'='admin' then if '${HSADMINNG_POSTGRES_ADMIN_USERNAME}'='admin' then
if not exists (select from pg_catalog.pg_roles where rolname = 'admin') then
create role admin; create role admin;
end if;
grant all privileges on all tables in schema public to admin; grant all privileges on all tables in schema public to admin;
end if; end if;
if '${HSADMINNG_POSTGRES_RESTRICTED_USERNAME}'='restricted' then if '${HSADMINNG_POSTGRES_RESTRICTED_USERNAME}'='restricted' then
if not exists (select from pg_catalog.pg_roles where rolname = 'restricted') then
create role restricted; create role restricted;
end if;
grant all privileges on all tables in schema public to restricted; grant all privileges on all tables in schema public to restricted;
end if; end if;
end $$; end $$;

View File

@ -17,6 +17,7 @@ import static net.hostsharing.hsadminng.config.HttpHeadersBuilder.headers;
import static org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric; import static org.apache.commons.lang3.RandomStringUtils.randomAlphanumeric;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static com.github.tomakehurst.wiremock.client.WireMock.*; import static com.github.tomakehurst.wiremock.client.WireMock.*;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestPropertySource(properties = {"server.port=0", "hsadminng.cas.server=http://localhost:8088/cas"}) @TestPropertySource(properties = {"server.port=0", "hsadminng.cas.server=http://localhost:8088/cas"})
@ActiveProfiles("wiremock") // IMPORTANT: To test prod config, do not use test profile! @ActiveProfiles("wiremock") // IMPORTANT: To test prod config, do not use test profile!

View File

@ -0,0 +1,136 @@
package net.hostsharing.hsadminng.hs.migration;
import liquibase.Liquibase;
import liquibase.exception.LiquibaseException;
import net.hostsharing.hsadminng.context.Context;
import net.hostsharing.hsadminng.rbac.test.JpaAttempt;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
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 javax.sql.DataSource;
import java.util.List;
import java.util.Objects;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.BEFORE_TEST_CLASS;
// TODO.impl: The reference-SQL-dump-generation needs to be automated
// BLOG: Liquibase-migration-test (not before the reference-SQL-dump-generation is simplified)
// HOWTO: generate the prod-reference-SQL-dump during a prod-release
/**
* Tests, if the Liquibase scripts can be applied to a database ionitialized with schemas
* and test-data from a previous version.
*
* <p>The test needs a dump, ideally from the version of the lastest prod-release:</p>
*
* <ol>
* <li>clean the database:<br/>
* <code>pg-sql-reset</code>
* </li>
*
* <li>restote the database from latest dump</br>
* <pre><code>
* docker exec -i hsadmin-ng-postgres psql -U postgres postgres \
* <src/test/resources/db/prod-only-office-schema-with-test-data.sql
* </code></pre>
* </li>
*
* <li>run the missing migrations:</br>
* <code>gw bootRun --args='--spring.profiles.active=only-office'</code>
* </li>
*
* <li>create the reference-schema SQL-file with some initializations:</li>
* <pre><code>
* cat >src/test/resources/db/prod-only-office-schema-with-test-data.sql <<EOF
* -- =================================================================================
* -- 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;
* GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO admin;
* CREATE ROLE restricted;
* GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO restricted;
*
* EOF
* </code></pre>
* </li>
*
* <li>add the dump to that reference-schema SQL-file:</p>
* <pre><code>docker exec -i hsadmin-ng-postgres /usr/bin/pg_dump \
* --column-inserts --disable-dollar-quoting -U postgres postgres \
* >>src/test/resources/db/prod-only-office-schema-with-test-data.sql
* </code></pre>
* </li>
* </ol>
*
* <p>The generated dump has to be committed to git and will be used in future test-runs
* until it gets replaced at the next release.</p>
*/
@Tag("officeIntegrationTest")
@DataJpaTest(properties = {
"spring.liquibase.enabled=false" // @Sql should go first, Liquibase will be initialized programmatically
})
@DirtiesContext
@ActiveProfiles("liquibase-migration-test")
@Import({ Context.class, JpaAttempt.class, LiquibaseConfig.class })
@Sql(value = "/db/prod-only-office-schema-with-test-data.sql", executionPhase = BEFORE_TEST_CLASS)
public class LiquibaseCompatibilityIntegrationTest extends CsvDataImport {
private static final String EXPECTED_CHANGESET_ONLY_AFTER_NEW_MIGRATION = "hs-hosting-SCHEMA";
private static int initialChangeSetCount = 0;
@Autowired
private DataSource dataSource;
@Autowired
private Liquibase liquibase;
@BeforeEach
public void setup() throws Exception {
assertThatDatabaseIsInitialized();
runLiquibaseMigrations();
}
@Test
void test() {
final var liquibaseScripts = singleColumnSqlQuery("SELECT id FROM public.databasechangelog");
assertThat(liquibaseScripts).hasSizeGreaterThan(initialChangeSetCount);
assertThat(liquibaseScripts).contains(EXPECTED_CHANGESET_ONLY_AFTER_NEW_MIGRATION);
}
private void assertThatDatabaseIsInitialized() {
final var schemas = singleColumnSqlQuery("SELECT tablename FROM pg_catalog.pg_tables WHERE schemaname='public'");
assertThat(schemas).containsExactly("databasechangelog", "databasechangeloglock");
final var liquibaseScripts = singleColumnSqlQuery("SELECT * FROM public.databasechangelog");
assertThat(liquibaseScripts).hasSizeGreaterThan(285);
assertThat(liquibaseScripts).doesNotContain(EXPECTED_CHANGESET_ONLY_AFTER_NEW_MIGRATION);
initialChangeSetCount = liquibaseScripts.size();
}
private void runLiquibaseMigrations() throws LiquibaseException {
liquibase.update(new liquibase.Contexts(), new liquibase.LabelExpression());
}
private List<String> singleColumnSqlQuery(final String sql) {
//noinspection unchecked
final var rows = (List<Object>) em.createNativeQuery(sql).getResultList();
return rows.stream().map(Objects::toString).toList();
}
}

View File

@ -0,0 +1,28 @@
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 javax.sql.DataSource;
@Configuration
@Profile("liquibase-migration-test")
public class LiquibaseConfig {
@Bean
public Liquibase 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
);
}
}

File diff suppressed because it is too large Load Diff