Introduce support for an extension to auto register a value type (#35645)

Closes #35592

Signed-off-by: stianst <stianst@gmail.com>
This commit is contained in:
Stian Thorgersen 2024-12-05 14:50:31 +01:00 committed by GitHub
parent 4c7dea5d70
commit 9ab5575959
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 167 additions and 92 deletions

View File

@ -10,6 +10,10 @@ public interface TestFrameworkExtension {
List<Supplier<?, ?>> suppliers();
default List<Class<?>> alwaysEnabledValueTypes() {
return Collections.emptyList();
}
default Map<Class<?>, String> valueTypeAliases() {
return Collections.emptyMap();
}

View File

@ -1,9 +1,4 @@
package org.keycloak.test.framework.server;
import org.keycloak.test.framework.injection.InstanceContext;
import org.keycloak.test.framework.injection.Registry;
import org.keycloak.test.framework.injection.RequestedInstance;
import org.keycloak.test.framework.injection.Supplier;
package org.keycloak.test.framework.injection;
import java.util.HashMap;
import java.util.LinkedList;
@ -11,13 +6,13 @@ import java.util.List;
public abstract class AbstractInterceptorHelper<I, V> {
private final Registry registry;
private final Class<?> interceptorClass;
private final List<Interception> interceptions = new LinkedList<>();
private final InterceptedBy interceptedBy = new InterceptedBy();
public AbstractInterceptorHelper(Registry registry, Class<I> interceptorClass) {
this.registry = registry;
this.interceptorClass = interceptorClass;
registry.getDeployedInstances().stream().filter(i -> isInterceptor(i.getSupplier())).forEach(i -> interceptions.add(new Interception(i)));
@ -34,6 +29,7 @@ public abstract class AbstractInterceptorHelper<I, V> {
public V intercept(V value, InstanceContext<?, ?> instanceContext) {
for (Interception interception : interceptions) {
value = intercept(value, interception.supplier, interception.existingInstance);
registry.getLogger().logIntercepted(value, interception.supplier);
}
instanceContext.addNote("InterceptedBy", interceptedBy);
return value;

View File

@ -0,0 +1,89 @@
package org.keycloak.test.framework.injection;
import org.keycloak.test.framework.TestFrameworkExtension;
import org.keycloak.test.framework.config.Config;
import java.lang.annotation.Annotation;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.ServiceLoader;
import java.util.Set;
public class Extensions {
private final RegistryLogger logger;
private final ValueTypeAlias valueTypeAlias;
private final List<Supplier<?, ?>> suppliers;
private final List<Class<?>> alwaysEnabledValueTypes;
public Extensions() {
List<TestFrameworkExtension> extensions = loadExtensions();
valueTypeAlias = loadValueTypeAlias(extensions);
logger = new RegistryLogger(valueTypeAlias);
suppliers = loadSuppliers(extensions, valueTypeAlias);
alwaysEnabledValueTypes = loadAlwaysEnabledValueTypes(extensions);
}
public ValueTypeAlias getValueTypeAlias() {
return valueTypeAlias;
}
public List<Supplier<?, ?>> getSuppliers() {
return suppliers;
}
public List<Class<?>> getAlwaysEnabledValueTypes() {
return alwaysEnabledValueTypes;
}
@SuppressWarnings("unchecked")
public <T> Supplier<T, ?> findSupplierByType(Class<T> typeClass) {
return (Supplier<T, ?>) suppliers.stream().filter(s -> s.getValueType().equals(typeClass)).findFirst().orElse(null);
}
@SuppressWarnings("unchecked")
public <T> Supplier<T, ?> findSupplierByAnnotation(Annotation annotation) {
return (Supplier<T, ?>) suppliers.stream().filter(s -> s.getAnnotationClass().equals(annotation.annotationType())).findFirst().orElse(null);
}
private List<TestFrameworkExtension> loadExtensions() {
List<TestFrameworkExtension> extensions = new LinkedList<>();
ServiceLoader.load(TestFrameworkExtension.class).iterator().forEachRemaining(extensions::add);
return extensions;
}
private ValueTypeAlias loadValueTypeAlias(List<TestFrameworkExtension> extensions) {
ValueTypeAlias valueTypeAlias = new ValueTypeAlias();
extensions.forEach(e -> valueTypeAlias.addAll(e.valueTypeAliases()));
return valueTypeAlias;
}
private List<Supplier<?, ?>> loadSuppliers(List<TestFrameworkExtension> extensions, ValueTypeAlias valueTypeAlias) {
List<Supplier<?, ?>> suppliers = new LinkedList<>();
List<Supplier<?, ?>> skippedSuppliers = new LinkedList<>();
Set<Class<?>> loadedValueTypes = new HashSet<>();
for (TestFrameworkExtension extension : extensions) {
for (var supplier : extension.suppliers()) {
Class<?> valueType = supplier.getValueType();
String requestedSupplier = Config.getSelectedSupplier(valueType, valueTypeAlias);
if (supplier.getAlias().equals(requestedSupplier) || (requestedSupplier == null && !loadedValueTypes.contains(valueType))) {
suppliers.add(supplier);
loadedValueTypes.add(valueType);
} else {
skippedSuppliers.add(supplier);
}
}
}
logger.logSuppliers(suppliers, skippedSuppliers);
return suppliers;
}
private List<Class<?>> loadAlwaysEnabledValueTypes(List<TestFrameworkExtension> extensions) {
return extensions.stream().flatMap(s -> s.alwaysEnabledValueTypes().stream()).toList();
}
}

View File

@ -1,35 +1,34 @@
package org.keycloak.test.framework.injection;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.keycloak.test.framework.TestFrameworkExtension;
import org.keycloak.test.framework.config.Config;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.stream.Collectors;
@SuppressWarnings({"rawtypes", "unchecked"})
public class Registry implements ExtensionContext.Store.CloseableResource {
private RegistryLogger logger;
private final RegistryLogger logger;
private ExtensionContext currentContext;
private final List<Supplier<?, ?>> suppliers = new LinkedList<>();
private final Extensions extensions;
private final List<InstanceContext<?, ?>> deployedInstances = new LinkedList<>();
private final List<RequestedInstance<?, ?>> requestedInstances = new LinkedList<>();
public Registry() {
loadSuppliers();
extensions = new Extensions();
logger = new RegistryLogger(extensions.getValueTypeAlias());
}
RegistryLogger getLogger() {
return logger;
}
public ExtensionContext getCurrentContext() {
@ -48,11 +47,11 @@ public class Registry implements ExtensionContext.Store.CloseableResource {
return dependency;
} else {
dependency = getRequestedDependency(typeClass, ref, dependent);
if(dependency != null) {
if (dependency != null) {
return dependency;
} else {
dependency = getUnConfiguredDependency(typeClass, ref, dependent);
if(dependency != null) {
if (dependency != null) {
return dependency;
}
}
@ -100,22 +99,18 @@ public class Registry implements ExtensionContext.Store.CloseableResource {
private <T> T getUnConfiguredDependency(Class<T> typeClass, String ref, InstanceContext dependent) {
InstanceContext dependency;
Optional<Supplier<?, ?>> supplied = suppliers.stream().filter(s -> s.getValueType().equals(typeClass)).findFirst();
if (supplied.isPresent()) {
Supplier<T, ?> supplier = (Supplier<T, ?>) supplied.get();
Annotation defaultAnnotation = DefaultAnnotationProxy.proxy(supplier.getAnnotationClass());
dependency = new InstanceContext(-1, this, supplier, defaultAnnotation, typeClass);
Supplier<?, ?> supplier = extensions.findSupplierByType(typeClass);
Annotation defaultAnnotation = DefaultAnnotationProxy.proxy(supplier.getAnnotationClass());
dependency = new InstanceContext(-1, this, supplier, defaultAnnotation, typeClass);
dependency.registerDependency(dependent);
dependency.setValue(supplier.getValue(dependency));
dependency.registerDependency(dependent);
dependency.setValue(supplier.getValue(dependency));
deployedInstances.add(dependency);
deployedInstances.add(dependency);
logger.logDependencyInjection(dependent, dependency, RegistryLogger.InjectionType.UN_CONFIGURED);
logger.logDependencyInjection(dependent, dependency, RegistryLogger.InjectionType.UN_CONFIGURED);
return (T) dependency.getValue();
}
return null;
return (T) dependency.getValue();
}
public void beforeEach(Object testInstance) {
@ -127,6 +122,14 @@ public class Registry implements ExtensionContext.Store.CloseableResource {
}
private void findRequestedInstances(Object testInstance) {
List<Class<?>> alwaysEnabledValueTypes = extensions.getAlwaysEnabledValueTypes();
for (Class<?> valueType : alwaysEnabledValueTypes) {
RequestedInstance requestedInstance = createRequestedInstance(null, valueType);
if (requestedInstance != null) {
requestedInstances.add(requestedInstance);
}
}
Class testClass = testInstance.getClass();
RequestedInstance requestedServerInstance = createRequestedInstance(testClass.getAnnotations(), null);
if (requestedServerInstance != null) {
@ -178,7 +181,7 @@ public class Registry implements ExtensionContext.Store.CloseableResource {
private void injectFields(Object testInstance) {
for (Field f : listFields(testInstance.getClass())) {
InstanceContext<?, ?> instance = getDeployedInstance(f.getType(), f.getAnnotations());
if(instance == null) { // a test class might have fields not meant for injection
if (instance == null) { // a test class might have fields not meant for injection
continue;
}
try {
@ -221,16 +224,23 @@ public class Registry implements ExtensionContext.Store.CloseableResource {
}
List<Supplier<?, ?>> getSuppliers() {
return suppliers;
return extensions.getSuppliers();
}
private RequestedInstance<?, ?> createRequestedInstance(Annotation[] annotations, Class<?> valueType) {
for (Annotation a : annotations) {
for (Supplier s : suppliers) {
if (s.getAnnotationClass().equals(a.annotationType())) {
return new RequestedInstance(s, a, valueType);
if (annotations != null) {
for (Annotation annotation : annotations) {
Supplier<?, ?> supplier = extensions.findSupplierByAnnotation(annotation);
if (supplier != null) {
return new RequestedInstance(supplier, annotation, valueType);
}
}
} else {
Supplier<?, ?> supplier = extensions.findSupplierByType(valueType);
if (supplier != null) {
Annotation defaultAnnotation = DefaultAnnotationProxy.proxy(supplier.getAnnotationClass());
return new RequestedInstance(supplier, defaultAnnotation, valueType);
}
}
return null;
}
@ -241,7 +251,7 @@ public class Registry implements ExtensionContext.Store.CloseableResource {
Supplier supplier = i.getSupplier();
if (supplier.getAnnotationClass().equals(a.annotationType())
&& valueType.isAssignableFrom(i.getValue().getClass())
&& Objects.equals(supplier.getRef(a), i.getRef()) ) {
&& Objects.equals(supplier.getRef(a), i.getRef())) {
return i;
}
}
@ -264,7 +274,7 @@ public class Registry implements ExtensionContext.Store.CloseableResource {
String requestedRef = requestedInstance.getRef();
Class requestedValueType = requestedInstance.getValueType();
for (InstanceContext<?, ?> i : deployedInstances) {
if(!Objects.equals(i.getRef(), requestedRef)) {
if (!Objects.equals(i.getRef(), requestedRef)) {
continue;
}
@ -279,47 +289,6 @@ public class Registry implements ExtensionContext.Store.CloseableResource {
return null;
}
private void loadSuppliers() {
Iterator<TestFrameworkExtension> extensions = ServiceLoader.load(TestFrameworkExtension.class).iterator();
ValueTypeAlias valueTypeAlias = new ValueTypeAlias();
List<Supplier> tmp = new LinkedList<>();
while (extensions.hasNext()) {
TestFrameworkExtension extension = extensions.next();
tmp.addAll(extension.suppliers());
valueTypeAlias.addAll(extension.valueTypeAliases());
}
logger = new RegistryLogger(valueTypeAlias);
Set<Class> loadedValueTypes = new HashSet<>();
Set<Supplier> skippedSuppliers = new HashSet<>();
for (Supplier supplier : tmp) {
boolean shouldAdd = false;
Class supplierValueType = supplier.getValueType();
if (!loadedValueTypes.contains(supplierValueType)) {
String requestedSupplier = Config.getSelectedSupplier(supplierValueType, valueTypeAlias);
if (requestedSupplier != null) {
if (requestedSupplier.equals(supplier.getAlias())) {
shouldAdd = true;
}
} else {
shouldAdd = true;
}
}
if (shouldAdd) {
suppliers.add(supplier);
loadedValueTypes.add(supplierValueType);
} else {
skippedSuppliers.add(supplier);
}
}
logger.logSuppliers(suppliers, skippedSuppliers);
}
private InstanceContext getDeployedInstance(Class typeClass, String ref) {
return deployedInstances.stream()
.filter(i -> i.getSupplier().getValueType().equals(typeClass) && Objects.equals(i.getRef(), ref))

View File

@ -3,7 +3,6 @@ package org.keycloak.test.framework.injection;
import org.jboss.logging.Logger;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@SuppressWarnings("rawtypes")
@ -89,7 +88,7 @@ class RegistryLogger {
LOGGER.debug("Closing all instances");
}
public void logSuppliers(List<Supplier<?, ?>> suppliers, Set<Supplier> skippedSuppliers) {
public void logSuppliers(List<Supplier<?, ?>> suppliers, List<Supplier<?, ?>> skippedSuppliers) {
if (LOGGER.isDebugEnabled()) {
StringBuilder sb = new StringBuilder();
sb.append("Loaded suppliers:");
@ -122,6 +121,10 @@ class RegistryLogger {
}
public void logIntercepted(Object value, Supplier<?, ?> supplier) {
LOGGER.debugv("{0} intercepted by {1}", value.getClass().getSimpleName(), supplier.getClass().getSimpleName());
}
public enum InjectionType {
EXISTING("existing"),

View File

@ -1,6 +1,8 @@
package org.keycloak.test.framework.injection;
import java.lang.annotation.Annotation;
import java.util.Collections;
import java.util.Set;
public interface Supplier<T, S extends Annotation> {
@ -37,4 +39,9 @@ public interface Supplier<T, S extends Annotation> {
default int order() {
return SupplierOrder.DEFAULT;
}
default Set<Class<?>> dependencies() {
return Collections.emptySet();
}
}

View File

@ -21,6 +21,7 @@ public class SupplierHelpers {
return value != null ? value : defaultValue;
}
@SuppressWarnings("unchecked")
public static <T> T getAnnotationField(Annotation annotation, String name) {
if (annotation != null) {
for (Method m : annotation.annotationType().getMethods()) {

View File

@ -10,7 +10,7 @@ import org.keycloak.test.framework.injection.RequestedInstance;
import org.keycloak.test.framework.injection.Supplier;
import org.keycloak.test.framework.injection.SupplierHelpers;
import org.keycloak.test.framework.injection.SupplierOrder;
import org.keycloak.test.framework.server.AbstractInterceptorHelper;
import org.keycloak.test.framework.injection.AbstractInterceptorHelper;
import org.keycloak.test.framework.server.KeycloakServer;
public class RealmSupplier implements Supplier<ManagedRealm, InjectRealm> {

View File

@ -1,6 +1,7 @@
package org.keycloak.test.framework.server;
import org.jboss.logging.Logger;
import org.keycloak.test.framework.injection.AbstractInterceptorHelper;
import org.keycloak.test.framework.annotations.KeycloakIntegrationTest;
import org.keycloak.test.framework.config.Config;
import org.keycloak.test.framework.database.TestDatabase;

View File

@ -12,6 +12,11 @@ public class RemoteTestFrameworkExtension implements TestFrameworkExtension {
return List.of(
new TimeOffsetSupplier(),
new RemoteProvidersSupplier()
);
);
}
@Override
public List<Class<?>> alwaysEnabledValueTypes() {
return List.of(RemoteProviders.class);
}
}

View File

@ -7,11 +7,11 @@ import org.keycloak.test.framework.injection.RequestedInstance;
import org.keycloak.test.framework.injection.Supplier;
import org.keycloak.test.framework.injection.SupplierOrder;
import org.keycloak.test.framework.remote.RemoteProviders;
import org.keycloak.test.framework.server.KeycloakServerConfigBuilder;
import org.keycloak.test.framework.server.KeycloakServerConfigInterceptor;
import org.keycloak.test.framework.server.KeycloakUrls;
public class TimeOffsetSupplier implements Supplier<TimeOffSet, InjectTimeOffSet>, KeycloakServerConfigInterceptor<TimeOffSet, InjectTimeOffSet> {
import java.util.Set;
public class TimeOffsetSupplier implements Supplier<TimeOffSet, InjectTimeOffSet> {
@Override
public Class<InjectTimeOffSet> getAnnotationClass() {
return InjectTimeOffSet.class;
@ -22,6 +22,11 @@ public class TimeOffsetSupplier implements Supplier<TimeOffSet, InjectTimeOffSet
return TimeOffSet.class;
}
@Override
public Set<Class<?>> dependencies() {
return Set.of(HttpClient.class, RemoteProviders.class, KeycloakUrls.class);
}
@Override
public TimeOffSet getValue(InstanceContext<TimeOffSet, InjectTimeOffSet> instanceContext) {
var httpClient = instanceContext.getDependency(HttpClient.class);
@ -55,11 +60,6 @@ public class TimeOffsetSupplier implements Supplier<TimeOffSet, InjectTimeOffSet
@Override
public int order() {
return SupplierOrder.BEFORE_KEYCLOAK_SERVER;
// Implementing the KeycloakServerConfigInterceptor is a workaround for RemoteProvidersSupplier to work
}
@Override
public KeycloakServerConfigBuilder intercept(KeycloakServerConfigBuilder serverConfig, InstanceContext<TimeOffSet, InjectTimeOffSet> instanceContext) {
return serverConfig;
}
}