> allowedFactories) {
+ this.allowedSpis = allowedSpis;
+ this.allowedFactories = allowedFactories;
+ }
+
+ boolean isSpiAllowed(Spi s) {
+ return allowedSpis.contains(s.getClass());
+ }
+
+ boolean isFactoryAllowed(ProviderFactory factory) {
+ return allowedFactories.stream().anyMatch((c) -> c.isAssignableFrom(factory.getClass()));
+ }
+
+}
diff --git a/testsuite/model/src/test/java/org/keycloak/model/KeycloakModelTest.java b/testsuite/model/src/test/java/org/keycloak/model/KeycloakModelTest.java
new file mode 100644
index 0000000000..2f4533c942
--- /dev/null
+++ b/testsuite/model/src/test/java/org/keycloak/model/KeycloakModelTest.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright 2020 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.
+ */
+package org.keycloak.model;
+
+import org.keycloak.Config.Scope;
+import org.keycloak.authorization.AuthorizationSpi;
+import org.keycloak.authorization.DefaultAuthorizationProviderFactory;
+import org.keycloak.authorization.store.StoreFactorySpi;
+import org.keycloak.cluster.ClusterSpi;
+import org.keycloak.component.ComponentModel;
+import org.keycloak.events.EventStoreSpi;
+import org.keycloak.executors.DefaultExecutorsProviderFactory;
+import org.keycloak.executors.ExecutorsSpi;
+import org.keycloak.models.AbstractKeycloakTransaction;
+import org.keycloak.models.ClientSpi;
+import org.keycloak.models.GroupSpi;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RealmSpi;
+import org.keycloak.models.RoleSpi;
+import org.keycloak.models.UserSpi;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.provider.Provider;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.provider.ProviderManager;
+import org.keycloak.provider.Spi;
+import org.keycloak.services.DefaultKeycloakSession;
+import org.keycloak.services.DefaultKeycloakSessionFactory;
+import org.keycloak.storage.UserStorageProvider;
+import org.keycloak.storage.UserStorageProviderModel;
+import com.google.common.collect.ImmutableSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.hamcrest.Matchers;
+import org.jboss.logging.Logger;
+import org.junit.After;
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+import static org.hamcrest.Matchers.hasSize;
+import static org.junit.Assert.assertThat;
+
+/**
+ * Base of testcases that operate on session level. The tests derived from this class
+ * will have access to a shared {@link KeycloakSessionFactory} in the {@link #FACTORY}
+ * field that can be used to obtain a session and e.g. start / stop transaction.
+ *
+ * This class expects {@code keycloak.model.parameters} system property to contain
+ * comma-separated class names that implement {@link KeycloakModelParameters} interface
+ * to provide list of factories and SPIs that are visible to the {@link KeycloakSessionFactory}
+ * that is offered to the tests.
+ *
+ * If no parameters are set via this property, the tests derived from this class are skipped.
+ * @author hmlnarik
+ */
+public abstract class KeycloakModelTest {
+
+ private static final Logger LOG = Logger.getLogger(KeycloakModelParameters.class);
+ protected final Logger log = Logger.getLogger(getClass());
+
+ @ClassRule
+ public static final TestRule GUARANTEE_REQUIRED_FACTORY = new TestRule() {
+ @Override
+ public Statement apply(Statement base, Description description) {
+ Class> testClass = description.getTestClass();
+ while (testClass != Object.class) {
+ for (RequireProvider ann : testClass.getAnnotationsByType(RequireProvider.class)) {
+ Assume.assumeThat("Provider must exist: " + ann.value(), FACTORY.getProviderFactory(ann.value()), Matchers.notNullValue());
+ }
+ testClass = testClass.getSuperclass();
+ }
+ return base;
+ }
+ };
+
+ private static final Set> ALLOWED_SPIS = ImmutableSet.>builder()
+ .add(AuthorizationSpi.class)
+ .add(ClientSpi.class)
+ .add(ClusterSpi.class)
+ .add(EventStoreSpi.class)
+ .add(ExecutorsSpi.class)
+ .add(GroupSpi.class)
+ .add(RealmSpi.class)
+ .add(RoleSpi.class)
+ .add(StoreFactorySpi.class)
+ .add(UserSpi.class)
+ .build();
+
+ private static final Set> ALLOWED_FACTORIES = ImmutableSet.>builder()
+ .add(DefaultAuthorizationProviderFactory.class)
+ .add(DefaultExecutorsProviderFactory.class)
+ .build();
+
+ protected static final List MODEL_PARAMETERS;
+ protected static final DefaultKeycloakSessionFactory FACTORY;
+
+ static {
+ KeycloakModelParameters basicParameters = new KeycloakModelParameters(ALLOWED_SPIS, ALLOWED_FACTORIES);
+ MODEL_PARAMETERS = Stream.concat(
+ Stream.of(basicParameters),
+ Stream.of(System.getProperty("keycloak.model.parameters", "").split("\\s*,\\s*"))
+ .filter(s -> s != null && ! s.trim().isEmpty())
+ .map(cn -> { try { return Class.forName(cn.indexOf('.') >= 0 ? cn : ("org.keycloak.model.parameters." + cn)); } catch (Exception e) { LOG.error("Cannot find " + cn); return null; }})
+ .filter(Objects::nonNull)
+ .map(c -> { try { return c.newInstance(); } catch (Exception e) { LOG.error("Cannot instantiate " + c); return null; }} )
+ .filter(KeycloakModelParameters.class::isInstance)
+ .map(KeycloakModelParameters.class::cast)
+ )
+ .collect(Collectors.toList());
+
+ FACTORY = new DefaultKeycloakSessionFactory() {
+ @Override
+ protected boolean isEnabled(ProviderFactory factory, Scope scope) {
+ return super.isEnabled(factory, scope) && isFactoryAllowed(factory);
+ }
+
+ @Override
+ protected Map, Map> loadFactories(ProviderManager pm) {
+ spis.removeIf(s -> ! isSpiAllowed(s));
+ return super.loadFactories(pm);
+ }
+
+ private boolean isSpiAllowed(Spi s) {
+ return MODEL_PARAMETERS.stream().anyMatch(p -> p.isSpiAllowed(s));
+ }
+
+ private boolean isFactoryAllowed(ProviderFactory factory) {
+ return MODEL_PARAMETERS.stream().anyMatch(p -> p.isFactoryAllowed(factory));
+ }
+ };
+ FACTORY.init();
+ }
+
+ @BeforeClass
+ public static void checkValidParameters() {
+ Assume.assumeTrue("keycloak.model.parameters property must be set", MODEL_PARAMETERS.size() > 1); // Additional parameters have to be set
+ }
+
+ protected void createEnvironment(KeycloakSession s) {
+ }
+
+ protected void cleanEnvironment(KeycloakSession s) {
+ }
+
+ @Before
+ public void createEnvironment() {
+ KeycloakModelUtils.runJobInTransaction(FACTORY, this::createEnvironment);
+ }
+
+ @After
+ public void cleanEnvironment() {
+ KeycloakModelUtils.runJobInTransaction(FACTORY, this::cleanEnvironment);
+ }
+
+ protected String registerUserFederationIfAvailable(RealmModel realm) {
+ final List userFedProviders = FACTORY.getProviderFactories(UserStorageProvider.class);
+
+ if (! userFedProviders.isEmpty() && realm != null) {
+ assertThat("Cannot handle more than 1 user federation provider", userFedProviders, hasSize(1));
+ UserStorageProviderModel federatedStorage = new UserStorageProviderModel();
+ federatedStorage.setName(userFedProviders.get(0).getId());
+ federatedStorage.setProviderId(userFedProviders.get(0).getId());
+ federatedStorage.setProviderType(UserStorageProvider.class.getName());
+ federatedStorage.setParentId(realm.getId());
+ ComponentModel res = realm.addComponentModel(federatedStorage);
+ log.infof("Added %s user federation provider: %s", federatedStorage.getName(), res.getId());
+ return res.getId();
+ }
+ return null;
+ }
+
+ protected void inRolledBackTransaction(T parameter, BiConsumer what) {
+ KeycloakSession session = new DefaultKeycloakSession(FACTORY);
+ session.getTransactionManager().begin();
+
+ what.accept(session, parameter);
+
+ session.getTransactionManager().rollback();
+ }
+
+ protected void inComittedTransaction(T parameter, BiConsumer what) {
+ inComittedTransaction(parameter, what, (a,b) -> {}, (a,b) -> {});
+ }
+
+ protected void inComittedTransaction(T parameter, BiConsumer what, BiConsumer onCommit, BiConsumer onRollback) {
+ KeycloakModelUtils.runJobInTransaction(FACTORY, session -> {
+ session.getTransactionManager().enlistAfterCompletion(new AbstractKeycloakTransaction() {
+ @Override
+ protected void commitImpl() {
+ if (onCommit != null) { onCommit.accept(session, parameter); }
+ }
+
+ @Override
+ protected void rollbackImpl() {
+ if (onRollback != null) { onRollback.accept(session, parameter); }
+ }
+ });
+ what.accept(session, parameter);
+ });
+ }
+
+}
diff --git a/testsuite/model/src/test/java/org/keycloak/model/RequireProvider.java b/testsuite/model/src/test/java/org/keycloak/model/RequireProvider.java
new file mode 100644
index 0000000000..bb7a70a51c
--- /dev/null
+++ b/testsuite/model/src/test/java/org/keycloak/model/RequireProvider.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2020 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.
+ */
+package org.keycloak.model;
+
+import org.keycloak.provider.Provider;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Identifies a requirement for a given provider to be present in the session factory.
+ * If the provider is not available, the test is skipped.
+ *
+ * @author hmlnarik
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+@Repeatable(RequireProviders.class)
+public @interface RequireProvider {
+ Class extends Provider> value() default Provider.class;
+
+}
diff --git a/testsuite/model/src/test/java/org/keycloak/model/RequireProviders.java b/testsuite/model/src/test/java/org/keycloak/model/RequireProviders.java
new file mode 100644
index 0000000000..0786125567
--- /dev/null
+++ b/testsuite/model/src/test/java/org/keycloak/model/RequireProviders.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2020 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.
+ */
+package org.keycloak.model;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ *
+ * @author hmlnarik
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface RequireProviders {
+ RequireProvider[] value();
+}
diff --git a/testsuite/model/src/test/java/org/keycloak/model/UserModelTest.java b/testsuite/model/src/test/java/org/keycloak/model/UserModelTest.java
new file mode 100644
index 0000000000..1210d6c3b1
--- /dev/null
+++ b/testsuite/model/src/test/java/org/keycloak/model/UserModelTest.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright 2020 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.
+ */
+package org.keycloak.model;
+
+import org.keycloak.component.ComponentModel;
+import org.keycloak.models.GroupModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RealmProvider;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.UserProvider;
+import org.keycloak.storage.UserStorageProvider;
+import org.keycloak.storage.UserStorageProviderFactory;
+import org.keycloak.storage.UserStorageProviderModel;
+import org.keycloak.storage.user.UserLookupProvider;
+import org.keycloak.storage.user.UserRegistrationProvider;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentSkipListSet;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import org.hamcrest.Matchers;
+import org.junit.Test;
+import static org.hamcrest.Matchers.hasItem;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeThat;
+
+/**
+ *
+ * @author hmlnarik
+ */
+@RequireProvider(UserProvider.class)
+@RequireProvider(RealmProvider.class)
+public class UserModelTest extends KeycloakModelTest {
+
+ protected static final int NUM_GROUPS = 100;
+
+ private String realmId;
+ private final List groupIds = new ArrayList<>(NUM_GROUPS);
+ private String userFederationId;
+
+ @Override
+ public void createEnvironment(KeycloakSession s) {
+ RealmModel realm = s.realms().createRealm("realm");
+ this.realmId = realm.getId();
+
+ this.userFederationId = registerUserFederationIfAvailable(realm);
+
+ IntStream.range(0, NUM_GROUPS).forEach(i -> {
+ groupIds.add(s.groups().createGroup(realm, "group-" + i).getId());
+ });
+ }
+
+ @Override
+ public void cleanEnvironment(KeycloakSession s) {
+ s.realms().removeRealm(realmId);
+ }
+
+ private void addRemoveUser(KeycloakSession session, int i) {
+ RealmModel realm = session.realms().getRealmByName("realm");
+
+ UserModel user = session.users().addUser(realm, "user-" + i);
+
+ IntStream.range(0, NUM_GROUPS / 20).forEach(gIndex -> {
+ user.joinGroup(session.groups().getGroupById(realm, groupIds.get((i + gIndex) % NUM_GROUPS)));
+ });
+
+ final UserModel obtainedUser = session.users().getUserById(user.getId(), realm);
+
+ assertThat(obtainedUser, Matchers.notNullValue());
+ assertThat(obtainedUser.getUsername(), is("user-" + i));
+ Set userGroupIds = obtainedUser.getGroupsStream().map(GroupModel::getName).collect(Collectors.toSet());
+ assertThat(userGroupIds, hasSize(NUM_GROUPS / 20));
+ assertThat(userGroupIds, hasItem("group-" + i));
+ assertThat(userGroupIds, hasItem("group-" + (i - 1 + (NUM_GROUPS / 20)) % NUM_GROUPS));
+
+ assertTrue(session.users().removeUser(realm, user));
+ assertFalse(session.users().removeUser(realm, user));
+ }
+
+ @Test
+ public void testAddRemoveUser() {
+ inRolledBackTransaction(1, this::addRemoveUser);
+ }
+
+ @Test
+ public void testAddRemoveUserConcurrent() {
+ IntStream.range(0,100).parallel().forEach(i -> inComittedTransaction(i, this::addRemoveUser));
+ }
+
+ @Test
+ public void testAddRemoveUsersInTheSameGroupConcurrent() {
+ final ConcurrentSkipListSet userIds = new ConcurrentSkipListSet<>();
+ String groupId = groupIds.get(0);
+
+ // Create users and let them join first group
+ IntStream.range(0, 100).parallel().forEach(index -> inComittedTransaction(index, (session, i) -> {
+ final RealmModel realm = session.realms().getRealm(realmId);
+ final UserModel user = session.users().addUser(realm, "user-" + i);
+ user.joinGroup(session.groups().getGroupById(realm, groupId));
+ userIds.add(user.getId());
+ }));
+
+ inComittedTransaction(1, (session, i) -> {
+ final RealmModel realm = session.realms().getRealm(realmId);
+ final GroupModel group = session.groups().getGroupById(realm, groupId);
+ assertThat(session.users().getGroupMembersStream(realm, group).count(), is(100L));
+ });
+
+ // Some of the transactions may fail due to conflicts as there are many parallel request, so repeat until all users are removed
+ Set remainingUserIds = new HashSet<>();
+ do {
+ userIds.stream().parallel().forEach(index -> inComittedTransaction(index, (session, userId) -> {
+ final RealmModel realm = session.realms().getRealm(realmId);
+ final UserModel user = session.users().getUserById(userId, realm);
+ log.debugf("Remove user %s: %s", userId, session.users().removeUser(realm, user));
+ }, null, (session, userId) -> remainingUserIds.add(userId) ));
+
+ userIds.clear();
+ userIds.addAll(remainingUserIds);
+ remainingUserIds.clear();
+ } while (! userIds.isEmpty());
+
+ inComittedTransaction(1, (session, i) -> {
+ final RealmModel realm = session.realms().getRealm(realmId);
+ final GroupModel group = session.groups().getGroupById(realm, groupId);
+ assertThat(session.users().getGroupMembersStream(realm, group).collect(Collectors.toList()), Matchers.empty());
+ });
+ }
+
+ @Test
+ public void testAddDirtyRemoveFederationUsersInTheSameGroupConcurrent() {
+ assumeThat("Test for federated providers only", userFederationId, Matchers.notNullValue());
+
+ final ConcurrentSkipListSet userIds = new ConcurrentSkipListSet<>();
+ String groupId = groupIds.get(0);
+
+ // Create users and let them join first group
+ IntStream.range(0, 100).parallel().forEach(index -> inComittedTransaction(index, (session, i) -> {
+ final RealmModel realm = session.realms().getRealm(realmId);
+ final UserModel user = session.users().addUser(realm, "user-" + i);
+ user.joinGroup(session.groups().getGroupById(realm, groupId));
+ userIds.add(user.getId());
+ }));
+
+ // Remove users _from the federation_, simulates eg. user being removed from LDAP without Keycloak knowing
+ inComittedTransaction(1, (session, i) -> {
+ final RealmModel realm = session.realms().getRealm(realmId);
+ UserStorageProvider instance = (UserStorageProvider)session.getAttribute(userFederationId);
+
+ if (instance == null) {
+ ComponentModel model = realm.getComponent(userFederationId);
+ UserStorageProviderModel storageModel = new UserStorageProviderModel(model);
+ UserStorageProviderFactory factory = (UserStorageProviderFactory)session.getKeycloakSessionFactory().getProviderFactory(UserStorageProvider.class, model.getProviderId());
+ instance = factory.create(session, model);
+ if (instance == null) {
+ throw new RuntimeException("UserStorageProvideFactory (of type " + factory.getClass().getName() + ") produced a null instance");
+ }
+ session.enlistForClose(instance);
+ session.setAttribute(userFederationId, instance);
+ }
+
+ final UserStorageProvider lambdaInstance = instance;
+ log.debugf("Removing selected users from backend");
+ IntStream.range(FIRST_DELETED_USER_INDEX, LAST_DELETED_USER_INDEX).forEach(j -> {
+ final UserModel user = ((UserLookupProvider) lambdaInstance).getUserByUsername("user-" + j, realm);
+ ((UserRegistrationProvider) lambdaInstance).removeUser(realm, user);
+ });
+ });
+
+ inComittedTransaction(1, (session, i) -> {
+ final RealmModel realm = session.realms().getRealm(realmId);
+ final GroupModel group = session.groups().getGroupById(realm, groupId);
+ assertThat(session.users().getGroupMembersStream(realm, group).count(), is(100L - DELETED_USER_COUNT));
+ });
+
+ // Now delete the users, and count those that were not found to be deleted. This should be equal to the number
+ // of users removed directly in the user federation.
+ // Some of the transactions may fail due to conflicts as there are many parallel request, so repeat until all users are removed
+ AtomicInteger notFoundUsers = new AtomicInteger();
+ Set remainingUserIds = new HashSet<>();
+ do {
+ userIds.stream().parallel().forEach(index -> inComittedTransaction(index, (session, userId) -> {
+ final RealmModel realm = session.realms().getRealm(realmId);
+ final UserModel user = session.users().getUserById(userId, realm);
+ if (user != null) {
+ log.debugf("Deleting user: %s", userId);
+ session.users().removeUser(realm, user);
+ } else {
+ log.debugf("Failed deleting user: %s", userId);
+ notFoundUsers.incrementAndGet();
+ }
+ }, null, (session, userId) -> {
+ log.debugf("Could not delete user %s", userId);
+ remainingUserIds.add(userId);
+ }));
+
+ userIds.clear();
+ userIds.addAll(remainingUserIds);
+ remainingUserIds.clear();
+ } while (! userIds.isEmpty());
+
+ assertThat(notFoundUsers.get(), is(DELETED_USER_COUNT));
+
+ inComittedTransaction(1, (session, i) -> {
+ final RealmModel realm = session.realms().getRealm(realmId);
+ final GroupModel group = session.groups().getGroupById(realm, groupId);
+ assertThat(session.users().getGroupMembersStream(realm, group).collect(Collectors.toList()), Matchers.empty());
+ });
+ }
+ private static final int FIRST_DELETED_USER_INDEX = 10;
+ private static final int LAST_DELETED_USER_INDEX = 90;
+ private static final int DELETED_USER_COUNT = LAST_DELETED_USER_INDEX - FIRST_DELETED_USER_INDEX;
+}
diff --git a/model/jpa/src/test/java/org/keycloak/events/jpa/JpaAdminEventQueryTest.java b/testsuite/model/src/test/java/org/keycloak/model/events/AdminEventQueryTest.java
similarity index 51%
rename from model/jpa/src/test/java/org/keycloak/events/jpa/JpaAdminEventQueryTest.java
rename to testsuite/model/src/test/java/org/keycloak/model/events/AdminEventQueryTest.java
index 4f06ff2ea4..98985f4122 100644
--- a/model/jpa/src/test/java/org/keycloak/events/jpa/JpaAdminEventQueryTest.java
+++ b/testsuite/model/src/test/java/org/keycloak/model/events/AdminEventQueryTest.java
@@ -14,36 +14,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.keycloak.events.jpa;
+package org.keycloak.model.events;
-import org.keycloak.Config.Scope;
import org.keycloak.common.ClientConnection;
-import org.keycloak.connections.jpa.DefaultJpaConnectionProviderFactory;
-import org.keycloak.connections.jpa.JpaConnectionSpi;
-import org.keycloak.connections.jpa.updater.JpaUpdaterProviderFactory;
-import org.keycloak.connections.jpa.updater.JpaUpdaterSpi;
-import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionProviderFactory;
-import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionSpi;
-import org.keycloak.connections.jpa.updater.liquibase.lock.LiquibaseDBLockProviderFactory;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventStoreProvider;
-import org.keycloak.events.EventStoreProviderFactory;
-import org.keycloak.events.EventStoreSpi;
import org.keycloak.events.EventType;
+import org.keycloak.model.KeycloakModelTest;
+import org.keycloak.model.RequireProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
-import org.keycloak.models.RealmSpi;
-import org.keycloak.models.dblock.DBLockSpi;
-import org.keycloak.models.jpa.JpaRealmProviderFactory;
-import org.keycloak.provider.Provider;
-import org.keycloak.provider.ProviderFactory;
-import org.keycloak.provider.ProviderManager;
-import org.keycloak.provider.Spi;
-import org.keycloak.services.DefaultKeycloakSession;
-import org.keycloak.services.DefaultKeycloakSessionFactory;
-import com.google.common.collect.ImmutableSet;
-import java.util.Map;
-import java.util.Set;
import java.util.stream.Collectors;
import org.junit.After;
import org.junit.Before;
@@ -55,43 +35,17 @@ import static org.junit.Assert.assertThat;
*
* @author hmlnarik
*/
-public class JpaAdminEventQueryTest {
+@RequireProvider(EventStoreProvider.class)
+public class AdminEventQueryTest extends KeycloakModelTest {
- private static final Set> ALLOWED_SPIS = ImmutableSet.>builder()
- .add(DBLockSpi.class)
- .add(EventStoreSpi.class)
- .add(JpaConnectionSpi.class)
- .add(JpaUpdaterSpi.class)
- .add(LiquibaseConnectionSpi.class)
- .add(RealmSpi.class)
- .build();
-
- private static final Set> ALLOWED_FACTORIES = ImmutableSet.>builder()
- .add(DefaultJpaConnectionProviderFactory.class)
- .add(EventStoreProviderFactory.class)
- .add(JpaUpdaterProviderFactory.class)
- .add(JpaRealmProviderFactory.class)
- .add(LiquibaseConnectionProviderFactory.class)
- .add(LiquibaseDBLockProviderFactory.class)
- .build();
-
- private static final DefaultKeycloakSessionFactory factory = new DefaultKeycloakSessionFactory() {
- @Override
- protected boolean isEnabled(ProviderFactory factory, Scope scope) {
- return super.isEnabled(factory, scope) && ALLOWED_FACTORIES.stream().filter(c -> c.isAssignableFrom(factory.getClass())).findAny().isPresent();
- }
-
- @Override
- protected Map, Map> loadFactories(ProviderManager pm) {
- spis.removeIf(s -> ! ALLOWED_SPIS.contains(s.getClass()));
- return super.loadFactories(pm);
- }
- };
- static { factory.init(); }
-
- private final KeycloakSession session = new DefaultKeycloakSession(factory);
+ private final KeycloakSession session = FACTORY.create();
private final EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class);
+ @Test
+ public void testClear() {
+ eventStore.clear();
+ }
+
@Before
public void startTransaction() {
session.getTransactionManager().begin();
@@ -102,11 +56,6 @@ public class JpaAdminEventQueryTest {
session.getTransactionManager().rollback();
}
- @Test
- public void testClear() {
- eventStore.clear();
- }
-
@Test
public void testQuery() {
RealmModel realm = session.realms().createRealm("realm");
diff --git a/testsuite/model/src/test/java/org/keycloak/model/parameters/BackwardsCompatibilityUserStorage.java b/testsuite/model/src/test/java/org/keycloak/model/parameters/BackwardsCompatibilityUserStorage.java
new file mode 100644
index 0000000000..a644e25f4f
--- /dev/null
+++ b/testsuite/model/src/test/java/org/keycloak/model/parameters/BackwardsCompatibilityUserStorage.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2020 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.
+ */
+package org.keycloak.model.parameters;
+
+import org.keycloak.model.KeycloakModelParameters;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.provider.Spi;
+import org.keycloak.storage.UserStorageProviderSpi;
+import org.keycloak.storage.federated.UserFederatedStorageProviderSpi;
+import org.keycloak.storage.jpa.JpaUserFederatedStorageProviderFactory;
+import org.keycloak.testsuite.federation.BackwardsCompatibilityUserStorageFactory;
+import com.google.common.collect.ImmutableSet;
+import java.util.Set;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class BackwardsCompatibilityUserStorage extends KeycloakModelParameters {
+
+ static final Set> ALLOWED_SPIS = ImmutableSet.>builder()
+ .build();
+
+ static final Set> ALLOWED_FACTORIES = ImmutableSet.>builder()
+ .add(BackwardsCompatibilityUserStorageFactory.class)
+ .build();
+
+ public BackwardsCompatibilityUserStorage() {
+ super(ALLOWED_SPIS, ALLOWED_FACTORIES);
+ }
+}
diff --git a/testsuite/model/src/test/java/org/keycloak/model/parameters/ConcurrentHashMapStorage.java b/testsuite/model/src/test/java/org/keycloak/model/parameters/ConcurrentHashMapStorage.java
new file mode 100644
index 0000000000..537e7a9395
--- /dev/null
+++ b/testsuite/model/src/test/java/org/keycloak/model/parameters/ConcurrentHashMapStorage.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2020 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.
+ */
+package org.keycloak.model.parameters;
+
+import org.keycloak.model.KeycloakModelParameters;
+import org.keycloak.models.map.client.MapClientProviderFactory;
+import org.keycloak.models.map.group.MapGroupProviderFactory;
+import org.keycloak.models.map.role.MapRoleProviderFactory;
+import org.keycloak.models.map.storage.ConcurrentHashMapStorageProvider;
+import org.keycloak.models.map.storage.MapStorageProvider;
+import org.keycloak.models.map.storage.MapStorageSpi;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.provider.Spi;
+import com.google.common.collect.ImmutableSet;
+import java.util.Set;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class ConcurrentHashMapStorage extends KeycloakModelParameters {
+
+ static final Set> ALLOWED_SPIS = ImmutableSet.>builder()
+ .build();
+
+ static final Set> ALLOWED_FACTORIES = ImmutableSet.>builder()
+ .add(ConcurrentHashMapStorageProvider.class)
+ .build();
+
+ static {
+ System.setProperty("keycloak.mapStorage.concurrenthashmap.dir", System.getProperty("keycloak.mapStorage.concurrenthashmap.dir", "${project.build.directory:target}"));
+ }
+
+ public ConcurrentHashMapStorage() {
+ super(ALLOWED_SPIS, ALLOWED_FACTORIES);
+ }
+
+}
diff --git a/testsuite/model/src/test/java/org/keycloak/model/parameters/Infinispan.java b/testsuite/model/src/test/java/org/keycloak/model/parameters/Infinispan.java
new file mode 100644
index 0000000000..273320fb71
--- /dev/null
+++ b/testsuite/model/src/test/java/org/keycloak/model/parameters/Infinispan.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2020 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.
+ */
+package org.keycloak.model.parameters;
+
+import org.keycloak.cluster.infinispan.InfinispanClusterProviderFactory;
+import org.keycloak.connections.infinispan.InfinispanConnectionProviderFactory;
+import org.keycloak.connections.infinispan.InfinispanConnectionSpi;
+import org.keycloak.model.KeycloakModelParameters;
+import org.keycloak.models.cache.CacheRealmProviderSpi;
+import org.keycloak.models.cache.CacheUserProviderSpi;
+import org.keycloak.models.cache.infinispan.InfinispanCacheRealmProviderFactory;
+import org.keycloak.models.cache.infinispan.InfinispanUserCacheProviderFactory;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.provider.Spi;
+import com.google.common.collect.ImmutableSet;
+import java.util.Set;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class Infinispan extends KeycloakModelParameters {
+
+ static final Set> ALLOWED_SPIS = ImmutableSet.>builder()
+ .add(CacheRealmProviderSpi.class)
+ .add(CacheUserProviderSpi.class)
+ .add(InfinispanConnectionSpi.class)
+
+ .build();
+
+ static final Set> ALLOWED_FACTORIES = ImmutableSet.>builder()
+ .add(InfinispanCacheRealmProviderFactory.class)
+ .add(InfinispanClusterProviderFactory.class)
+ .add(InfinispanConnectionProviderFactory.class)
+ .add(InfinispanUserCacheProviderFactory.class)
+ .build();
+
+ static {
+ System.setProperty("keycloak.connectionsInfinispan.default.embedded", System.getProperty("keycloak.connectionsInfinispan.default.embedded", "true"));
+ }
+
+ public Infinispan() {
+ super(ALLOWED_SPIS, ALLOWED_FACTORIES);
+ }
+}
diff --git a/testsuite/model/src/test/java/org/keycloak/model/parameters/Jpa.java b/testsuite/model/src/test/java/org/keycloak/model/parameters/Jpa.java
new file mode 100644
index 0000000000..b0d6c90bcc
--- /dev/null
+++ b/testsuite/model/src/test/java/org/keycloak/model/parameters/Jpa.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2020 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.
+ */
+package org.keycloak.model.parameters;
+
+import org.keycloak.authorization.jpa.store.JPAAuthorizationStoreFactory;
+import org.keycloak.connections.jpa.DefaultJpaConnectionProviderFactory;
+import org.keycloak.connections.jpa.JpaConnectionSpi;
+import org.keycloak.connections.jpa.updater.JpaUpdaterProviderFactory;
+import org.keycloak.connections.jpa.updater.JpaUpdaterSpi;
+import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionProviderFactory;
+import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionSpi;
+import org.keycloak.connections.jpa.updater.liquibase.lock.LiquibaseDBLockProviderFactory;
+import org.keycloak.events.jpa.JpaEventStoreProviderFactory;
+import org.keycloak.model.KeycloakModelParameters;
+import org.keycloak.models.dblock.DBLockSpi;
+import org.keycloak.models.jpa.JpaClientProviderFactory;
+import org.keycloak.models.jpa.JpaGroupProviderFactory;
+import org.keycloak.models.jpa.JpaRealmProviderFactory;
+import org.keycloak.models.jpa.JpaRoleProviderFactory;
+import org.keycloak.models.jpa.JpaUserProviderFactory;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.provider.Spi;
+import com.google.common.collect.ImmutableSet;
+import java.util.Set;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class Jpa extends KeycloakModelParameters {
+
+ static final Set> ALLOWED_SPIS = ImmutableSet.>builder()
+ // jpa-specific
+ .add(DBLockSpi.class)
+ .add(JpaConnectionSpi.class)
+ .add(JpaUpdaterSpi.class)
+ .add(LiquibaseConnectionSpi.class)
+
+ .build();
+
+ static final Set> ALLOWED_FACTORIES = ImmutableSet.>builder()
+ // jpa-specific
+ .add(DefaultJpaConnectionProviderFactory.class)
+ .add(JPAAuthorizationStoreFactory.class)
+ .add(JpaClientProviderFactory.class)
+ .add(JpaEventStoreProviderFactory.class)
+ .add(JpaGroupProviderFactory.class)
+ .add(JpaRealmProviderFactory.class)
+ .add(JpaRoleProviderFactory.class)
+ .add(JpaUpdaterProviderFactory.class)
+ .add(JpaUserProviderFactory.class)
+ .add(LiquibaseConnectionProviderFactory.class)
+ .add(LiquibaseDBLockProviderFactory.class)
+ .build();
+
+ public Jpa() {
+ super(ALLOWED_SPIS, ALLOWED_FACTORIES);
+ }
+
+}
diff --git a/testsuite/model/src/test/java/org/keycloak/model/parameters/JpaFederation.java b/testsuite/model/src/test/java/org/keycloak/model/parameters/JpaFederation.java
new file mode 100644
index 0000000000..8e7a422eaf
--- /dev/null
+++ b/testsuite/model/src/test/java/org/keycloak/model/parameters/JpaFederation.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2020 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.
+ */
+package org.keycloak.model.parameters;
+
+import org.keycloak.model.KeycloakModelParameters;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.provider.Spi;
+import org.keycloak.storage.UserStorageProviderSpi;
+import org.keycloak.storage.federated.UserFederatedStorageProviderSpi;
+import org.keycloak.storage.jpa.JpaUserFederatedStorageProviderFactory;
+import org.keycloak.testsuite.federation.BackwardsCompatibilityUserStorageFactory;
+import com.google.common.collect.ImmutableSet;
+import java.util.Set;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class JpaFederation extends KeycloakModelParameters {
+
+ static final Set> ALLOWED_SPIS = ImmutableSet.>builder()
+ .addAll(Jpa.ALLOWED_SPIS)
+ .add(UserStorageProviderSpi.class)
+ .add(UserFederatedStorageProviderSpi.class)
+
+ .build();
+
+ static final Set> ALLOWED_FACTORIES = ImmutableSet.>builder()
+ .addAll(Jpa.ALLOWED_FACTORIES)
+ .add(JpaUserFederatedStorageProviderFactory.class)
+ .build();
+
+ public JpaFederation() {
+ super(ALLOWED_SPIS, ALLOWED_FACTORIES);
+ }
+}
diff --git a/testsuite/model/src/test/java/org/keycloak/model/parameters/Map.java b/testsuite/model/src/test/java/org/keycloak/model/parameters/Map.java
new file mode 100644
index 0000000000..1fe04a8643
--- /dev/null
+++ b/testsuite/model/src/test/java/org/keycloak/model/parameters/Map.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2020 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.
+ */
+package org.keycloak.model.parameters;
+
+import org.keycloak.model.KeycloakModelParameters;
+import org.keycloak.models.map.client.MapClientProviderFactory;
+import org.keycloak.models.map.group.MapGroupProviderFactory;
+import org.keycloak.models.map.role.MapRoleProviderFactory;
+import org.keycloak.models.map.storage.MapStorageProvider;
+import org.keycloak.models.map.storage.MapStorageSpi;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.provider.Spi;
+import com.google.common.collect.ImmutableSet;
+import java.util.Set;
+
+/**
+ *
+ * @author hmlnarik
+ */
+public class Map extends KeycloakModelParameters {
+
+ static final Set> ALLOWED_SPIS = ImmutableSet.>builder()
+ .add(MapStorageSpi.class)
+
+ .build();
+
+ static final Set> ALLOWED_FACTORIES = ImmutableSet.>builder()
+ .add(MapClientProviderFactory.class)
+ .add(MapGroupProviderFactory.class)
+ .add(MapRoleProviderFactory.class)
+ .add(MapStorageProvider.class)
+ .build();
+
+ public Map() {
+ super(ALLOWED_SPIS, ALLOWED_FACTORIES);
+ }
+}
diff --git a/model/jpa/src/test/resources/log4j.properties b/testsuite/model/src/test/resources/log4j.properties
similarity index 82%
rename from model/jpa/src/test/resources/log4j.properties
rename to testsuite/model/src/test/resources/log4j.properties
index 2d89ff6a06..1d0c1c002a 100644
--- a/model/jpa/src/test/resources/log4j.properties
+++ b/testsuite/model/src/test/resources/log4j.properties
@@ -19,18 +19,16 @@ log4j.rootLogger=info, keycloak
log4j.appender.keycloak=org.apache.log4j.ConsoleAppender
log4j.appender.keycloak.layout=org.apache.log4j.EnhancedPatternLayout
-keycloak.testsuite.logging.pattern=%d{HH:mm:ss,SSS} %-5p [%c] %m%n
+keycloak.testsuite.logging.pattern=%d{HH:mm:ss,SSS} %-5p %t [%c] %m%n
log4j.appender.keycloak.layout.ConversionPattern=${keycloak.testsuite.logging.pattern}
# Logging with "info" when running test from IDE, but disabled when running test with "mvn" . Both cases can be overriden by use system property "keycloak.logging.level" (eg. -Dkeycloak.logging.level=debug )
-log4j.logger.org.keycloak=${keycloak.logging.level:debug}
-
-keycloak.testsuite.logging.level=debug
-log4j.logger.org.keycloak.testsuite=${keycloak.testsuite.logging.level}
+log4j.logger.org.keycloak.models=debug
+# log4j.logger.org.hibernate=debug
# Enable to view loaded SPI and Providers
-log4j.logger.org.keycloak.services.DefaultKeycloakSessionFactory=debug
-log4j.logger.org.keycloak.provider.ProviderManager=debug
+# log4j.logger.org.keycloak.services.DefaultKeycloakSessionFactory=debug
+# log4j.logger.org.keycloak.provider.ProviderManager=debug
# log4j.logger.org.keycloak.provider.FileSystemProviderLoaderFactory=debug
# Liquibase updates logged with "info" by default. Logging level can be changed by system property "keycloak.liquibase.logging.level"
diff --git a/testsuite/pom.xml b/testsuite/pom.xml
index 5e4d8f9049..76eae031dd 100755
--- a/testsuite/pom.xml
+++ b/testsuite/pom.xml
@@ -45,6 +45,7 @@
db-allocator-plugin
integration-arquillian
+ model
utils