diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a547a2ca44..4ab3755961 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -76,7 +76,7 @@ jobs:
run: |
SEP=""
PROJECTS=""
- for i in `find -name '*Test.java' -type f | egrep -v './(testsuite|quarkus|docs)/' | sed 's|/src/test/java/.*||' | sort | uniq | sed 's|./||'`; do
+ for i in `find -name '*Test.java' -type f | egrep -v './(testsuite|quarkus|docs|test-poc)/' | sed 's|/src/test/java/.*||' | sort | uniq | sed 's|./||'`; do
PROJECTS="$PROJECTS$SEP$i"
SEP=","
done
diff --git a/pom.xml b/pom.xml
index 87e38148f6..835e49d708 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1758,6 +1758,13 @@
+
+ test-poc
+
+ test-poc
+
+
+
eap8-adapters
diff --git a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/QuarkusKeycloakApplication.java b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/QuarkusKeycloakApplication.java
index c77a00f12a..8e59b823f5 100644
--- a/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/QuarkusKeycloakApplication.java
+++ b/quarkus/runtime/src/main/java/org/keycloak/quarkus/runtime/integration/jaxrs/QuarkusKeycloakApplication.java
@@ -41,7 +41,9 @@ import static org.keycloak.quarkus.runtime.Environment.isImportExportMode;
public class QuarkusKeycloakApplication extends KeycloakApplication {
private static final String KEYCLOAK_ADMIN_ENV_VAR = "KEYCLOAK_ADMIN";
+ private static final String KEYCLOAK_ADMIN_PROP_VAR = "keycloakAdmin";
private static final String KEYCLOAK_ADMIN_PASSWORD_ENV_VAR = "KEYCLOAK_ADMIN_PASSWORD";
+ private static final String KEYCLOAK_ADMIN_PASSWORD_PROP_VAR = "keycloakAdminPassword";
void onStartupEvent(@Observes StartupEvent event) {
QuarkusPlatform platform = (QuarkusPlatform) Platform.getPlatform();
@@ -69,8 +71,8 @@ public class QuarkusKeycloakApplication extends KeycloakApplication {
}
private void createAdminUser() {
- String adminUserName = System.getenv(KEYCLOAK_ADMIN_ENV_VAR);
- String adminPassword = System.getenv(KEYCLOAK_ADMIN_PASSWORD_ENV_VAR);
+ String adminUserName = getEnvOrProp(KEYCLOAK_ADMIN_ENV_VAR, KEYCLOAK_ADMIN_PROP_VAR);
+ String adminPassword = getEnvOrProp(KEYCLOAK_ADMIN_PASSWORD_ENV_VAR, KEYCLOAK_ADMIN_PASSWORD_PROP_VAR);
if ((adminUserName == null || adminUserName.trim().length() == 0)
|| (adminPassword == null || adminPassword.trim().length() == 0)) {
@@ -88,4 +90,9 @@ public class QuarkusKeycloakApplication extends KeycloakApplication {
}
}
+ private String getEnvOrProp(String envKey, String propKey) {
+ String value = System.getenv(envKey);
+ return value != null ? value : System.getProperty(propKey);
+ }
+
}
diff --git a/test-poc/base/pom.xml b/test-poc/base/pom.xml
new file mode 100755
index 0000000000..1d05373f4d
--- /dev/null
+++ b/test-poc/base/pom.xml
@@ -0,0 +1,66 @@
+
+
+
+
+
+ keycloak-test-parent
+ org.keycloak.test
+ 999.0.0-SNAPSHOT
+ ../pom.xml
+
+ 4.0.0
+
+ keycloak-tests-base
+ Keycloak Base Tests
+ jar
+ Example tests to demonstrate new testing framework
+
+
+
+ org.keycloak.test
+ keycloak-test-junit5-framework
+ 999.0.0-SNAPSHOT
+ test
+
+
+ org.junit.platform
+ junit-platform-suite
+ 1.10.2
+ test
+
+
+ org.jboss.logmanager
+ jboss-logmanager
+
+
+
+
+
+
+ maven-surefire-plugin
+
+
+ org.jboss.logmanager.LogManager
+
+
+
+
+
+
+
diff --git a/test-poc/base/src/test/java/org/keycloak/test/base/CustomConfigTest.java b/test-poc/base/src/test/java/org/keycloak/test/base/CustomConfigTest.java
new file mode 100644
index 0000000000..9366a2f3e1
--- /dev/null
+++ b/test-poc/base/src/test/java/org/keycloak/test/base/CustomConfigTest.java
@@ -0,0 +1,37 @@
+package org.keycloak.test.base;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.keycloak.admin.client.Keycloak;
+import org.keycloak.common.Profile;
+import org.keycloak.representations.info.FeatureRepresentation;
+import org.keycloak.test.framework.KeycloakIntegrationTest;
+import org.keycloak.test.framework.TestAdminClient;
+import org.keycloak.test.framework.server.KeycloakTestServerConfig;
+
+import java.util.Optional;
+import java.util.Set;
+
+@KeycloakIntegrationTest(config = CustomConfigTest.CustomServerConfig.class)
+public class CustomConfigTest {
+
+ @TestAdminClient
+ Keycloak adminClient;
+
+ @Test
+ public void testUpdateEmailFeatureEnabled() {
+ Optional updateEmailFeature = adminClient.serverInfo().getInfo().getFeatures().stream().filter(f -> f.getName().equals(Profile.Feature.UPDATE_EMAIL.name())).findFirst();
+ Assertions.assertTrue(updateEmailFeature.isPresent());
+ Assertions.assertTrue(updateEmailFeature.get().isEnabled());
+ }
+
+ public static class CustomServerConfig implements KeycloakTestServerConfig {
+
+ @Override
+ public Set features() {
+ return Set.of("update-email");
+ }
+
+ }
+
+}
diff --git a/test-poc/base/src/test/java/org/keycloak/test/base/DefaultConfig1Test.java b/test-poc/base/src/test/java/org/keycloak/test/base/DefaultConfig1Test.java
new file mode 100644
index 0000000000..94bbf71f3d
--- /dev/null
+++ b/test-poc/base/src/test/java/org/keycloak/test/base/DefaultConfig1Test.java
@@ -0,0 +1,24 @@
+package org.keycloak.test.base;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.keycloak.admin.client.Keycloak;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.test.framework.KeycloakIntegrationTest;
+import org.keycloak.test.framework.TestAdminClient;
+
+import java.util.List;
+
+@KeycloakIntegrationTest
+public class DefaultConfig1Test {
+
+ @TestAdminClient
+ Keycloak adminClient;
+
+ @Test
+ public void testAdminClient() {
+ List realms = adminClient.realms().findAll();
+ Assertions.assertFalse(realms.isEmpty());
+ }
+
+}
diff --git a/test-poc/base/src/test/java/org/keycloak/test/base/DefaultConfig2Test.java b/test-poc/base/src/test/java/org/keycloak/test/base/DefaultConfig2Test.java
new file mode 100644
index 0000000000..53bc6ccb6e
--- /dev/null
+++ b/test-poc/base/src/test/java/org/keycloak/test/base/DefaultConfig2Test.java
@@ -0,0 +1,24 @@
+package org.keycloak.test.base;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.keycloak.admin.client.Keycloak;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.test.framework.KeycloakIntegrationTest;
+import org.keycloak.test.framework.TestAdminClient;
+
+import java.util.List;
+
+@KeycloakIntegrationTest
+public class DefaultConfig2Test {
+
+ @TestAdminClient
+ Keycloak adminClient;
+
+ @Test
+ public void testAdminClient() {
+ List realms = adminClient.realms().findAll();
+ Assertions.assertFalse(realms.isEmpty());
+ }
+
+}
diff --git a/test-poc/base/src/test/java/org/keycloak/test/base/ManagedResourcesTest.java b/test-poc/base/src/test/java/org/keycloak/test/base/ManagedResourcesTest.java
new file mode 100644
index 0000000000..3260489557
--- /dev/null
+++ b/test-poc/base/src/test/java/org/keycloak/test/base/ManagedResourcesTest.java
@@ -0,0 +1,36 @@
+package org.keycloak.test.base;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.test.framework.KeycloakIntegrationTest;
+import org.keycloak.test.framework.TestClient;
+import org.keycloak.test.framework.TestRealm;
+
+import java.util.List;
+
+@KeycloakIntegrationTest
+public class ManagedResourcesTest {
+
+ @TestRealm
+ RealmResource realmResource;
+
+ @TestClient
+ ClientResource clientResource;
+
+ @Test
+ public void testCreatedRealm() {
+ Assertions.assertEquals("ManagedResourcesTest", realmResource.toRepresentation().getRealm());
+ }
+
+ @Test
+ public void testCreatedClient() {
+ Assertions.assertEquals("ManagedResourcesTest", clientResource.toRepresentation().getClientId());
+
+ List clients = realmResource.clients().findByClientId("ManagedResourcesTest");
+ Assertions.assertEquals(1, clients.size());
+ }
+
+}
diff --git a/test-poc/base/src/test/resources/logging.properties b/test-poc/base/src/test/resources/logging.properties
new file mode 100644
index 0000000000..19448548bd
--- /dev/null
+++ b/test-poc/base/src/test/resources/logging.properties
@@ -0,0 +1,15 @@
+loggers=org.keycloak.test
+logger.org.keycloak.test.level=TRACE
+
+logger.handlers=CONSOLE
+
+handler.CONSOLE=org.jboss.logmanager.handlers.ConsoleHandler
+handler.CONSOLE.properties=autoFlush
+handler.CONSOLE.level=ERROR
+handler.CONSOLE.autoFlush=true
+handler.CONSOLE.formatter=PATTERN
+
+# The log format pattern for both logs
+formatter.PATTERN=org.jboss.logmanager.formatters.PatternFormatter
+formatter.PATTERN.properties=pattern
+formatter.PATTERN.pattern=%d{HH:mm:ss,SSS} %-5p %t [%c] %m%n
\ No newline at end of file
diff --git a/test-poc/framework/pom.xml b/test-poc/framework/pom.xml
new file mode 100755
index 0000000000..6bf3cd3f79
--- /dev/null
+++ b/test-poc/framework/pom.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+ keycloak-test-parent
+ org.keycloak.test
+ 999.0.0-SNAPSHOT
+ ../pom.xml
+
+ 4.0.0
+
+ keycloak-test-junit5-framework
+ Keycloak JUnit 5 testing framework
+ jar
+ PoC JUnit 5 testing framework for Keycloak
+
+
+
+ org.keycloak
+ keycloak-admin-client
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+
+
+ org.keycloak
+ keycloak-junit5
+
+
+
+
diff --git a/test-poc/framework/src/main/java/org/keycloak/test/framework/KeycloakIntegrationTest.java b/test-poc/framework/src/main/java/org/keycloak/test/framework/KeycloakIntegrationTest.java
new file mode 100644
index 0000000000..f43bca5dde
--- /dev/null
+++ b/test-poc/framework/src/main/java/org/keycloak/test/framework/KeycloakIntegrationTest.java
@@ -0,0 +1,17 @@
+package org.keycloak.test.framework;
+
+import org.keycloak.test.framework.server.DefaultKeycloakTestServerConfig;
+import org.keycloak.test.framework.server.KeycloakTestServerConfig;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface KeycloakIntegrationTest {
+
+ Class extends KeycloakTestServerConfig> config() default DefaultKeycloakTestServerConfig.class;
+
+}
diff --git a/test-poc/framework/src/main/java/org/keycloak/test/framework/KeycloakIntegrationTestExtension.java b/test-poc/framework/src/main/java/org/keycloak/test/framework/KeycloakIntegrationTestExtension.java
new file mode 100644
index 0000000000..b359de5c4d
--- /dev/null
+++ b/test-poc/framework/src/main/java/org/keycloak/test/framework/KeycloakIntegrationTestExtension.java
@@ -0,0 +1,50 @@
+package org.keycloak.test.framework;
+
+import org.junit.jupiter.api.extension.AfterAllCallback;
+import org.junit.jupiter.api.extension.BeforeAllCallback;
+import org.junit.jupiter.api.extension.BeforeEachCallback;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.keycloak.test.framework.injection.Registry;
+
+public class KeycloakIntegrationTestExtension implements BeforeAllCallback, AfterAllCallback, BeforeEachCallback {
+
+ @Override
+ public void beforeAll(ExtensionContext context) {
+ if (isExtensionEnabled(context)) {
+ getRegistry(context).beforeAll(context.getRequiredTestClass());
+ }
+ }
+
+ @Override
+ public void beforeEach(ExtensionContext context) {
+ if (isExtensionEnabled(context)) {
+ getRegistry(context).beforeEach(context.getRequiredTestInstance());
+ }
+ }
+
+ @Override
+ public void afterAll(ExtensionContext context) {
+ if (isExtensionEnabled(context)) {
+ getRegistry(context).afterAll();
+ }
+ }
+
+ private boolean isExtensionEnabled(ExtensionContext context) {
+ return context.getRequiredTestClass().isAnnotationPresent(KeycloakIntegrationTest.class);
+ }
+
+ private Registry getRegistry(ExtensionContext context) {
+ ExtensionContext.Store store = getStore(context);
+ Registry registry = (Registry) store.getOrComputeIfAbsent(Registry.class, r -> new Registry());
+ registry.setCurrentContext(context);
+ return registry;
+ }
+
+ private ExtensionContext.Store getStore(ExtensionContext context) {
+ while (context.getParent().isPresent()) {
+ context = context.getParent().get();
+ }
+ return context.getStore(ExtensionContext.Namespace.create(getClass()));
+ }
+
+}
diff --git a/test-poc/framework/src/main/java/org/keycloak/test/framework/TestAdminClient.java b/test-poc/framework/src/main/java/org/keycloak/test/framework/TestAdminClient.java
new file mode 100644
index 0000000000..556686e157
--- /dev/null
+++ b/test-poc/framework/src/main/java/org/keycloak/test/framework/TestAdminClient.java
@@ -0,0 +1,13 @@
+package org.keycloak.test.framework;
+
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface TestAdminClient {
+
+}
diff --git a/test-poc/framework/src/main/java/org/keycloak/test/framework/TestClient.java b/test-poc/framework/src/main/java/org/keycloak/test/framework/TestClient.java
new file mode 100644
index 0000000000..221f84390d
--- /dev/null
+++ b/test-poc/framework/src/main/java/org/keycloak/test/framework/TestClient.java
@@ -0,0 +1,17 @@
+package org.keycloak.test.framework;
+
+import org.keycloak.test.framework.realm.ClientConfig;
+import org.keycloak.test.framework.realm.DefaultClientConfig;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface TestClient {
+
+ Class extends ClientConfig> config() default DefaultClientConfig.class;
+
+}
diff --git a/test-poc/framework/src/main/java/org/keycloak/test/framework/TestRealm.java b/test-poc/framework/src/main/java/org/keycloak/test/framework/TestRealm.java
new file mode 100644
index 0000000000..c0d41a1b0e
--- /dev/null
+++ b/test-poc/framework/src/main/java/org/keycloak/test/framework/TestRealm.java
@@ -0,0 +1,17 @@
+package org.keycloak.test.framework;
+
+import org.keycloak.test.framework.realm.DefaultRealmConfig;
+import org.keycloak.test.framework.realm.RealmConfig;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface TestRealm {
+
+ Class extends RealmConfig> config() default DefaultRealmConfig.class;
+
+}
diff --git a/test-poc/framework/src/main/java/org/keycloak/test/framework/admin/KeycloakAdminClientSupplier.java b/test-poc/framework/src/main/java/org/keycloak/test/framework/admin/KeycloakAdminClientSupplier.java
new file mode 100644
index 0000000000..f755644f70
--- /dev/null
+++ b/test-poc/framework/src/main/java/org/keycloak/test/framework/admin/KeycloakAdminClientSupplier.java
@@ -0,0 +1,50 @@
+package org.keycloak.test.framework.admin;
+
+import org.keycloak.admin.client.Keycloak;
+import org.keycloak.test.framework.TestAdminClient;
+import org.keycloak.test.framework.injection.InstanceWrapper;
+import org.keycloak.test.framework.injection.LifeCycle;
+import org.keycloak.test.framework.injection.Registry;
+import org.keycloak.test.framework.injection.Supplier;
+import org.keycloak.test.framework.server.KeycloakTestServer;
+
+public class KeycloakAdminClientSupplier implements Supplier {
+
+ @Override
+ public Class getAnnotationClass() {
+ return TestAdminClient.class;
+ }
+
+ @Override
+ public Class getValueType() {
+ return Keycloak.class;
+ }
+
+ @Override
+ public InstanceWrapper getValue(Registry registry, TestAdminClient annotation) {
+ InstanceWrapper wrapper = new InstanceWrapper<>(this, annotation);
+
+ KeycloakTestServer testServer = registry.getDependency(KeycloakTestServer.class, wrapper);
+
+ Keycloak keycloak = Keycloak.getInstance(testServer.getBaseUrl(), "master", "admin", "admin", "admin-cli");
+ wrapper.setValue(keycloak);
+
+ return wrapper;
+ }
+
+ @Override
+ public LifeCycle getLifeCycle() {
+ return LifeCycle.GLOBAL;
+ }
+
+ @Override
+ public boolean compatible(InstanceWrapper a, InstanceWrapper b) {
+ return true;
+ }
+
+ @Override
+ public void close(Keycloak keycloak) {
+ keycloak.close();
+ }
+
+}
diff --git a/test-poc/framework/src/main/java/org/keycloak/test/framework/injection/InstanceWrapper.java b/test-poc/framework/src/main/java/org/keycloak/test/framework/injection/InstanceWrapper.java
new file mode 100644
index 0000000000..05475c2bc1
--- /dev/null
+++ b/test-poc/framework/src/main/java/org/keycloak/test/framework/injection/InstanceWrapper.java
@@ -0,0 +1,61 @@
+package org.keycloak.test.framework.injection;
+
+import java.lang.annotation.Annotation;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+public class InstanceWrapper {
+
+ private final Supplier supplier;
+ private final A annotation;
+ private final Set> dependencies = new HashSet<>();
+ private T value;
+ private final Map notes = new HashMap<>();
+
+ public InstanceWrapper(Supplier supplier, A annotation) {
+ this.supplier = supplier;
+ this.annotation = annotation;
+ }
+
+ public InstanceWrapper(Supplier supplier, A annotation, T value) {
+ this.supplier = supplier;
+ this.annotation = annotation;
+ this.value = value;
+ }
+
+ public void setValue(T value) {
+ this.value = value;
+ }
+
+ public Supplier getSupplier() {
+ return supplier;
+ }
+
+ public T getValue() {
+ return value;
+ }
+
+ public A getAnnotation() {
+ return annotation;
+ }
+
+ public Set> getDependencies() {
+ return dependencies;
+ }
+
+ public void registerDependency(InstanceWrapper instanceWrapper) {
+ dependencies.add(instanceWrapper);
+ }
+
+ public void addNote(String key, Object value) {
+ notes.put(key, value);
+ }
+
+ @SuppressWarnings("unchecked")
+ public N getNote(String key, Class type) {
+ return (N) notes.get(key);
+ }
+
+}
diff --git a/test-poc/framework/src/main/java/org/keycloak/test/framework/injection/LifeCycle.java b/test-poc/framework/src/main/java/org/keycloak/test/framework/injection/LifeCycle.java
new file mode 100644
index 0000000000..7f1b03c09b
--- /dev/null
+++ b/test-poc/framework/src/main/java/org/keycloak/test/framework/injection/LifeCycle.java
@@ -0,0 +1,8 @@
+package org.keycloak.test.framework.injection;
+
+public enum LifeCycle {
+
+ GLOBAL,
+ CLASS
+
+}
diff --git a/test-poc/framework/src/main/java/org/keycloak/test/framework/injection/Registry.java b/test-poc/framework/src/main/java/org/keycloak/test/framework/injection/Registry.java
new file mode 100644
index 0000000000..5a03fe02eb
--- /dev/null
+++ b/test-poc/framework/src/main/java/org/keycloak/test/framework/injection/Registry.java
@@ -0,0 +1,215 @@
+package org.keycloak.test.framework.injection;
+
+import org.jboss.logging.Logger;
+import org.junit.jupiter.api.extension.ExtensionContext;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Field;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Optional;
+import java.util.ServiceLoader;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@SuppressWarnings({"rawtypes", "unchecked"})
+public class Registry {
+
+ private static final Logger LOGGER = Logger.getLogger(Registry.class);
+
+ private ExtensionContext currentContext;
+ private final List> suppliers = new LinkedList<>();
+ private final List> deployedInstances = new LinkedList<>();
+ private final List> requestedInstances = new LinkedList<>();
+
+ public Registry() {
+ loadSuppliers();
+ }
+
+ public ExtensionContext getCurrentContext() {
+ return currentContext;
+ }
+
+ public void setCurrentContext(ExtensionContext currentContext) {
+ this.currentContext = currentContext;
+ }
+
+ public T getDependency(Class typeClass, InstanceWrapper dependent) {
+ InstanceWrapper dependency = getDeployedInstance(typeClass);
+ if (dependency != null) {
+ dependency.registerDependency(dependent);
+
+ if (LOGGER.isTraceEnabled()) {
+ LOGGER.tracev("Injecting existing dependency {0} into {1}",
+ dependency.getSupplier().getClass().getSimpleName(),
+ dependent.getSupplier().getClass().getSimpleName());
+ }
+
+ return (T) dependency.getValue();
+ }
+
+ dependency = getRequestedInstance(typeClass);
+ if (dependency != null) {
+ dependency = dependency.getSupplier().getValue(this, dependency.getAnnotation());
+ dependency.registerDependency(dependent);
+ deployedInstances.add(dependency);
+
+ if (LOGGER.isTraceEnabled()) {
+ LOGGER.tracev("Injecting requested dependency {0} into {1}",
+ dependency.getSupplier().getClass().getSimpleName(),
+ dependent.getSupplier().getClass().getSimpleName());
+ }
+
+ return (T) dependency.getValue();
+ }
+
+ Optional> supplied = suppliers.stream().filter(s -> s.getValueType().equals(typeClass)).findFirst();
+ if (supplied.isPresent()) {
+ Supplier, ?> supplier = supplied.get();
+ dependency = supplier.getValue(this, null);
+ deployedInstances.add(dependency);
+
+ if (LOGGER.isTraceEnabled()) {
+ LOGGER.tracev("Injecting un-configured dependency {0} into {1}",
+ dependency.getSupplier().getClass().getSimpleName(),
+ dependent.getSupplier().getClass().getSimpleName());
+ }
+
+ return (T) dependency.getValue();
+ }
+
+ throw new RuntimeException("Dependency not found: " + typeClass);
+ }
+
+ public void beforeAll(Class testClass) {
+ InstanceWrapper requestedServerInstance = createInstanceWrapper(testClass.getAnnotations());
+ requestedInstances.add(requestedServerInstance);
+
+ for (Field f : testClass.getDeclaredFields()) {
+ InstanceWrapper instanceWrapper = createInstanceWrapper(f.getAnnotations());
+ if (instanceWrapper != null) {
+ requestedInstances.add(instanceWrapper);
+ }
+ }
+
+ if (LOGGER.isTraceEnabled()) {
+ LOGGER.tracev("Requested suppliers: {0}",
+ requestedInstances.stream().map(r -> r.getSupplier().getClass().getSimpleName()).collect(Collectors.joining(", ")));
+ }
+
+ Iterator> itr = requestedInstances.iterator();
+ while (itr.hasNext()) {
+ InstanceWrapper, ?> requestedInstance = itr.next();
+ InstanceWrapper deployedInstance = getDeployedInstance(requestedInstance.getSupplier());
+ if (deployedInstance != null) {
+ if (deployedInstance.getSupplier().compatible(deployedInstance, requestedInstance)) {
+ if (LOGGER.isTraceEnabled()) {
+ LOGGER.tracev("Reusing compatible: {0}",
+ deployedInstance.getSupplier().getClass().getSimpleName());
+ }
+
+ itr.remove();
+ } else {
+ if (LOGGER.isTraceEnabled()) {
+ LOGGER.tracev("Destroying non-compatible: {0}",
+ deployedInstance.getSupplier().getClass().getSimpleName());
+ }
+
+ destroy(deployedInstance);
+ }
+ }
+ }
+
+ itr = requestedInstances.iterator();
+ while (itr.hasNext()) {
+ InstanceWrapper requestedInstance = itr.next();
+
+ InstanceWrapper instance = requestedInstance.getSupplier().getValue(this, requestedInstance.getAnnotation());
+
+ if (LOGGER.isTraceEnabled()) {
+ LOGGER.tracev("Created instance: {0}",
+ requestedInstance.getSupplier().getClass().getSimpleName());
+ }
+
+ deployedInstances.add(instance);
+
+ itr.remove();
+ }
+
+ }
+
+ public void beforeEach(Object testInstance) {
+ for (Field f : testInstance.getClass().getDeclaredFields()) {
+ InstanceWrapper, ?> instance = getDeployedInstance(f.getAnnotations());
+ try {
+ f.setAccessible(true);
+ f.set(testInstance, instance.getValue());
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ public void afterAll() {
+ List> destroy = deployedInstances.stream().filter(i -> i.getSupplier().getLifeCycle().equals(LifeCycle.CLASS)).toList();
+ destroy.forEach(this::destroy);
+ }
+
+ private InstanceWrapper, ?> createInstanceWrapper(Annotation[] annotations) {
+ for (Annotation a : annotations) {
+ for (Supplier s : suppliers) {
+ if (s.getAnnotationClass().equals(a.annotationType())) {
+ return new InstanceWrapper(s, a);
+ }
+ }
+ }
+ return null;
+ }
+
+ private InstanceWrapper, ?> getDeployedInstance(Annotation[] annotations) {
+ for (Annotation a : annotations) {
+ for (InstanceWrapper, ?> i : deployedInstances) {
+ if (i.getSupplier().getAnnotationClass().equals(a.annotationType())) {
+ return i;
+ }
+ }
+ }
+ return null;
+ }
+
+ private void destroy(InstanceWrapper instanceWrapper) {
+ boolean removed = deployedInstances.remove(instanceWrapper);
+ if (removed) {
+ Set dependencies = instanceWrapper.getDependencies();
+ dependencies.forEach(this::destroy);
+ instanceWrapper.getSupplier().close(instanceWrapper.getValue());
+
+ if (LOGGER.isTraceEnabled()) {
+ LOGGER.tracev("Closed instance: {0}",
+ instanceWrapper.getSupplier().getClass().getSimpleName());
+ }
+ }
+ }
+
+ private InstanceWrapper getDeployedInstance(Supplier supplier) {
+ return deployedInstances.stream().filter(i -> i.getSupplier().equals(supplier)).findFirst().orElse(null);
+ }
+
+ private void loadSuppliers() {
+ ServiceLoader.load(Supplier.class).iterator().forEachRemaining(suppliers::add);
+
+ if (LOGGER.isTraceEnabled()) {
+ LOGGER.tracev("Suppliers: {0}", suppliers.stream().map(s -> s.getClass().getSimpleName()).collect(Collectors.joining(", ")));
+ }
+ }
+
+ private InstanceWrapper getDeployedInstance(Class typeClass) {
+ return deployedInstances.stream().filter(i -> i.getSupplier().getValueType().equals(typeClass)).findFirst().orElse(null);
+ }
+
+ private InstanceWrapper getRequestedInstance(Class typeClass) {
+ return requestedInstances.stream().filter(i -> i.getSupplier().getValueType().equals(typeClass)).findFirst().orElse(null);
+ }
+
+}
diff --git a/test-poc/framework/src/main/java/org/keycloak/test/framework/injection/Supplier.java b/test-poc/framework/src/main/java/org/keycloak/test/framework/injection/Supplier.java
new file mode 100644
index 0000000000..d86040f288
--- /dev/null
+++ b/test-poc/framework/src/main/java/org/keycloak/test/framework/injection/Supplier.java
@@ -0,0 +1,19 @@
+package org.keycloak.test.framework.injection;
+
+import java.lang.annotation.Annotation;
+
+public interface Supplier {
+
+ Class getAnnotationClass();
+
+ Class getValueType();
+
+ InstanceWrapper getValue(Registry registry, S annotation);
+
+ LifeCycle getLifeCycle();
+
+ boolean compatible(InstanceWrapper a, InstanceWrapper b);
+
+ default void close(T instance) {}
+
+}
diff --git a/test-poc/framework/src/main/java/org/keycloak/test/framework/injection/SupplierHelpers.java b/test-poc/framework/src/main/java/org/keycloak/test/framework/injection/SupplierHelpers.java
new file mode 100644
index 0000000000..62c9909126
--- /dev/null
+++ b/test-poc/framework/src/main/java/org/keycloak/test/framework/injection/SupplierHelpers.java
@@ -0,0 +1,13 @@
+package org.keycloak.test.framework.injection;
+
+public class SupplierHelpers {
+
+ public static T getInstance(Class clazz) {
+ try {
+ return clazz.getDeclaredConstructor().newInstance();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+}
diff --git a/test-poc/framework/src/main/java/org/keycloak/test/framework/realm/ClientConfig.java b/test-poc/framework/src/main/java/org/keycloak/test/framework/realm/ClientConfig.java
new file mode 100644
index 0000000000..e05eb64a5d
--- /dev/null
+++ b/test-poc/framework/src/main/java/org/keycloak/test/framework/realm/ClientConfig.java
@@ -0,0 +1,9 @@
+package org.keycloak.test.framework.realm;
+
+import org.keycloak.representations.idm.ClientRepresentation;
+
+public interface ClientConfig {
+
+ ClientRepresentation getRepresentation();
+
+}
diff --git a/test-poc/framework/src/main/java/org/keycloak/test/framework/realm/ClientSupplier.java b/test-poc/framework/src/main/java/org/keycloak/test/framework/realm/ClientSupplier.java
new file mode 100644
index 0000000000..ab69f31889
--- /dev/null
+++ b/test-poc/framework/src/main/java/org/keycloak/test/framework/realm/ClientSupplier.java
@@ -0,0 +1,72 @@
+package org.keycloak.test.framework.realm;
+
+import jakarta.ws.rs.core.Response;
+import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.test.framework.TestClient;
+import org.keycloak.test.framework.injection.InstanceWrapper;
+import org.keycloak.test.framework.injection.LifeCycle;
+import org.keycloak.test.framework.injection.Registry;
+import org.keycloak.test.framework.injection.Supplier;
+import org.keycloak.test.framework.injection.SupplierHelpers;
+
+public class ClientSupplier implements Supplier {
+
+ private static final String CLIENT_UUID_KEY = "clientUuid";
+
+ @Override
+ public Class getAnnotationClass() {
+ return TestClient.class;
+ }
+
+ @Override
+ public Class getValueType() {
+ return ClientResource.class;
+ }
+
+ @Override
+ public InstanceWrapper getValue(Registry registry, TestClient annotation) {
+ InstanceWrapper wrapper = new InstanceWrapper<>(this, annotation);
+
+ RealmResource realm = registry.getDependency(RealmResource.class, wrapper);
+
+ ClientConfig config = SupplierHelpers.getInstance(annotation.config());
+ ClientRepresentation clientRepresentation = config.getRepresentation();
+
+ if (clientRepresentation.getClientId() == null) {
+ clientRepresentation.setClientId(registry.getCurrentContext().getRequiredTestClass().getSimpleName());
+ }
+
+ Response response = realm.clients().create(clientRepresentation);
+
+ String path = response.getLocation().getPath();
+ String clientId = path.substring(path.lastIndexOf('/') + 1);
+
+ response.close();
+
+ wrapper.addNote(CLIENT_UUID_KEY, clientId);
+
+ ClientResource clientResource = realm.clients().get(clientId);
+ wrapper.setValue(clientResource);
+
+ return wrapper;
+ }
+
+ @Override
+ public LifeCycle getLifeCycle() {
+ return LifeCycle.CLASS;
+ }
+
+ @Override
+ public boolean compatible(InstanceWrapper a, InstanceWrapper b) {
+ return a.getAnnotation().config().equals(b.getAnnotation().config()) &&
+ a.getNote(CLIENT_UUID_KEY, String.class).equals(b.getNote(CLIENT_UUID_KEY, String.class));
+ }
+
+ @Override
+ public void close(ClientResource client) {
+ client.remove();
+ }
+
+}
diff --git a/test-poc/framework/src/main/java/org/keycloak/test/framework/realm/DefaultClientConfig.java b/test-poc/framework/src/main/java/org/keycloak/test/framework/realm/DefaultClientConfig.java
new file mode 100644
index 0000000000..47eadda3b5
--- /dev/null
+++ b/test-poc/framework/src/main/java/org/keycloak/test/framework/realm/DefaultClientConfig.java
@@ -0,0 +1,12 @@
+package org.keycloak.test.framework.realm;
+
+import org.keycloak.representations.idm.ClientRepresentation;
+
+public class DefaultClientConfig implements ClientConfig {
+
+ @Override
+ public ClientRepresentation getRepresentation() {
+ return new ClientRepresentation();
+ }
+
+}
diff --git a/test-poc/framework/src/main/java/org/keycloak/test/framework/realm/DefaultRealmConfig.java b/test-poc/framework/src/main/java/org/keycloak/test/framework/realm/DefaultRealmConfig.java
new file mode 100644
index 0000000000..153cbbe78f
--- /dev/null
+++ b/test-poc/framework/src/main/java/org/keycloak/test/framework/realm/DefaultRealmConfig.java
@@ -0,0 +1,12 @@
+package org.keycloak.test.framework.realm;
+
+import org.keycloak.representations.idm.RealmRepresentation;
+
+public class DefaultRealmConfig implements RealmConfig {
+
+ @Override
+ public RealmRepresentation getRepresentation() {
+ return new RealmRepresentation();
+ }
+
+}
diff --git a/test-poc/framework/src/main/java/org/keycloak/test/framework/realm/RealmConfig.java b/test-poc/framework/src/main/java/org/keycloak/test/framework/realm/RealmConfig.java
new file mode 100644
index 0000000000..5909b1aa71
--- /dev/null
+++ b/test-poc/framework/src/main/java/org/keycloak/test/framework/realm/RealmConfig.java
@@ -0,0 +1,9 @@
+package org.keycloak.test.framework.realm;
+
+import org.keycloak.representations.idm.RealmRepresentation;
+
+public interface RealmConfig {
+
+ RealmRepresentation getRepresentation();
+
+}
diff --git a/test-poc/framework/src/main/java/org/keycloak/test/framework/realm/RealmSupplier.java b/test-poc/framework/src/main/java/org/keycloak/test/framework/realm/RealmSupplier.java
new file mode 100644
index 0000000000..5ce4e8250c
--- /dev/null
+++ b/test-poc/framework/src/main/java/org/keycloak/test/framework/realm/RealmSupplier.java
@@ -0,0 +1,67 @@
+package org.keycloak.test.framework.realm;
+
+import org.keycloak.admin.client.Keycloak;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.test.framework.TestRealm;
+import org.keycloak.test.framework.injection.InstanceWrapper;
+import org.keycloak.test.framework.injection.LifeCycle;
+import org.keycloak.test.framework.injection.Registry;
+import org.keycloak.test.framework.injection.Supplier;
+import org.keycloak.test.framework.injection.SupplierHelpers;
+
+public class RealmSupplier implements Supplier {
+
+ private static final String REALM_NAME_KEY = "realmName";
+
+ @Override
+ public Class getAnnotationClass() {
+ return TestRealm.class;
+ }
+
+ @Override
+ public Class getValueType() {
+ return RealmResource.class;
+ }
+
+ @Override
+ public InstanceWrapper getValue(Registry registry, TestRealm annotation) {
+ InstanceWrapper wrapper = new InstanceWrapper<>(this, annotation);
+
+ Keycloak adminClient = registry.getDependency(Keycloak.class, wrapper);
+
+ RealmConfig config = SupplierHelpers.getInstance(annotation.config());
+ RealmRepresentation realmRepresentation = config.getRepresentation();
+
+ if (realmRepresentation.getRealm() == null) {
+ realmRepresentation.setRealm(registry.getCurrentContext().getRequiredTestClass().getSimpleName());
+ }
+
+ String realmName = realmRepresentation.getRealm();
+ wrapper.addNote(REALM_NAME_KEY, realmName);
+
+ adminClient.realms().create(realmRepresentation);
+
+ RealmResource realmResource = adminClient.realm(realmRepresentation.getRealm());
+ wrapper.setValue(realmResource);
+
+ return wrapper;
+ }
+
+ @Override
+ public LifeCycle getLifeCycle() {
+ return LifeCycle.CLASS;
+ }
+
+ @Override
+ public boolean compatible(InstanceWrapper a, InstanceWrapper b) {
+ return a.getAnnotation().config().equals(b.getAnnotation().config()) &&
+ a.getNote(REALM_NAME_KEY, String.class).equals(b.getNote(REALM_NAME_KEY, String.class));
+ }
+
+ @Override
+ public void close(RealmResource realm) {
+ realm.remove();
+ }
+
+}
diff --git a/test-poc/framework/src/main/java/org/keycloak/test/framework/server/DefaultKeycloakTestServerConfig.java b/test-poc/framework/src/main/java/org/keycloak/test/framework/server/DefaultKeycloakTestServerConfig.java
new file mode 100644
index 0000000000..82803e882f
--- /dev/null
+++ b/test-poc/framework/src/main/java/org/keycloak/test/framework/server/DefaultKeycloakTestServerConfig.java
@@ -0,0 +1,5 @@
+package org.keycloak.test.framework.server;
+
+public class DefaultKeycloakTestServerConfig implements KeycloakTestServerConfig {
+
+}
diff --git a/test-poc/framework/src/main/java/org/keycloak/test/framework/server/EmbeddedKeycloakTestServer.java b/test-poc/framework/src/main/java/org/keycloak/test/framework/server/EmbeddedKeycloakTestServer.java
new file mode 100644
index 0000000000..3966a552d1
--- /dev/null
+++ b/test-poc/framework/src/main/java/org/keycloak/test/framework/server/EmbeddedKeycloakTestServer.java
@@ -0,0 +1,49 @@
+package org.keycloak.test.framework.server;
+
+import org.keycloak.Keycloak;
+import org.keycloak.common.Version;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+
+public class EmbeddedKeycloakTestServer implements KeycloakTestServer {
+
+ private Keycloak keycloak;
+
+ @Override
+ public void start(KeycloakTestServerConfig serverConfig) {
+ System.setProperty("keycloakAdmin", "admin");
+ System.setProperty("keycloakAdminPassword", "admin");
+
+ List rawOptions = new LinkedList<>();
+ rawOptions.add("start-dev");
+// rawOptions.add("--db=dev-mem"); // TODO With dev-mem there's an issue as the H2 DB isn't stopped when restarting embedded server
+ rawOptions.add("--cache=local");
+
+ if (!serverConfig.features().isEmpty()) {
+ rawOptions.add("--features=" + String.join(",", serverConfig.features()));
+ }
+
+ serverConfig.options().forEach((key, value) -> rawOptions.add("--" + key + "=" + value));
+
+ keycloak = Keycloak.builder()
+ .setVersion(Version.VERSION)
+ .start(rawOptions);
+ }
+
+ @Override
+ public void stop() {
+ try {
+ keycloak.stop();
+ } catch (TimeoutException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public String getBaseUrl() {
+ return "http://localhost:8080";
+ }
+
+}
diff --git a/test-poc/framework/src/main/java/org/keycloak/test/framework/server/KeycloakTestServer.java b/test-poc/framework/src/main/java/org/keycloak/test/framework/server/KeycloakTestServer.java
new file mode 100644
index 0000000000..e0bff45737
--- /dev/null
+++ b/test-poc/framework/src/main/java/org/keycloak/test/framework/server/KeycloakTestServer.java
@@ -0,0 +1,11 @@
+package org.keycloak.test.framework.server;
+
+public interface KeycloakTestServer {
+
+ void start(KeycloakTestServerConfig serverConfig);
+
+ void stop();
+
+ String getBaseUrl();
+
+}
diff --git a/test-poc/framework/src/main/java/org/keycloak/test/framework/server/KeycloakTestServerConfig.java b/test-poc/framework/src/main/java/org/keycloak/test/framework/server/KeycloakTestServerConfig.java
new file mode 100644
index 0000000000..fa87552fb8
--- /dev/null
+++ b/test-poc/framework/src/main/java/org/keycloak/test/framework/server/KeycloakTestServerConfig.java
@@ -0,0 +1,17 @@
+package org.keycloak.test.framework.server;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+public interface KeycloakTestServerConfig {
+
+ default Map options() {
+ return Collections.emptyMap();
+ }
+
+ default Set features() {
+ return Collections.emptySet();
+ }
+
+}
diff --git a/test-poc/framework/src/main/java/org/keycloak/test/framework/server/KeycloakTestServerSupplier.java b/test-poc/framework/src/main/java/org/keycloak/test/framework/server/KeycloakTestServerSupplier.java
new file mode 100644
index 0000000000..d82b2446cd
--- /dev/null
+++ b/test-poc/framework/src/main/java/org/keycloak/test/framework/server/KeycloakTestServerSupplier.java
@@ -0,0 +1,49 @@
+package org.keycloak.test.framework.server;
+
+import org.keycloak.test.framework.KeycloakIntegrationTest;
+import org.keycloak.test.framework.injection.InstanceWrapper;
+import org.keycloak.test.framework.injection.LifeCycle;
+import org.keycloak.test.framework.injection.Registry;
+import org.keycloak.test.framework.injection.Supplier;
+import org.keycloak.test.framework.injection.SupplierHelpers;
+
+public class KeycloakTestServerSupplier implements Supplier {
+
+ @Override
+ public Class getValueType() {
+ return KeycloakTestServer.class;
+ }
+
+ @Override
+ public Class getAnnotationClass() {
+ return KeycloakIntegrationTest.class;
+ }
+
+ @Override
+ public InstanceWrapper getValue(Registry registry, KeycloakIntegrationTest annotation) {
+ KeycloakTestServerConfig serverConfig = SupplierHelpers.getInstance(annotation.config());
+
+// RemoteKeycloakTestServer keycloakTestServer = new RemoteKeycloakTestServer();
+ EmbeddedKeycloakTestServer keycloakTestServer = new EmbeddedKeycloakTestServer();
+
+ keycloakTestServer.start(serverConfig);
+
+ return new InstanceWrapper<>(this, annotation, keycloakTestServer);
+ }
+
+ @Override
+ public LifeCycle getLifeCycle() {
+ return LifeCycle.GLOBAL;
+ }
+
+ @Override
+ public boolean compatible(InstanceWrapper a, InstanceWrapper b) {
+ return a.getAnnotation().config().equals(b.getAnnotation().config());
+ }
+
+ @Override
+ public void close(KeycloakTestServer remoteKeycloakTestServer) {
+ remoteKeycloakTestServer.stop();
+ }
+
+}
diff --git a/test-poc/framework/src/main/java/org/keycloak/test/framework/server/RemoteKeycloakTestServer.java b/test-poc/framework/src/main/java/org/keycloak/test/framework/server/RemoteKeycloakTestServer.java
new file mode 100644
index 0000000000..62d5550227
--- /dev/null
+++ b/test-poc/framework/src/main/java/org/keycloak/test/framework/server/RemoteKeycloakTestServer.java
@@ -0,0 +1,20 @@
+package org.keycloak.test.framework.server;
+
+public class RemoteKeycloakTestServer implements KeycloakTestServer {
+
+ @Override
+ public void start(KeycloakTestServerConfig serverConfig) {
+
+ }
+
+ @Override
+ public void stop() {
+
+ }
+
+ @Override
+ public String getBaseUrl() {
+ return "http://localhost:8080";
+ }
+
+}
diff --git a/test-poc/framework/src/main/resources/META-INF/services/org.junit.jupiter.api.extension.Extension b/test-poc/framework/src/main/resources/META-INF/services/org.junit.jupiter.api.extension.Extension
new file mode 100644
index 0000000000..354b8c3c30
--- /dev/null
+++ b/test-poc/framework/src/main/resources/META-INF/services/org.junit.jupiter.api.extension.Extension
@@ -0,0 +1 @@
+org.keycloak.test.framework.KeycloakIntegrationTestExtension
\ No newline at end of file
diff --git a/test-poc/framework/src/main/resources/META-INF/services/org.keycloak.test.framework.injection.Supplier b/test-poc/framework/src/main/resources/META-INF/services/org.keycloak.test.framework.injection.Supplier
new file mode 100644
index 0000000000..cba6f65800
--- /dev/null
+++ b/test-poc/framework/src/main/resources/META-INF/services/org.keycloak.test.framework.injection.Supplier
@@ -0,0 +1,4 @@
+org.keycloak.test.framework.admin.KeycloakAdminClientSupplier
+org.keycloak.test.framework.server.KeycloakTestServerSupplier
+org.keycloak.test.framework.realm.RealmSupplier
+org.keycloak.test.framework.realm.ClientSupplier
\ No newline at end of file
diff --git a/test-poc/pom.xml b/test-poc/pom.xml
new file mode 100755
index 0000000000..94892e8552
--- /dev/null
+++ b/test-poc/pom.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+ keycloak-parent
+ org.keycloak
+ 999.0.0-SNAPSHOT
+ ../pom.xml
+
+ 4.0.0
+
+ keycloak-test-parent
+ org.keycloak.test
+ pom
+ Keycloak Test Parent
+ Keycloak Test Parent
+
+
+ 17
+ 17
+ 17
+
+
+
+ base
+ framework
+
+
+