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 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 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 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 + + +