Default jdbc-ping cluster setup for distributed caches fails in Oracle

* Add DatabaseConfig to TestDatabase so the underlying DB can be
  configured per test
* Allow DB initScripts to be configured by tests

Closes #40784
Closes #41105

Signed-off-by: Ryan Emerson <remerson@redhat.com>
Signed-off-by: Alexander Schwartz <aschwart@redhat.com>
Co-authored-by: Alexander Schwartz <aschwart@redhat.com>
This commit is contained in:
Ryan Emerson 2025-07-17 16:57:25 +01:00 committed by GitHub
parent 85b494ec51
commit 52a83509dc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 257 additions and 22 deletions

View File

@ -708,6 +708,11 @@ jobs:
- name: Database container port
run: |
# The Ryuk container process exists temporarily after the JVM terminates, wait for only the database container to remain
while [ "$(docker ps -q | wc -l)" -ne 1 ]; do
docker ps
sleep 10
done
DATABASE_PORT=$(docker ps -l --format '{{ .ID }}' | xargs docker port | cut -d ':' -f 2)
echo "DATABASE_PORT=$DATABASE_PORT" >> $GITHUB_ENV

View File

@ -20,6 +20,7 @@ package org.keycloak.connections.jpa.util;
import jakarta.persistence.PersistenceUnitTransactionType;
import jakarta.persistence.ValidationMode;
import org.hibernate.dialect.Dialect;
import org.hibernate.engine.jdbc.env.spi.IdentifierHelper;
import org.hibernate.internal.SessionFactoryImpl;
import org.hibernate.jpa.boot.spi.PersistenceUnitDescriptor;
import org.hibernate.jpa.boot.spi.PersistenceXmlParser;
@ -32,6 +33,7 @@ import org.keycloak.models.KeycloakSession;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
@ -56,9 +58,10 @@ public class JpaUtils {
private static final Logger logger = Logger.getLogger(JpaUtils.class);
public static String getTableNameForNativeQuery(String tableName, EntityManager em) {
String schema = (String) em.getEntityManagerFactory().getProperties().get(HIBERNATE_DEFAULT_SCHEMA);
final Dialect dialect = em.getEntityManagerFactory().unwrap(SessionFactoryImpl.class).getJdbcServices().getDialect();
return (schema==null) ? tableName : dialect.openQuote() + schema + dialect.closeQuote() + "." + tableName;
IdentifierHelper identifierHelper = em.getEntityManagerFactory().unwrap(SessionFactoryImpl.class).getJdbcServices().getJdbcEnvironment().getIdentifierHelper();
String schema = em.getEntityManagerFactory().unwrap(SessionFactoryImpl.class).getSessionFactoryOptions().getDefaultSchema();
return (schema==null) ? tableName : identifierHelper.toIdentifier(schema).render(dialect) + "." + tableName;
}
private static List<ParsedPersistenceXmlDescriptor> transformPersistenceUnits(Collection<PersistenceUnitDescriptor> descriptors) {

View File

@ -349,6 +349,7 @@ Valid values:
| mariadb | MariaDB test container |
| mssql | Microsoft SQL Server test container |
| mysql | MySQL test container |
| oracle | Oracle test container |
| postgres | PostgreSQL test container |
Configuration:

View File

@ -1,5 +1,7 @@
package org.keycloak.testframework.annotations;
import org.keycloak.testframework.database.DatabaseConfigurator;
import org.keycloak.testframework.database.DefaultDatabaseConfigurator;
import org.keycloak.testframework.injection.LifeCycle;
import java.lang.annotation.ElementType;
@ -13,4 +15,5 @@ public @interface InjectTestDatabase {
LifeCycle lifecycle() default LifeCycle.GLOBAL;
Class<? extends DatabaseConfigurator> config() default DefaultDatabaseConfigurator.class;
}

View File

@ -31,7 +31,7 @@ public class Config {
}
public static <T> T getValueTypeConfig(Class<?> valueType, String name, String defaultValue, Class<T> type) {
name = "kc.test." + valueTypeAlias.getAlias(valueType) + "." + name;
name = getValueTypeFQN(valueType, name);
Optional<T> optionalValue = config.getOptionalValue(name, type);
if (optionalValue.isPresent()) {
return optionalValue.get();
@ -42,11 +42,15 @@ public class Config {
}
}
public static <T> T getValueTypeConfig(Class<?> valueType, String name, T defaultValue, Class<T> type) {
name = "kc.test." + valueTypeAlias.getAlias(valueType) + "." + name;
name = getValueTypeFQN(valueType, name);
Optional<T> optionalValue = config.getOptionalValue(name, type);
return optionalValue.orElse(defaultValue);
}
public static String getValueTypeFQN(Class<?> valueType, String name) {
return "kc.test." + valueTypeAlias.getAlias(valueType) + "." + name;
}
public static <T> T get(String name, T defaultValue, Class<T> clazz) {
return config.getOptionalValue(name, clazz).orElse(defaultValue);
}

View File

@ -1,30 +1,39 @@
package org.keycloak.testframework.database;
import org.jboss.logging.Logger;
import org.keycloak.testframework.config.Config;
import org.keycloak.testframework.logging.JBossLogConsumer;
import org.testcontainers.containers.JdbcDatabaseContainer;
import java.io.IOException;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import org.jboss.logging.Logger;
import org.keycloak.testframework.config.Config;
import org.keycloak.testframework.logging.JBossLogConsumer;
import org.testcontainers.containers.JdbcDatabaseContainer;
public abstract class AbstractContainerTestDatabase implements TestDatabase {
protected boolean reuse;
protected JdbcDatabaseContainer<?> container;
protected DatabaseConfig config;
public AbstractContainerTestDatabase() {
reuse = Config.getValueTypeConfig(TestDatabase.class, "reuse", false, Boolean.class);
}
public void start(DatabaseConfig config) {
this.config = config;
String reuseProp = Config.getValueTypeFQN(TestDatabase.class, "reuse");
boolean reuseConfigured = Config.get(reuseProp, false, Boolean.class);
if (config.preventReuse() && reuseConfigured) {
getLogger().warnf("Ignoring '%s' as test explicitly prevents it", reuseProp);
this.reuse = false;
} else {
this.reuse = reuseConfigured;
}
public void start() {
container = createContainer();
container = container.withStartupTimeout(Duration.ofMinutes(10))
.withLogConsumer(new JBossLogConsumer(Logger.getLogger("managed.db." + getDatabaseVendor())))
.withReuse(reuse);
.withReuse(reuse)
.withInitScript(config.initScript());
withDatabaseAndUser(getDatabase(), getUsername(), getPassword());
container.start();
@ -73,7 +82,7 @@ public abstract class AbstractContainerTestDatabase implements TestDatabase {
}
public String getDatabase() {
return "keycloak";
; return config.database() == null ? "keycloak" : config.database();
}
public String getUsername() {
@ -96,5 +105,4 @@ public abstract class AbstractContainerTestDatabase implements TestDatabase {
public abstract String getDatabaseVendor();
public abstract Logger getLogger();
}

View File

@ -6,6 +6,7 @@ import org.keycloak.testframework.injection.InstanceContext;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.injection.RequestedInstance;
import org.keycloak.testframework.injection.Supplier;
import org.keycloak.testframework.injection.SupplierHelpers;
import org.keycloak.testframework.injection.SupplierOrder;
import org.keycloak.testframework.server.KeycloakServer;
import org.keycloak.testframework.server.KeycloakServerConfigBuilder;
@ -15,8 +16,15 @@ public abstract class AbstractDatabaseSupplier implements Supplier<TestDatabase,
@Override
public TestDatabase getValue(InstanceContext<TestDatabase, InjectTestDatabase> instanceContext) {
DatabaseConfigBuilder builder = DatabaseConfigBuilder
.create()
.withPreventReuse(instanceContext.getLifeCycle() != LifeCycle.GLOBAL);
DatabaseConfigurator configurator = SupplierHelpers.getInstance(instanceContext.getAnnotation().config());
configurator.configure(builder);
TestDatabase testDatabase = getTestDatabase();
testDatabase.start();
testDatabase.start(builder.build());
return testDatabase;
}

View File

@ -0,0 +1,4 @@
package org.keycloak.testframework.database;
public record DatabaseConfig(String initScript, String database, boolean preventReuse) {
}

View File

@ -0,0 +1,32 @@
package org.keycloak.testframework.database;
public class DatabaseConfigBuilder {
private String initScript;
private String database;
private boolean preventReuse;
private DatabaseConfigBuilder() {}
public static DatabaseConfigBuilder create() {
return new DatabaseConfigBuilder();
}
public DatabaseConfigBuilder withInitScript(String initScript) {
this.initScript = initScript;
return this;
}
public DatabaseConfigBuilder withDatabase(String database) {
this.database = database;
return this;
}
public DatabaseConfigBuilder withPreventReuse(boolean preventReuse) {
this.preventReuse = preventReuse;
return this;
}
public DatabaseConfig build() {
return new DatabaseConfig(initScript, database, preventReuse);
}
}

View File

@ -0,0 +1,5 @@
package org.keycloak.testframework.database;
public interface DatabaseConfigurator {
DatabaseConfigBuilder configure(DatabaseConfigBuilder builder);
}

View File

@ -0,0 +1,8 @@
package org.keycloak.testframework.database;
public class DefaultDatabaseConfigurator implements DatabaseConfigurator {
@Override
public DatabaseConfigBuilder configure(DatabaseConfigBuilder builder) {
return builder;
}
}

View File

@ -17,7 +17,9 @@ public class DevFileDatabaseSupplier extends AbstractDatabaseSupplier {
private static class DevFileTestDatabase implements TestDatabase {
@Override
public void start() {
public void start(DatabaseConfig config) {
if (config.initScript() != null)
throw new IllegalArgumentException("init script not supported, configure h2 properties via --db-url-properties");
}
@Override

View File

@ -17,7 +17,9 @@ public class DevMemDatabaseSupplier extends AbstractDatabaseSupplier {
private static class DevMemTestDatabase implements TestDatabase {
@Override
public void start() {
public void start(DatabaseConfig config) {
if (config.initScript() != null)
throw new IllegalArgumentException("init script not supported, configure h2 properties via --db-url-properties");
}
@Override

View File

@ -4,10 +4,9 @@ import java.util.Map;
public interface TestDatabase {
void start();
void start(DatabaseConfig databaseConfig);
void stop();
Map<String, String> serverConfig();
}

View File

@ -5,12 +5,14 @@ import org.testcontainers.containers.JdbcDatabaseContainer;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.utility.DockerImageName;
class PostgresTestDatabase extends AbstractContainerTestDatabase {
public class PostgresTestDatabase extends AbstractContainerTestDatabase {
private static final Logger LOGGER = Logger.getLogger(PostgresTestDatabase.class);
public static final String NAME = "postgres";
PostgresTestDatabase() {}
@Override
public JdbcDatabaseContainer<?> createContainer() {
return new PostgreSQLContainer<>(DockerImageName.parse(DatabaseProperties.getContainerImageName(NAME)).asCompatibleSubstituteFor(NAME));

View File

@ -133,6 +133,16 @@
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>test-jar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

View File

@ -0,0 +1,70 @@
package org.keycloak.tests.db;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable;
import org.keycloak.admin.client.resource.RolesResource;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.testframework.annotations.InjectClient;
import org.keycloak.testframework.annotations.InjectTestDatabase;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.config.Config;
import org.keycloak.testframework.database.DatabaseConfigBuilder;
import org.keycloak.testframework.database.PostgresTestDatabase;
import org.keycloak.testframework.database.TestDatabase;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedClient;
import org.keycloak.testframework.server.KeycloakServerConfig;
import org.keycloak.testframework.server.KeycloakServerConfigBuilder;
import org.keycloak.testsuite.util.RoleBuilder;
@DisabledIfEnvironmentVariable(named = "KC_TEST_DATABASE", matches = "mssql", disabledReason = "MSSQL does not support setting the default schema per session")
@KeycloakIntegrationTest(config = CaseSensitiveSchemaTest.KeycloakConfig.class)
public class CaseSensitiveSchemaTest {
@InjectTestDatabase(lifecycle = LifeCycle.CLASS, config = DatabaseConfigurator.class)
TestDatabase db;
@InjectClient
ManagedClient managedClient;
@Test
public void testCaseSensitiveSchema() {
RoleRepresentation role1 = RoleBuilder.create()
.name("role1")
.description("role1-description")
.singleAttribute("role1-attr-key", "role1-attr-val")
.build();
RolesResource roles = managedClient.admin().roles();
roles.create(role1);
roles.deleteRole(role1.getName());
}
protected static String dbType() {
String database = Config.getSelectedSupplier(TestDatabase.class);
return database == null ? "dev-mem" : database;
}
public static class KeycloakConfig implements KeycloakServerConfig {
@Override
public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) {
return switch (dbType()) {
// DBs that convert unquoted to lower-case by default
case PostgresTestDatabase.NAME -> config.option("db-schema", "KEYCLOAK");
// DBs that convert unquoted to upper-case by default
case "dev-file", "dev-mem" -> config.option("db-url-properties", ";INIT=CREATE SCHEMA IF NOT EXISTS keycloak").option("db-schema", "keycloak");
default -> config.option("db-schema", "keycloak");
};
}
}
public static class DatabaseConfigurator implements org.keycloak.testframework.database.DatabaseConfigurator {
@Override
public DatabaseConfigBuilder configure(DatabaseConfigBuilder builder) {
if (PostgresTestDatabase.NAME.equals(dbType())) {
builder.withInitScript("org/keycloak/tests/db/case-sensitive-schema-postgres.sql");
}
return builder;
}
}
}

View File

@ -0,0 +1,42 @@
package org.keycloak.tests.db;
import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable;
import org.keycloak.testframework.annotations.InjectTestDatabase;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.database.DatabaseConfigBuilder;
import org.keycloak.testframework.database.PostgresTestDatabase;
import org.keycloak.testframework.database.TestDatabase;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.server.KeycloakServerConfig;
import org.keycloak.testframework.server.KeycloakServerConfigBuilder;
@DisabledIfEnvironmentVariable(named = "KC_TEST_DATABASE", matches = "mssql", disabledReason = "MSSQL does not support setting the default schema per session")
@DisabledIfEnvironmentVariable(named = "KC_TEST_DATABASE", matches = "oracle", disabledReason = "Oracle image does not support configuring user/databases with '-'")
@KeycloakIntegrationTest(config = PreserveSchemaCaseLiquibaseTest.KeycloakConfig.class)
public class PreserveSchemaCaseLiquibaseTest extends CaseSensitiveSchemaTest {
@InjectTestDatabase(lifecycle = LifeCycle.CLASS, config = DatabaseConfigurator.class)
TestDatabase db;
public static class KeycloakConfig implements KeycloakServerConfig {
@Override
public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) {
switch (dbType()) {
case "dev-file":
case "dev-mem":
config.option("db-url-properties", ";INIT=CREATE SCHEMA IF NOT EXISTS \"keycloak-t\"");
}
return config.option("db-schema", "keycloak-t");
}
}
public static class DatabaseConfigurator implements org.keycloak.testframework.database.DatabaseConfigurator {
@Override
public DatabaseConfigBuilder configure(DatabaseConfigBuilder builder) {
if (dbType().equals(PostgresTestDatabase.NAME)) {
return builder.withInitScript("org/keycloak/tests/db/preserve-schema-case-liquibase-postgres.sql");
}
return builder.withDatabase("keycloak-t");
}
}
}

View File

@ -0,0 +1 @@
CREATE SCHEMA KEYCLOAK;

View File

@ -0,0 +1 @@
CREATE SCHEMA "keycloak-t";

View File

@ -49,6 +49,12 @@
<groupId>org.keycloak.testframework</groupId>
<artifactId>keycloak-test-framework-ui</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak.tests</groupId>
<artifactId>keycloak-tests-base</artifactId>
<version>${project.version}</version>
<type>test-jar</type>
</dependency>
</dependencies>
<build>

View File

@ -0,0 +1,19 @@
package org.keycloak.tests.clustering;
import org.junit.jupiter.api.Test;
import org.keycloak.testframework.annotations.InjectTestDatabase;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.database.TestDatabase;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.tests.db.CaseSensitiveSchemaTest;
@KeycloakIntegrationTest(config = CaseSensitiveSchemaTest.KeycloakConfig.class)
public class JdbcPingCustomSchemaTest {
@InjectTestDatabase(lifecycle = LifeCycle.CLASS, config = CaseSensitiveSchemaTest.DatabaseConfigurator.class)
TestDatabase db;
@Test
public void testClusterFormed() {
// no-op ClusteredKeycloakServer will fail if a cluster is not formed
}
}