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.
We would also reduce the depth of the expensive recursive CTE-query.
This has to be explored further.
For now, we just keep it in mind and FIXME
This has to be explored further. For now, we just keep it in mind and avoid roles+grants
which would not fit into a simplified system with a fixed role-type-system.
### 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) {
useJUnitPlatform {
excludeTags 'importOfficeData', 'importHostingAssets', 'scenarioTest', 'generalIntegrationTest',
@ -360,7 +360,7 @@ tasks.register('generalIntegrationTest', Test) {
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) {
useJUnitPlatform {
includeTags 'officeIntegrationTest'
@ -372,26 +372,26 @@ tasks.register('officeIntegrationTest', Test) {
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) {
useJUnitPlatform {
includeTags 'bookingIntegrationTest'
}
group 'verification'
description 'runs integration tests of the office module'
description 'runs integration tests of the booking module'
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) {
useJUnitPlatform {
includeTags 'hostingIntegrationTest'
}
group 'verification'
description 'runs integration tests of the office module'
description 'runs integration tests of the hosting module'
mustRunAfter spotlessJava
}

View File

@ -3,7 +3,7 @@
-- ============================================================================
-- NUMERIC-HASH-FUNCTIONS
--changeset michael.hoennig:hash endDelimiter:--//
--changeset michael.hoennig:hash runOnChange:true validCheckSum:ANY endDelimiter:--//
-- ----------------------------------------------------------------------------
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 $$
begin
if '${HSADMINNG_POSTGRES_ADMIN_USERNAME}'='admin' then
if not exists (select from pg_catalog.pg_roles where rolname = 'admin') then
create role admin;
end if;
grant all privileges on all tables in schema public to admin;
end if;
if '${HSADMINNG_POSTGRES_RESTRICTED_USERNAME}'='restricted' then
if not exists (select from pg_catalog.pg_roles where rolname = 'restricted') then
create role restricted;
end if;
grant all privileges on all tables in schema public to restricted;
end if;
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.assertj.core.api.Assertions.assertThat;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestPropertySource(properties = {"server.port=0", "hsadminng.cas.server=http://localhost:8088/cas"})
@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