JUnit 5 test framework PoC (#29517)

Closes #29516

Signed-off-by: stianst <stianst@gmail.com>
This commit is contained in:
Stian Thorgersen 2024-05-27 15:05:35 +02:00 committed by GitHub
parent 2683c0a7d1
commit 568a5cb678
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 1131 additions and 3 deletions

View file

@ -76,7 +76,7 @@ jobs:
run: | run: |
SEP="" SEP=""
PROJECTS="" 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" PROJECTS="$PROJECTS$SEP$i"
SEP="," SEP=","
done done

View file

@ -1758,6 +1758,13 @@
</modules> </modules>
</profile> </profile>
<profile>
<id>test-poc</id>
<modules>
<module>test-poc</module>
</modules>
</profile>
<!-- Profile to activate EAP8 Adapters Build --> <!-- Profile to activate EAP8 Adapters Build -->
<profile> <profile>
<id>eap8-adapters</id> <id>eap8-adapters</id>

View file

@ -41,7 +41,9 @@ import static org.keycloak.quarkus.runtime.Environment.isImportExportMode;
public class QuarkusKeycloakApplication extends KeycloakApplication { public class QuarkusKeycloakApplication extends KeycloakApplication {
private static final String KEYCLOAK_ADMIN_ENV_VAR = "KEYCLOAK_ADMIN"; 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_ENV_VAR = "KEYCLOAK_ADMIN_PASSWORD";
private static final String KEYCLOAK_ADMIN_PASSWORD_PROP_VAR = "keycloakAdminPassword";
void onStartupEvent(@Observes StartupEvent event) { void onStartupEvent(@Observes StartupEvent event) {
QuarkusPlatform platform = (QuarkusPlatform) Platform.getPlatform(); QuarkusPlatform platform = (QuarkusPlatform) Platform.getPlatform();
@ -69,8 +71,8 @@ public class QuarkusKeycloakApplication extends KeycloakApplication {
} }
private void createAdminUser() { private void createAdminUser() {
String adminUserName = System.getenv(KEYCLOAK_ADMIN_ENV_VAR); String adminUserName = getEnvOrProp(KEYCLOAK_ADMIN_ENV_VAR, KEYCLOAK_ADMIN_PROP_VAR);
String adminPassword = System.getenv(KEYCLOAK_ADMIN_PASSWORD_ENV_VAR); String adminPassword = getEnvOrProp(KEYCLOAK_ADMIN_PASSWORD_ENV_VAR, KEYCLOAK_ADMIN_PASSWORD_PROP_VAR);
if ((adminUserName == null || adminUserName.trim().length() == 0) if ((adminUserName == null || adminUserName.trim().length() == 0)
|| (adminPassword == null || adminPassword.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);
}
} }

66
test-poc/base/pom.xml Executable file
View file

@ -0,0 +1,66 @@
<?xml version="1.0"?>
<!--
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
~ and other contributors as indicated by the @author tags.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>keycloak-test-parent</artifactId>
<groupId>org.keycloak.test</groupId>
<version>999.0.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-tests-base</artifactId>
<name>Keycloak Base Tests</name>
<packaging>jar</packaging>
<description>Example tests to demonstrate new testing framework</description>
<dependencies>
<dependency>
<groupId>org.keycloak.test</groupId>
<artifactId>keycloak-test-junit5-framework</artifactId>
<version>999.0.0-SNAPSHOT</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-suite</artifactId>
<version>1.10.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.logmanager</groupId>
<artifactId>jboss-logmanager</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>
</project>

View file

@ -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<FeatureRepresentation> 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<String> features() {
return Set.of("update-email");
}
}
}

View file

@ -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<RealmRepresentation> realms = adminClient.realms().findAll();
Assertions.assertFalse(realms.isEmpty());
}
}

View file

@ -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<RealmRepresentation> realms = adminClient.realms().findAll();
Assertions.assertFalse(realms.isEmpty());
}
}

View file

@ -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<ClientRepresentation> clients = realmResource.clients().findByClientId("ManagedResourcesTest");
Assertions.assertEquals(1, clients.size());
}
}

View file

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

49
test-poc/framework/pom.xml Executable file
View file

@ -0,0 +1,49 @@
<?xml version="1.0"?>
<!--
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
~ and other contributors as indicated by the @author tags.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>keycloak-test-parent</artifactId>
<groupId>org.keycloak.test</groupId>
<version>999.0.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-test-junit5-framework</artifactId>
<name>Keycloak JUnit 5 testing framework</name>
<packaging>jar</packaging>
<description>PoC JUnit 5 testing framework for Keycloak</description>
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-admin-client</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-junit5</artifactId>
</dependency>
</dependencies>
</project>

View file

@ -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;
}

View file

@ -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()));
}
}

View file

@ -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 {
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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<Keycloak, TestAdminClient> {
@Override
public Class<TestAdminClient> getAnnotationClass() {
return TestAdminClient.class;
}
@Override
public Class<Keycloak> getValueType() {
return Keycloak.class;
}
@Override
public InstanceWrapper<Keycloak, TestAdminClient> getValue(Registry registry, TestAdminClient annotation) {
InstanceWrapper<Keycloak, TestAdminClient> 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<Keycloak, TestAdminClient> a, InstanceWrapper<Keycloak, TestAdminClient> b) {
return true;
}
@Override
public void close(Keycloak keycloak) {
keycloak.close();
}
}

View file

@ -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<T, A extends Annotation> {
private final Supplier<T, A> supplier;
private final A annotation;
private final Set<InstanceWrapper<T, A>> dependencies = new HashSet<>();
private T value;
private final Map<String, Object> notes = new HashMap<>();
public InstanceWrapper(Supplier<T, A> supplier, A annotation) {
this.supplier = supplier;
this.annotation = annotation;
}
public InstanceWrapper(Supplier<T, A> supplier, A annotation, T value) {
this.supplier = supplier;
this.annotation = annotation;
this.value = value;
}
public void setValue(T value) {
this.value = value;
}
public Supplier<T, A> getSupplier() {
return supplier;
}
public T getValue() {
return value;
}
public A getAnnotation() {
return annotation;
}
public Set<InstanceWrapper<T, A>> getDependencies() {
return dependencies;
}
public void registerDependency(InstanceWrapper<T, A> instanceWrapper) {
dependencies.add(instanceWrapper);
}
public void addNote(String key, Object value) {
notes.put(key, value);
}
@SuppressWarnings("unchecked")
public <N> N getNote(String key, Class<N> type) {
return (N) notes.get(key);
}
}

View file

@ -0,0 +1,8 @@
package org.keycloak.test.framework.injection;
public enum LifeCycle {
GLOBAL,
CLASS
}

View file

@ -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<Supplier<?, ?>> suppliers = new LinkedList<>();
private final List<InstanceWrapper<?, ?>> deployedInstances = new LinkedList<>();
private final List<InstanceWrapper<?, ?>> requestedInstances = new LinkedList<>();
public Registry() {
loadSuppliers();
}
public ExtensionContext getCurrentContext() {
return currentContext;
}
public void setCurrentContext(ExtensionContext currentContext) {
this.currentContext = currentContext;
}
public <T> T getDependency(Class<T> 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<Supplier<?, ?>> 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<InstanceWrapper<?, ?>> 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<InstanceWrapper<?, ?>> 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<InstanceWrapper> 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);
}
}

View file

@ -0,0 +1,19 @@
package org.keycloak.test.framework.injection;
import java.lang.annotation.Annotation;
public interface Supplier<T, S extends Annotation> {
Class<S> getAnnotationClass();
Class<T> getValueType();
InstanceWrapper<T, S> getValue(Registry registry, S annotation);
LifeCycle getLifeCycle();
boolean compatible(InstanceWrapper<T, S> a, InstanceWrapper<T, S> b);
default void close(T instance) {}
}

View file

@ -0,0 +1,13 @@
package org.keycloak.test.framework.injection;
public class SupplierHelpers {
public static <T> T getInstance(Class<T> clazz) {
try {
return clazz.getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

View file

@ -0,0 +1,9 @@
package org.keycloak.test.framework.realm;
import org.keycloak.representations.idm.ClientRepresentation;
public interface ClientConfig {
ClientRepresentation getRepresentation();
}

View file

@ -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<ClientResource, TestClient> {
private static final String CLIENT_UUID_KEY = "clientUuid";
@Override
public Class<TestClient> getAnnotationClass() {
return TestClient.class;
}
@Override
public Class<ClientResource> getValueType() {
return ClientResource.class;
}
@Override
public InstanceWrapper<ClientResource, TestClient> getValue(Registry registry, TestClient annotation) {
InstanceWrapper<ClientResource, TestClient> 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<ClientResource, TestClient> a, InstanceWrapper<ClientResource, TestClient> 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();
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -0,0 +1,9 @@
package org.keycloak.test.framework.realm;
import org.keycloak.representations.idm.RealmRepresentation;
public interface RealmConfig {
RealmRepresentation getRepresentation();
}

View file

@ -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<RealmResource, TestRealm> {
private static final String REALM_NAME_KEY = "realmName";
@Override
public Class<TestRealm> getAnnotationClass() {
return TestRealm.class;
}
@Override
public Class<RealmResource> getValueType() {
return RealmResource.class;
}
@Override
public InstanceWrapper<RealmResource, TestRealm> getValue(Registry registry, TestRealm annotation) {
InstanceWrapper<RealmResource, TestRealm> 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<RealmResource, TestRealm> a, InstanceWrapper<RealmResource, TestRealm> 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();
}
}

View file

@ -0,0 +1,5 @@
package org.keycloak.test.framework.server;
public class DefaultKeycloakTestServerConfig implements KeycloakTestServerConfig {
}

View file

@ -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<String> 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";
}
}

View file

@ -0,0 +1,11 @@
package org.keycloak.test.framework.server;
public interface KeycloakTestServer {
void start(KeycloakTestServerConfig serverConfig);
void stop();
String getBaseUrl();
}

View file

@ -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<String, String> options() {
return Collections.emptyMap();
}
default Set<String> features() {
return Collections.emptySet();
}
}

View file

@ -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<KeycloakTestServer, KeycloakIntegrationTest> {
@Override
public Class<KeycloakTestServer> getValueType() {
return KeycloakTestServer.class;
}
@Override
public Class<KeycloakIntegrationTest> getAnnotationClass() {
return KeycloakIntegrationTest.class;
}
@Override
public InstanceWrapper<KeycloakTestServer, KeycloakIntegrationTest> 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<KeycloakTestServer, KeycloakIntegrationTest> a, InstanceWrapper<KeycloakTestServer, KeycloakIntegrationTest> b) {
return a.getAnnotation().config().equals(b.getAnnotation().config());
}
@Override
public void close(KeycloakTestServer remoteKeycloakTestServer) {
remoteKeycloakTestServer.stop();
}
}

View file

@ -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";
}
}

View file

@ -0,0 +1 @@
org.keycloak.test.framework.KeycloakIntegrationTestExtension

View file

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

46
test-poc/pom.xml Executable file
View file

@ -0,0 +1,46 @@
<?xml version="1.0"?>
<!--
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
~ and other contributors as indicated by the @author tags.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>999.0.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-test-parent</artifactId>
<groupId>org.keycloak.test</groupId>
<packaging>pom</packaging>
<name>Keycloak Test Parent</name>
<description>Keycloak Test Parent</description>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<maven.compiler.release>17</maven.compiler.release>
</properties>
<modules>
<module>base</module>
<module>framework</module>
</modules>
</project>