Skip to content

Commit dd213d5

Browse files
committed
test(schema): add Testcontainers schema validation for MariaDB and PostgreSQL (#286)
Validate that Hibernate can create the full schema without errors on real MariaDB and PostgreSQL instances via Testcontainers. Tests verify: - All 10 expected tables (entities + join tables) are created - WebAuthn byte[] columns map to BLOB-compatible types (longblob on MariaDB, bytea on PostgreSQL) rather than inline VARBINARY Adds testcontainers-postgresql and testcontainers-junit-jupiter deps.
1 parent d0142c7 commit dd213d5

4 files changed

Lines changed: 189 additions & 0 deletions

File tree

build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,9 @@ dependencies {
9090

9191
// Additional test dependencies for improved testing
9292
testImplementation 'org.testcontainers:testcontainers:2.0.3'
93+
testImplementation 'org.testcontainers:testcontainers-junit-jupiter:2.0.3'
9394
testImplementation 'org.testcontainers:testcontainers-mariadb:2.0.3'
95+
testImplementation 'org.testcontainers:testcontainers-postgresql:2.0.3'
9496
testImplementation 'com.github.tomakehurst:wiremock:3.0.1'
9597
testImplementation 'com.tngtech.archunit:archunit-junit5:1.4.1'
9698
testImplementation 'org.assertj:assertj-core:3.27.7'
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package com.digitalsanctuary.spring.user.persistence.schema;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import com.digitalsanctuary.spring.user.test.app.TestApplication;
5+
import java.util.List;
6+
import java.util.Set;
7+
import org.junit.jupiter.api.DisplayName;
8+
import org.junit.jupiter.api.Test;
9+
import org.springframework.beans.factory.annotation.Autowired;
10+
import org.springframework.boot.test.context.SpringBootTest;
11+
import org.springframework.jdbc.core.JdbcTemplate;
12+
13+
/**
14+
* Abstract base class for database schema validation tests. Subclasses provide a real database via Testcontainers and
15+
* configure Spring to connect to it. This test verifies that Hibernate can create the full schema without errors on each
16+
* target database.
17+
*
18+
* <p>
19+
* The test uses {@code ddl-auto: create} (via Spring Boot properties) and then queries
20+
* {@code INFORMATION_SCHEMA.TABLES} to verify all expected tables were created. This catches silent DDL failures like
21+
* the one described in GitHub issue #286.
22+
* </p>
23+
*/
24+
@SpringBootTest(classes = TestApplication.class)
25+
abstract class AbstractSchemaValidationTest {
26+
27+
/**
28+
* All tables expected to be created by Hibernate from the entity model. Includes entity tables and join tables.
29+
*/
30+
private static final Set<String> EXPECTED_TABLES = Set.of(
31+
// Entity tables
32+
"user_account", "role", "privilege", "verification_token", "password_reset_token",
33+
"password_history_entry", "user_entities", "user_credentials",
34+
// Join tables
35+
"users_roles", "roles_privileges");
36+
37+
@Autowired
38+
private JdbcTemplate jdbcTemplate;
39+
40+
@Test
41+
@DisplayName("should create all expected tables without errors")
42+
void shouldCreateAllExpectedTables() {
43+
List<String> tables = jdbcTemplate.queryForList(
44+
"SELECT LOWER(table_name) FROM INFORMATION_SCHEMA.TABLES WHERE LOWER(table_schema) = LOWER(?)",
45+
String.class, getSchemaName());
46+
47+
assertThat(tables)
48+
.as("All entity and join tables should be created by Hibernate on %s", getDatabaseName())
49+
.containsAll(EXPECTED_TABLES);
50+
}
51+
52+
@Test
53+
@DisplayName("should create WebAuthn byte[] columns as BLOB-compatible types (not inline VARBINARY)")
54+
void shouldCreateWebAuthnBlobColumns() {
55+
List<String> blobColumns = List.of("public_key", "attestation_object", "attestation_client_data_json");
56+
57+
for (String column : blobColumns) {
58+
String dataType = jdbcTemplate.queryForObject(
59+
"SELECT LOWER(data_type) FROM INFORMATION_SCHEMA.COLUMNS "
60+
+ "WHERE LOWER(table_schema) = LOWER(?) AND LOWER(table_name) = 'user_credentials' "
61+
+ "AND LOWER(column_name) = ?",
62+
String.class, getSchemaName(), column);
63+
64+
assertThat(dataType)
65+
.as("Column '%s' on %s should be a BLOB-compatible type, not VARBINARY", column, getDatabaseName())
66+
.isIn(getAllowedBlobTypes());
67+
}
68+
}
69+
70+
/**
71+
* Returns the human-readable database name for assertion messages.
72+
*/
73+
protected abstract String getDatabaseName();
74+
75+
/**
76+
* Returns the schema name used in INFORMATION_SCHEMA queries. MariaDB/MySQL uses the database name as schema;
77+
* PostgreSQL uses 'public' by default.
78+
*/
79+
protected abstract String getSchemaName();
80+
81+
/**
82+
* Returns the set of column data types considered acceptable for BLOB columns on this database.
83+
*/
84+
protected abstract Set<String> getAllowedBlobTypes();
85+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package com.digitalsanctuary.spring.user.persistence.schema;
2+
3+
import java.util.Set;
4+
import org.junit.jupiter.api.DisplayName;
5+
import org.springframework.test.context.DynamicPropertyRegistry;
6+
import org.springframework.test.context.DynamicPropertySource;
7+
import org.testcontainers.containers.MariaDBContainer;
8+
import org.testcontainers.junit.jupiter.Container;
9+
import org.testcontainers.junit.jupiter.Testcontainers;
10+
11+
/**
12+
* Validates that Hibernate can create the full schema on MariaDB without errors. This specifically catches the InnoDB
13+
* row-size limit issue described in GitHub issue #286 where VARBINARY(65535) columns caused silent table creation
14+
* failure.
15+
*/
16+
@Testcontainers
17+
@DisplayName("MariaDB Schema Validation Tests")
18+
class MariaDBSchemaValidationTest extends AbstractSchemaValidationTest {
19+
20+
@Container
21+
static final MariaDBContainer<?> MARIADB = new MariaDBContainer<>("mariadb:11.4")
22+
.withDatabaseName("testdb")
23+
.withUsername("test")
24+
.withPassword("test");
25+
26+
@DynamicPropertySource
27+
static void configureProperties(DynamicPropertyRegistry registry) {
28+
registry.add("spring.datasource.url", MARIADB::getJdbcUrl);
29+
registry.add("spring.datasource.username", MARIADB::getUsername);
30+
registry.add("spring.datasource.password", MARIADB::getPassword);
31+
registry.add("spring.datasource.driver-class-name", () -> "org.mariadb.jdbc.Driver");
32+
registry.add("spring.jpa.hibernate.ddl-auto", () -> "create");
33+
registry.add("spring.jpa.database-platform", () -> "org.hibernate.dialect.MariaDBDialect");
34+
registry.add("spring.jpa.properties.hibernate.dialect", () -> "org.hibernate.dialect.MariaDBDialect");
35+
}
36+
37+
@Override
38+
protected String getDatabaseName() {
39+
return "MariaDB";
40+
}
41+
42+
@Override
43+
protected String getSchemaName() {
44+
return "testdb";
45+
}
46+
47+
@Override
48+
protected Set<String> getAllowedBlobTypes() {
49+
return Set.of("longblob", "mediumblob", "blob");
50+
}
51+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package com.digitalsanctuary.spring.user.persistence.schema;
2+
3+
import java.util.Set;
4+
import org.junit.jupiter.api.DisplayName;
5+
import org.springframework.test.context.DynamicPropertyRegistry;
6+
import org.springframework.test.context.DynamicPropertySource;
7+
import org.testcontainers.containers.PostgreSQLContainer;
8+
import org.testcontainers.junit.jupiter.Container;
9+
import org.testcontainers.junit.jupiter.Testcontainers;
10+
11+
/**
12+
* Validates that Hibernate can create the full schema on PostgreSQL without errors. Ensures the byte[] columns map to
13+
* {@code bytea} (not {@code oid}), which would happen if {@code @Lob} were used instead of
14+
* {@code length = Length.LONG32}.
15+
*/
16+
@Testcontainers
17+
@DisplayName("PostgreSQL Schema Validation Tests")
18+
class PostgreSQLSchemaValidationTest extends AbstractSchemaValidationTest {
19+
20+
@Container
21+
static final PostgreSQLContainer<?> POSTGRES = new PostgreSQLContainer<>("postgres:17")
22+
.withDatabaseName("testdb")
23+
.withUsername("test")
24+
.withPassword("test");
25+
26+
@DynamicPropertySource
27+
static void configureProperties(DynamicPropertyRegistry registry) {
28+
registry.add("spring.datasource.url", POSTGRES::getJdbcUrl);
29+
registry.add("spring.datasource.username", POSTGRES::getUsername);
30+
registry.add("spring.datasource.password", POSTGRES::getPassword);
31+
registry.add("spring.datasource.driver-class-name", () -> "org.postgresql.Driver");
32+
registry.add("spring.jpa.hibernate.ddl-auto", () -> "create");
33+
registry.add("spring.jpa.database-platform", () -> "org.hibernate.dialect.PostgreSQLDialect");
34+
registry.add("spring.jpa.properties.hibernate.dialect", () -> "org.hibernate.dialect.PostgreSQLDialect");
35+
}
36+
37+
@Override
38+
protected String getDatabaseName() {
39+
return "PostgreSQL";
40+
}
41+
42+
@Override
43+
protected String getSchemaName() {
44+
return "public";
45+
}
46+
47+
@Override
48+
protected Set<String> getAllowedBlobTypes() {
49+
return Set.of("bytea");
50+
}
51+
}

0 commit comments

Comments
 (0)