feature/test-liquibase-migration-from-a-prod-dump (#152)
Co-authored-by: Michael Hoennig <michael@hoennig.de> Reviewed-on: https://dev.hostsharing.net/hostsharing/hs.hsadmin.ng/pulls/152 Reviewed-by: Marc Sandlus <marc.sandlus@hostsharing.net>
This commit is contained in:
		| @@ -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 | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								build.gradle
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								build.gradle
									
									
									
									
									
								
							| @@ -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 | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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 $$ | ||||||
|   | |||||||
| @@ -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 $$; | ||||||
|   | |||||||
| @@ -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! | ||||||
|   | |||||||
| @@ -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(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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 | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										17155
									
								
								src/test/resources/db/prod-only-office-schema-with-test-data.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17155
									
								
								src/test/resources/db/prod-only-office-schema-with-test-data.sql
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Reference in New Issue
	
	Block a user