keycloak-uncached

KEYCLOAK-6541 base changes

4/18/2018 8:31:04 AM

Changes

Details

diff --git a/testsuite/integration-arquillian/HOW-TO-RUN.md b/testsuite/integration-arquillian/HOW-TO-RUN.md
index 2d9824d..149a66e 100644
--- a/testsuite/integration-arquillian/HOW-TO-RUN.md
+++ b/testsuite/integration-arquillian/HOW-TO-RUN.md
@@ -77,18 +77,15 @@ TODO: Add info about Wildfly logging
 
 ## Run adapter tests
 
-### Wildfly
+### Undertow
+    mvn -f testsuite/integration-arquillian/tests/base/pom.xml \
+        -Dtest=org.keycloak.testsuite.adapter.**.*Test
 
+### Wildfly
     
-    # Prepare servers
-    mvn -f testsuite/integration-arquillian/servers/pom.xml clean install \
-       -Pauth-server-wildfly \
-       -Papp-server-wildfly
-
     # Run tests
-    mvn -f testsuite/integration-arquillian/tests/other/adapters/jboss/wildfly/pom.xml \
+    mvn -f testsuite/integration-arquillian/pom.xml \
        clean install \
-       -Pauth-server-wildfly \
        -Papp-server-wildfly
     
 
diff --git a/testsuite/integration-arquillian/pom.xml b/testsuite/integration-arquillian/pom.xml
index 70191c4..1c7a710 100644
--- a/testsuite/integration-arquillian/pom.xml
+++ b/testsuite/integration-arquillian/pom.xml
@@ -41,15 +41,15 @@
         <app.server.java.home>${java.home}</app.server.java.home>
 
         <!--component versions-->
-        <arquillian-core.version>1.1.13.Final</arquillian-core.version>
-        <selenium.version>3.5.3</selenium.version>
-        <arquillian-drone.version>2.4.2</arquillian-drone.version>
-        <arquillian-graphene.version>2.3.1</arquillian-graphene.version>
+        <!--to update arquillian-core to 1.3.0.Final or higher see https://issues.jboss.org/browse/ARQ-2181 -->
+        <arquillian-core.version>1.2.1.Final</arquillian-core.version>
+        <selenium.version>3.11.0</selenium.version>
+        <arquillian-drone.version>2.5.1</arquillian-drone.version>
+        <arquillian-graphene.version>2.3.2</arquillian-graphene.version>
         <arquillian-wildfly-container.version>2.1.0.Final</arquillian-wildfly-container.version>
         <arquillian-wls-container.version>1.0.1.Final</arquillian-wls-container.version>
         <arquillian-container-karaf.version>2.2.0.Final</arquillian-container-karaf.version>
         <arquillian-infinispan-container.version>1.2.0.Beta2</arquillian-infinispan-container.version>
-        <version.shrinkwrap.resolvers>2.2.6</version.shrinkwrap.resolvers>
         <undertow-embedded.version>1.0.0.Alpha2</undertow-embedded.version>
         <version.org.wildfly.extras.creaper>1.6.1</version.org.wildfly.extras.creaper>
         <testcontainers.version>1.5.1</testcontainers.version>
diff --git a/testsuite/integration-arquillian/README.md b/testsuite/integration-arquillian/README.md
index c364f04..21b19e4 100644
--- a/testsuite/integration-arquillian/README.md
+++ b/testsuite/integration-arquillian/README.md
@@ -46,16 +46,14 @@ ifconfig lo multicast
 
 Lifecycle of application server is always tied to a particular TestClass.
 
-Each *adapter* test class is annotated by `@AppServerContainer("app-server-*")` annotation 
-that links it to a particular Arquillian container in `arquillian.xml`.
-The `AppServerTestEnricher` then ensures the server is started during `BeforeClass` event and stopped during `AfterClass` event for that particular test class. 
-In case the `@AppServerContainer` annotation has no value it's assumed that the application container 
-is the same as the auth server container - a "relative" adapter test scenario.
+Each *adapter* test class is annotated by one or more `@AppServerContainer("app-server-*")` annotations
+that links it to a particular Arquillian container.
+The `AppServerTestEnricher` then ensures the corresponding server is started during `BeforeClass` event and stopped during `AfterClass` event for that particular test class. 
 
-The app-servers with installed Keycloak adapter are prepared in `servers/app-server` submodules, activated by `-Papp-server-MODULE`.
+The app-servers with installed Keycloak adapter are prepared in `servers/app-server` submodules, activated by `-Papp-server-MODULE` or `-Dapp.server=MODULE`
 [More details.](servers/app-server/README.md)
 
-The corresponding adapter test modules are in `tests/other/adapters` submodules, and are activated by the same profiles.
+NOTE: Some corresponding adapter test modules are in `tests/other/adapters` submodules, and are activated by the same profiles. It will be tranferred into base testsuite.
 
 ## SuiteContext and TestContext
 
@@ -104,13 +102,6 @@ The other test modules depend on this module.
 Tests for Keycloak Admin Console are located in a separate module `tests/other/console` 
 and are **disabled** by default. Can be enabled by `-Pconsole-ui-tests`.
 
-### Adapter Tests
-
-Adapter tests are located in submodules of the `tests/other/adapters` module.
-
-They are **disabled** by default; they can be enabled by corresponding profiles.
-Multiple profiles can be enabled for a single test execution.
-
 #### Types of adapter tests
 
 1. Using *custom test servlets*
@@ -173,7 +164,7 @@ integration-arquillian
    │
    └──other   (common settings for all test modules dependent on base)
       │
-      ├──adapters         (common settings for all adapter test modules)
+      ├──adapters         (common settings for all adapter test modules - will be moved into base)
       │  ├──jboss
       │  ├──tomcat
       │  └──karaf
diff --git a/testsuite/integration-arquillian/servers/app-server/app-server-spi/pom.xml b/testsuite/integration-arquillian/servers/app-server/app-server-spi/pom.xml
new file mode 100644
index 0000000..83c997c
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/app-server/app-server-spi/pom.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0"?>
+<!--
+~ Copyright 2018 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 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+    <parent>
+        <groupId>org.keycloak.testsuite</groupId>
+        <artifactId>integration-arquillian-servers-app-server</artifactId>
+        <version>4.0.0.Final-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>integration-arquillian-servers-app-server-spi</artifactId>
+    <packaging>jar</packaging>
+    <name>App Server - SPI</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.jboss.arquillian.protocol</groupId>
+            <artifactId>arquillian-protocol-servlet</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/testsuite/integration-arquillian/servers/app-server/app-server-spi/src/main/java/org/keycloak/testsuite/arquillian/container/AppServerContainerService.java b/testsuite/integration-arquillian/servers/app-server/app-server-spi/src/main/java/org/keycloak/testsuite/arquillian/container/AppServerContainerService.java
new file mode 100644
index 0000000..986c020
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/app-server/app-server-spi/src/main/java/org/keycloak/testsuite/arquillian/container/AppServerContainerService.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2018 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.testsuite.arquillian.container;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ServiceConfigurationError;
+import java.util.ServiceLoader;
+import org.jboss.shrinkwrap.descriptor.spi.node.Node;
+
+/**
+ * @author <a href="mailto:vramik@redhat.com">Vlasta Ramik</a>
+ */
+public class AppServerContainerService  {
+
+    private static AppServerContainerService service;
+    private final ServiceLoader<AppServerContainerSPI> loader;
+
+    private AppServerContainerService() {
+        loader = ServiceLoader.load(AppServerContainerSPI.class);
+    }
+
+    public static synchronized AppServerContainerService getInstance() {
+        if (service == null) {
+            service = new AppServerContainerService();
+        }
+        return service;
+    }
+
+    public List<Node> getContainers(String appServerName) {
+        List<Node> containers = null;
+        try {
+            Iterator<AppServerContainerSPI> definitions = loader.iterator();
+
+            List<AppServerContainerSPI> availableDefinitions = new ArrayList<>();
+            while (definitions != null && definitions.hasNext()) {
+                availableDefinitions.add(definitions.next());
+            }
+            for (AppServerContainerSPI def : availableDefinitions) {
+                if (def.getName().equals(appServerName)) {
+                    containers = def.getContainers();
+                }
+            }
+        } catch (ServiceConfigurationError serviceError) {
+            containers = null;
+            throw serviceError;
+        }
+        return containers;
+    }
+}
diff --git a/testsuite/integration-arquillian/servers/app-server/app-server-spi/src/main/java/org/keycloak/testsuite/arquillian/container/AppServerContainerSPI.java b/testsuite/integration-arquillian/servers/app-server/app-server-spi/src/main/java/org/keycloak/testsuite/arquillian/container/AppServerContainerSPI.java
new file mode 100644
index 0000000..3fdb55f
--- /dev/null
+++ b/testsuite/integration-arquillian/servers/app-server/app-server-spi/src/main/java/org/keycloak/testsuite/arquillian/container/AppServerContainerSPI.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2018 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.testsuite.arquillian.container;
+
+import java.util.List;
+import org.jboss.shrinkwrap.descriptor.spi.node.Node;
+
+/**
+ * @author <a href="mailto:vramik@redhat.com">Vlasta Ramik</a>
+ */
+public interface AppServerContainerSPI  {
+
+    public static final String APP_SERVER = "app-server";
+
+    /**
+     * @return string name of container
+     */
+    public String getName();
+
+    /**
+     * @return List of available containers or null if there are none
+     */
+    public List<Node> getContainers();
+}
diff --git a/testsuite/integration-arquillian/servers/app-server/pom.xml b/testsuite/integration-arquillian/servers/app-server/pom.xml
index 4604c26..b97e0bf 100644
--- a/testsuite/integration-arquillian/servers/app-server/pom.xml
+++ b/testsuite/integration-arquillian/servers/app-server/pom.xml
@@ -36,6 +36,7 @@
     </properties>
 
     <modules>
+        <module>app-server-spi</module>
         <module>jboss</module>
         <module>karaf</module>
         <module>tomcat</module>
diff --git a/testsuite/integration-arquillian/servers/app-server/README.md b/testsuite/integration-arquillian/servers/app-server/README.md
index e6a3354..ba60e85 100644
--- a/testsuite/integration-arquillian/servers/app-server/README.md
+++ b/testsuite/integration-arquillian/servers/app-server/README.md
@@ -13,10 +13,9 @@ Submodules are enabled with profiles: `-Papp-server-MODULE`
 
 ### Modules
 
-* __`as7` JBossAS 7__
-* __`wildfly8` Wildfly 8__
 * __`wildfly9` Wildfly 9__
-* __`wildfly` Wildfly 10__
+* __`wildfly10` Wildfly 10__
+* __`wildfly` Wildfly 11__
 * __`eap6` EAP 6__ Requires access to EAP product repo, or setting `-Deap6.version` to public EAP 6 Alpha.
 * __`eap` EAP 7__ Requires access to EAP product repo.
 * __`relative`__ Activate with `-Papp-server-relative`.
diff --git a/testsuite/integration-arquillian/tests/base/pom.xml b/testsuite/integration-arquillian/tests/base/pom.xml
index 9580db8..74fd0a7 100644
--- a/testsuite/integration-arquillian/tests/base/pom.xml
+++ b/testsuite/integration-arquillian/tests/base/pom.xml
@@ -45,8 +45,8 @@
         <exclude.x509>**/x509/*Test.java</exclude.x509>
         <!-- KEYCLOAK-6771 exclude Mutual TLS Holder of Key Token x509 tests by default, enabled by 'ssl' profile -->
         <exclude.HoK>**/hok/**/*Test.java</exclude.HoK>
-        <!-- exclude undertow adapter tests. They can be added by -Dtest=org.keycloak.testsuite.adapter.undertow.**.*Test -->
-        <exclude.undertow.adapter>**/adapter/undertow/**/*Test.java</exclude.undertow.adapter>
+        <!-- see include-CORS-tests profile -->
+        <exclude.cors.tests>**/cors/*Test.java</exclude.cors.tests>
     </properties>
     
     <dependencies>
@@ -121,6 +121,11 @@
             <groupId>com.google.guava</groupId>
             <artifactId>guava</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.keycloak.testsuite</groupId>
+            <artifactId>integration-arquillian-servers-app-server-spi</artifactId>
+            <version>${project.version}</version>
+        </dependency>
     </dependencies>
     
     <build>
@@ -162,6 +167,7 @@
                         <exclude>${exclude.crossdc}</exclude>
                         <exclude>${exclude.undertow.adapter}</exclude>
                         <exclude>${exclude.x509}</exclude>
+                        <exclude>${exclude.cors.tests}</exclude>
                         <exclude>${exclude.HoK}</exclude>
                     </excludes>
                 </configuration>
@@ -330,7 +336,25 @@
                 </kie.maven.settings>
             </properties>
         </profile>
-        
+        <profile>
+            <id>include-CORS-tests</id>
+            <!--
+            If you want to run CORS tests it is necessary to put
+
+            127.0.0.1 localhost-auth
+            127.0.0.1 localhost-db
+
+            to your /etc/hosts file
+            -->
+            <activation>
+                <property>
+                    <name>includeCorsTests</name>
+                </property>
+            </activation>
+            <properties>
+                <exclude.cors.tests>-</exclude.cors.tests>
+            </properties>
+        </profile>
     </profiles>
 
 </project>
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AdapterTestExecutionDecider.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AdapterTestExecutionDecider.java
new file mode 100644
index 0000000..b665781
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AdapterTestExecutionDecider.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2018 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.testsuite.arquillian;
+
+import java.lang.reflect.Method;
+import org.jboss.arquillian.test.spi.execution.ExecutionDecision;
+import org.jboss.arquillian.test.spi.execution.TestExecutionDecider;
+import org.jboss.arquillian.core.api.Instance;
+import org.jboss.arquillian.core.api.annotation.Inject;
+import org.jboss.logging.Logger;
+
+/**
+ * @author <a href="mailto:vramik@redhat.com">Vlastislav Ramik</a>
+ */
+public class AdapterTestExecutionDecider implements TestExecutionDecider {
+
+    private final Logger log = Logger.getLogger(AdapterTestExecutionDecider.class);
+
+    @Inject private Instance<TestContext> testContextInstance;
+
+    @Override
+    public ExecutionDecision decide(Method method) {
+        TestContext testContext = testContextInstance.get();
+        if (!testContext.isAdapterTest()) return ExecutionDecision.execute();
+        if (testContext.isAdapterContainerEnabled() || testContext.isAdapterContainerEnabledCluster()) {
+            return ExecutionDecision.execute();
+        } else {
+            log.debug("Skipping test: Not enabled by @AppServerContainer annotations.");
+            return ExecutionDecision.dontExecute("Not enabled by @AppServerContainer annotations.");
+        }
+    }
+
+    @Override
+    public int precedence() {
+        return 1;
+    }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/AppServerContainer.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/AppServerContainer.java
index 8be428f..647d32b 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/AppServerContainer.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/AppServerContainer.java
@@ -19,6 +19,7 @@ package org.keycloak.testsuite.arquillian.annotation;
 
 import java.lang.annotation.Documented;
 import java.lang.annotation.ElementType;
+import java.lang.annotation.Repeatable;
 import java.lang.annotation.Retention;
 import java.lang.annotation.Target;
 
@@ -31,7 +32,7 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME;
 @Documented
 @Retention(RUNTIME)
 @Target({ElementType.TYPE})
-public @interface AppServerContainer 
-{
-   String value() default "";
-}
\ No newline at end of file
+@Repeatable(AppServerContainers.class)
+public @interface AppServerContainer {
+    String value();
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/AppServerContainers.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/AppServerContainers.java
new file mode 100644
index 0000000..887ace8
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/annotation/AppServerContainers.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2018 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.testsuite.arquillian.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * Holder for @AppServerContainer annotations
+ * 
+ */
+@Documented
+@Retention(RUNTIME)
+@Target({ElementType.TYPE})
+public @interface AppServerContainers {
+    AppServerContainer[] value();
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AppServerTestEnricher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AppServerTestEnricher.java
index cbd922e..6bb1e84 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AppServerTestEnricher.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AppServerTestEnricher.java
@@ -2,13 +2,12 @@ package org.keycloak.testsuite.arquillian;
 
 import org.jboss.arquillian.container.test.api.ContainerController;
 import org.jboss.arquillian.core.api.Instance;
-import org.jboss.arquillian.core.api.InstanceProducer;
 import org.jboss.arquillian.core.api.annotation.Inject;
 import org.jboss.arquillian.core.api.annotation.Observes;
-import org.jboss.arquillian.test.spi.annotation.ClassScoped;
 import org.jboss.arquillian.test.spi.event.suite.BeforeClass;
 import org.jboss.logging.Logger;
 import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
+import org.keycloak.testsuite.arquillian.annotation.AppServerContainers;
 import org.wildfly.extras.creaper.core.ManagementClient;
 import org.wildfly.extras.creaper.core.online.ManagementProtocol;
 import org.wildfly.extras.creaper.core.online.OnlineManagementClient;
@@ -17,6 +16,10 @@ import org.wildfly.extras.creaper.core.online.OnlineOptions;
 import java.io.IOException;
 import java.net.MalformedURLException;
 import java.net.URL;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
 
 import static org.keycloak.testsuite.arquillian.AuthServerTestEnricher.getAuthServerContextRoot;
 
@@ -28,21 +31,25 @@ public class AppServerTestEnricher {
 
     protected final Logger log = Logger.getLogger(this.getClass());
 
-    @Inject
-    @ClassScoped
-    private InstanceProducer<TestContext> testContextProducer;
-    private TestContext testContext;
+    public static final String APP_SERVER_PREFIX = "app-server-";
+    public static final String CURRENT_APP_SERVER = System.getProperty("app.server", "undertow");
 
-    public static String getAppServerQualifier(Class testClass) {
-        Class<? extends AuthServerTestEnricher> annotatedClass = getNearestSuperclassWithAnnotation(testClass, AppServerContainer.class);
+    @Inject private Instance<ContainerController> containerConrollerInstance;
+    @Inject private Instance<TestContext> testContextInstance;
+    private TestContext testContext;
 
-        String appServerQ = (annotatedClass == null ? null
-                : annotatedClass.getAnnotation(AppServerContainer.class).value());
+    public static List<String> getAppServerQualifiers(Class testClass) {
+        Class<?> annotatedClass = getNearestSuperclassWithAppServerAnnotation(testClass);
 
-        return annotatedClass == null ? null // no @AppServerContainer annotation --> no adapter test
-                : (appServerQ == null || appServerQ.isEmpty() // @AppServerContainer annotation present but qualifier not set --> relative adapter test
-                        ? AuthServerTestEnricher.AUTH_SERVER_CONTAINER // app server == auth server
-                        : appServerQ);
+        if (annotatedClass == null) return null; // no @AppServerContainer annotation --> no adapter test
+        
+        AppServerContainer[] appServerContainers = annotatedClass.getAnnotationsByType(AppServerContainer.class);
+        
+        List<String> appServerQualifiers = new ArrayList<>();
+        for (AppServerContainer appServerContainer : appServerContainers) {
+            appServerQualifiers.add(appServerContainer.value());
+        }
+        return appServerQualifiers;
     }
 
     public static String getAppServerContextRoot() {
@@ -62,11 +69,32 @@ public class AppServerTestEnricher {
     }
 
     public void updateTestContextWithAppServerInfo(@Observes(precedence = 1) BeforeClass event) {
-        testContext = testContextProducer.get();
-        String appServerQualifier = getAppServerQualifier(testContext.getTestClass());
-        for (ContainerInfo container : testContext.getSuiteContext().getContainers()) {
-            if (container.getQualifier().equals(appServerQualifier)) {
-                testContext.setAppServerInfo(updateWithAppServerInfo(container));
+        testContext = testContextInstance.get();
+
+        List<String> appServerQualifiers = getAppServerQualifiers(testContext.getTestClass());
+        if (appServerQualifiers == null) { // no adapter test
+            log.info("\n\n" + testContext);
+            return;
+        } 
+
+        String appServerQualifier = null;
+        for (String qualifier : appServerQualifiers) {
+            if (qualifier.contains(";")) {// cluster adapter test
+                final List<String> appServers = Arrays.asList(qualifier.split("\\s*;\\s*"));
+                List<ContainerInfo> appServerBackendsInfo = testContext.getSuiteContext().getContainers().stream()
+                    .filter(ci -> appServers.contains(ci.getQualifier()))
+                    .map(this::updateWithAppServerInfo)
+                    .collect(Collectors.toList());
+                testContext.setAppServerBackendsInfo(appServerBackendsInfo);
+            } else {// non-cluster adapter test
+                for (ContainerInfo container : testContext.getSuiteContext().getContainers()) {
+                    if (container.getQualifier().equals(qualifier)) {
+                        testContext.setAppServerInfo(updateWithAppServerInfo(container));
+                        appServerQualifier = qualifier;
+                        break;
+                    }
+                    //TODO add warning if there are two or more matching containers.
+                }
             }
         }
         // validate app server
@@ -83,7 +111,7 @@ public class AppServerTestEnricher {
     private ContainerInfo updateWithAppServerInfo(ContainerInfo appServerInfo, int clusterPortOffset) {
         try {
 
-            String appServerContextRootStr = isRelative(testContext.getTestClass())
+            String appServerContextRootStr = isRelative()
                     ? getAuthServerContextRoot(clusterPortOffset)
                     : getAppServerContextRoot(clusterPortOffset);
 
@@ -110,12 +138,9 @@ public class AppServerTestEnricher {
 
         return managementClient;
     }
-
-    @Inject
-    private Instance<ContainerController> containerConrollerInstance;
-
+    
     public void startAppServer(@Observes(precedence = -1) BeforeClass event) throws MalformedURLException, InterruptedException, IOException {
-        if (testContext.isAdapterTest() && !testContext.isRelativeAdapterTest()) {
+        if (testContext.isAdapterContainerEnabled() && !testContext.isRelativeAdapterTest()) {
             ContainerController controller = containerConrollerInstance.get();
             if (!controller.isStarted(testContext.getAppServerInfo().getQualifier())) {
                 log.info("Starting app server: " + testContext.getAppServerInfo().getQualifier());
@@ -131,39 +156,42 @@ public class AppServerTestEnricher {
      * @return testClass or the nearest superclass of testClass annotated with
      * annotationClass
      */
-    public static Class getNearestSuperclassWithAnnotation(Class testClass, Class annotationClass) {
-        return testClass.isAnnotationPresent(annotationClass) ? testClass
+    private static Class getNearestSuperclassWithAppServerAnnotation(Class<?> testClass) {
+        return (testClass.isAnnotationPresent(AppServerContainer.class) || testClass.isAnnotationPresent(AppServerContainers.class)) ? testClass
                 : (testClass.getSuperclass().equals(Object.class) ? null // stop recursion
-                : getNearestSuperclassWithAnnotation(testClass.getSuperclass(), annotationClass)); // continue recursion
+                : getNearestSuperclassWithAppServerAnnotation(testClass.getSuperclass())); // continue recursion
     }
 
     public static boolean hasAppServerContainerAnnotation(Class testClass) {
-        return getNearestSuperclassWithAnnotation(testClass, AppServerContainer.class) != null;
+        return getNearestSuperclassWithAppServerAnnotation(testClass) != null;
+    }
+
+    public static boolean isUndertowAppServer() {
+        return CURRENT_APP_SERVER.equals("undertow");
     }
 
-    public static boolean isRelative(Class testClass) {
-        return getAppServerQualifier(testClass).equals(AuthServerTestEnricher.AUTH_SERVER_CONTAINER);
+    public static boolean isRelative() {
+        return CURRENT_APP_SERVER.equals("relative");
     }
 
-    public static boolean isWildflyAppServer(Class testClass) {
-        return getAppServerQualifier(testClass).contains("wildfly");
+    public static boolean isWildflyAppServer() {
+        return CURRENT_APP_SERVER.equals("wildfly");
     }
 
-    public static boolean isTomcatAppServer(Class testClass) {
-        return getAppServerQualifier(testClass).contains("tomcat");
+    public static boolean isTomcatAppServer() {
+        return CURRENT_APP_SERVER.equals("tomcat");
     }
 
-    public static boolean isWASAppServer(Class testClass) {
-        return getAppServerQualifier(testClass).contains("was");
+    public static boolean isWASAppServer() {
+        return CURRENT_APP_SERVER.equals("was");
     }
 
-    public static boolean isWLSAppServer(Class testClass) {
-        return getAppServerQualifier(testClass).contains("wls");
+    public static boolean isWLSAppServer() {
+        return CURRENT_APP_SERVER.equals("wls");
     }
 
-    public static boolean isOSGiAppServer(Class testClass) {
-        String q = getAppServerQualifier(testClass);
-        return q.contains("karaf") || q.contains("fuse");
+    public static boolean isOSGiAppServer() {
+        return CURRENT_APP_SERVER.contains("karaf") || CURRENT_APP_SERVER.contains("fuse");
     }
 
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java
index 4ae26e7..afa2a87 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/AuthServerTestEnricher.java
@@ -49,6 +49,7 @@ import java.util.Set;
 
 import java.util.stream.Collectors;
 import javax.ws.rs.NotFoundException;
+import org.jboss.arquillian.core.api.annotation.ApplicationScoped;
 
 /**
  *
@@ -97,7 +98,7 @@ public class AuthServerTestEnricher {
     private SuiteContext suiteContext;
 
     @Inject
-    @ClassScoped
+    @ApplicationScoped // needed in AdapterTestExecutionDecider
     private InstanceProducer<TestContext> testContextProducer;
 
     @Inject
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/ContainerInfo.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/ContainerInfo.java
index 505c104..9b08d80 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/ContainerInfo.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/ContainerInfo.java
@@ -13,7 +13,7 @@ import java.util.Objects;
  *
  * @author tkyjovsk
  */
-public class ContainerInfo {
+public class ContainerInfo implements Comparable<ContainerInfo> {
 
     private URL contextRoot;
     private Container arquillianContainer;
@@ -116,4 +116,9 @@ public class ContainerInfo {
         return Objects.equals(arquillianContainer.getContainerConfiguration().getMode(), "manual");
     }
 
+    @Override
+    public int compareTo(ContainerInfo o) {
+        return this.getQualifier().compareTo(o.getQualifier());
+    }
+
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/RegistryCreator.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/RegistryCreator.java
index 41278fc..6269d36 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/RegistryCreator.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/containers/RegistryCreator.java
@@ -19,6 +19,8 @@ package org.keycloak.testsuite.arquillian.containers;
 import org.jboss.arquillian.config.descriptor.api.ArquillianDescriptor;
 import org.jboss.arquillian.config.descriptor.api.ContainerDef;
 import org.jboss.arquillian.config.descriptor.api.GroupDef;
+import org.jboss.arquillian.config.descriptor.impl.ContainerDefImpl;
+import org.jboss.arquillian.config.descriptor.impl.GroupDefImpl;
 import org.jboss.arquillian.container.spi.ContainerRegistry;
 import org.jboss.arquillian.container.spi.client.container.DeployableContainer;
 import org.jboss.arquillian.core.api.Injector;
@@ -30,12 +32,14 @@ import org.jboss.arquillian.core.api.annotation.Observes;
 import org.jboss.arquillian.core.spi.ServiceLoader;
 import org.jboss.arquillian.core.spi.Validate;
 import org.jboss.logging.Logger;
-
+import org.jboss.shrinkwrap.descriptor.spi.node.Node;
+import org.jboss.shrinkwrap.descriptor.spi.node.NodeDescriptor;
+import org.keycloak.testsuite.arquillian.container.AppServerContainerService;
+import org.mvel2.MVEL;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 
-import org.mvel2.MVEL;
 import static org.keycloak.testsuite.arquillian.containers.SecurityActions.isClassPresent;
 import static org.keycloak.testsuite.arquillian.containers.SecurityActions.loadClass;
 
@@ -50,6 +54,8 @@ import static org.keycloak.testsuite.arquillian.containers.SecurityActions.loadC
 public class RegistryCreator {
 
     protected final Logger log = Logger.getLogger(this.getClass());
+    public static final String ADAPTER_IMPL_CONFIG_STRING = "adapterImplClass";
+    private static final String ENABLED = "enabled";
 
     @Inject
     @ApplicationScoped
@@ -74,18 +80,23 @@ public class RegistryCreator {
             throw new IllegalStateException("There are not any container adapters on the classpath");
         }
 
-        createRegistry(event.getContainers(), containers, reg, serviceLoader);
+        List<ContainerDef> containersDefs = event.getContainers();//arquillian.xml
+        List<GroupDef> groupDefs = event.getGroups();//arquillian.xml
+
+        addAppServerContainers(containersDefs, groupDefs);//dynamically loaded containers/groups
 
-        for (GroupDef group : event.getGroups()) {
-            createRegistry(group.getGroupContainers(), containers, reg, serviceLoader);
+        createRegistry(containersDefs, reg, serviceLoader);
+
+        for (GroupDef group : groupDefs) {
+            createRegistry(group.getGroupContainers(), reg, serviceLoader);
         }
 
         registry.set(reg);
     }
 
-    private void createRegistry(List<ContainerDef> containerDefs, Collection<DeployableContainer> containers, ContainerRegistry reg, ServiceLoader serviceLoader) {
+    private void createRegistry(List<ContainerDef> containerDefs, ContainerRegistry reg, ServiceLoader serviceLoader) {
         for (ContainerDef container : containerDefs) {
-            if (isCreatingContainer(container, containers)) {
+            if (isAdapterImplClassAvailable(container)) {
                 if (isEnabled(container)) {
                     log.info("Registering container: " + container.getContainerName());
                     reg.create(container, serviceLoader);
@@ -96,7 +107,20 @@ public class RegistryCreator {
         }
     }
 
-    private static final String ENABLED = "enabled";
+    private void addAppServerContainers(List<ContainerDef> containerDefs, List<GroupDef> groupDefs) {
+        Node parent = ((NodeDescriptor)containerDefs.get(0)).getRootNode();
+
+        String appServerName = System.getProperty("app.server", "undertow");
+
+        List<Node> containers = AppServerContainerService.getInstance().getContainers(appServerName);
+        for (Node container : containers) {
+            if (container.getName().equals("container")) {
+                containerDefs.add(new ContainerDefImpl("arquillian.xml", parent, container));
+            } else if (container.getName().equals("group")) {
+                groupDefs.add(new GroupDefImpl("arquillian.xml", parent, container));
+            }
+        }
+    }
 
     private static boolean isEnabled(ContainerDef containerDef) {
         Map<String, String> props = containerDef.getContainerProperties();
@@ -109,7 +133,7 @@ public class RegistryCreator {
     }
 
     @SuppressWarnings("rawtypes")
-    private boolean isCreatingContainer(ContainerDef containerDef, Collection<DeployableContainer> containers) {
+    private boolean isAdapterImplClassAvailable(ContainerDef containerDef) {
 
         if (hasAdapterImplClassProperty(containerDef)) {
             if (isClassPresent(getAdapterImplClassValue(containerDef))) {
@@ -135,8 +159,7 @@ public class RegistryCreator {
     public static String getAdapterImplClassValue(ContainerDef containerDef) {
         return containerDef.getContainerProperties().get(ADAPTER_IMPL_CONFIG_STRING).trim();
     }
-    public static final String ADAPTER_IMPL_CONFIG_STRING = "adapterImplClass";
-
+    
     @SuppressWarnings("rawtypes")
     public static DeployableContainer<?> getContainerAdapter(String adapterImplClass, Collection<DeployableContainer> containers) {
         Validate.notNullOrEmpty(adapterImplClass, "The value of " + ADAPTER_IMPL_CONFIG_STRING + " can not be a null object "
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/DeploymentArchiveProcessor.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/DeploymentArchiveProcessor.java
index 1724b26..81a16a9 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/DeploymentArchiveProcessor.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/DeploymentArchiveProcessor.java
@@ -42,7 +42,6 @@ import org.keycloak.testsuite.util.IOUtil;
 import org.keycloak.util.JsonSerialization;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
-import org.w3c.dom.NodeList;
 
 import java.io.File;
 import java.io.IOException;
@@ -107,7 +106,7 @@ public class DeploymentArchiveProcessor implements ApplicationArchiveProcessor {
 //        } else {
 //            log.info(testClass.getJavaClass().getSimpleName() + " is not an AdapterTest");
 //        }
-        if (isWLSAppServer(testClass.getJavaClass())) {
+        if (isWLSAppServer()) {
 //        {
             MavenResolverSystem resolver = Maven.resolver();
             MavenFormatStage dependencies = resolver
@@ -122,7 +121,7 @@ public class DeploymentArchiveProcessor implements ApplicationArchiveProcessor {
                     .addClass(org.keycloak.testsuite.arquillian.annotation.UseServletFilter.class);
         }
 
-        if (isWASAppServer(testClass.getJavaClass())) {
+        if (isWASAppServer()) {
 //        {
             MavenResolverSystem resolver = Maven.resolver();
             MavenFormatStage dependencies = resolver
@@ -146,7 +145,7 @@ public class DeploymentArchiveProcessor implements ApplicationArchiveProcessor {
     }
 
     protected void modifyAdapterConfigs(Archive<?> archive, TestClass testClass) {
-        boolean relative = isRelative(testClass.getJavaClass());
+        boolean relative = isRelative();
         modifyAdapterConfig(archive, ADAPTER_CONFIG_PATH, relative);
         modifyAdapterConfig(archive, ADAPTER_CONFIG_PATH_TENANT1, relative);
         modifyAdapterConfig(archive, ADAPTER_CONFIG_PATH_TENANT2, relative);
@@ -260,7 +259,7 @@ public class DeploymentArchiveProcessor implements ApplicationArchiveProcessor {
         } catch (Exception ex) {
             throw new RuntimeException("Error when processing " + archive.getName(), ex);
         }
-        if (isTomcatAppServer(testClass.getJavaClass())) {
+        if (isTomcatAppServer()) {
             modifyDocElementValue(webXmlDoc, "auth-method", "KEYCLOAK", "BASIC");
         }
 
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/DeploymentTargetModifier.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/DeploymentTargetModifier.java
index 339ded7..448e1a8 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/DeploymentTargetModifier.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/DeploymentTargetModifier.java
@@ -17,17 +17,20 @@
 
 package org.keycloak.testsuite.arquillian;
 
+import java.util.ArrayList;
+import java.util.Objects;
+import java.util.List;
 import org.jboss.arquillian.container.spi.client.deployment.DeploymentDescription;
 import org.jboss.arquillian.container.spi.client.deployment.TargetDescription;
+import org.jboss.arquillian.core.api.Instance;
+import org.jboss.arquillian.core.api.annotation.Inject;
 import org.jboss.arquillian.container.test.impl.client.deployment.AnnotationDeploymentScenarioGenerator;
 import org.jboss.arquillian.test.spi.TestClass;
 import org.jboss.logging.Logger;
 import org.keycloak.common.util.StringPropertyReplacer;
+import org.keycloak.testsuite.arquillian.AppServerTestEnricher;
 
-import java.util.List;
-
-import java.util.Objects;
-import static org.keycloak.testsuite.arquillian.AppServerTestEnricher.getAppServerQualifier;
+import static org.keycloak.testsuite.arquillian.AppServerTestEnricher.getAppServerQualifiers;
 
 /**
  * Changes target container for all Arquillian deployments based on value of
@@ -39,17 +42,33 @@ public class DeploymentTargetModifier extends AnnotationDeploymentScenarioGenera
 
     // Will be replaced in runtime by real auth-server-container
     public static final String AUTH_SERVER_CURRENT = "auth-server-current";
+    // Will be replaced in runtime by real app-server-container
+    public static final String APP_SERVER_CURRENT = "app-server-current";
 
     protected final Logger log = Logger.getLogger(this.getClass());
 
+    @Inject
+    private Instance<TestContext> testContext;
+
     @Override
     public List<DeploymentDescription> generate(TestClass testClass) {
+        TestContext context = testContext.get();
+        if (context.isAdapterTest() && !context.isAdapterContainerEnabled() && !context.isAdapterContainerEnabledCluster()) {
+            return new ArrayList<>(); // adapter test will be skipped, no need to genarate dependencies
+        }
+
         List<DeploymentDescription> deployments = super.generate(testClass);
 
-        checkAuthServerTestDeployment(deployments, testClass);
+        checkTestDeployments(deployments, testClass);
+        List<String> appServerQualifiers = getAppServerQualifiers(testClass.getJavaClass());
+        if (appServerQualifiers == null) return deployments; // no adapter test
 
-        String appServerQualifier = getAppServerQualifier(
-                testClass.getJavaClass());
+        String appServerQualifier = appServerQualifiers.stream()
+                .filter(q -> q.contains(AppServerTestEnricher.CURRENT_APP_SERVER))
+                .findAny()
+                .orElse(null);
+
+        if (appServerQualifier.contains(";")) return deployments;
 
         if (appServerQualifier != null && !appServerQualifier.isEmpty()) {
             for (DeploymentDescription deployment : deployments) {
@@ -65,21 +84,24 @@ public class DeploymentTargetModifier extends AnnotationDeploymentScenarioGenera
                 }
             }
         }
-
         return deployments;
     }
 
-    private void checkAuthServerTestDeployment(List<DeploymentDescription> descriptions, TestClass testClass) {
+    private void checkTestDeployments(List<DeploymentDescription> descriptions, TestClass testClass) {
         for (DeploymentDescription deployment : descriptions) {
             if (deployment.getTarget() != null) {
                 String containerQualifier = deployment.getTarget().getName();
                 if (AUTH_SERVER_CURRENT.equals(containerQualifier)) {
                     String newAuthServerQualifier = AuthServerTestEnricher.AUTH_SERVER_CONTAINER;
-                    updateAuthServerQualifier(deployment, testClass, newAuthServerQualifier);
+                    updateServerQualifier(deployment, testClass, newAuthServerQualifier);
+                } else if (containerQualifier.contains(APP_SERVER_CURRENT)) {
+                    String suffix = containerQualifier.split(APP_SERVER_CURRENT)[1];
+                    String newAppServerQualifier = AppServerTestEnricher.APP_SERVER_PREFIX  + AppServerTestEnricher.CURRENT_APP_SERVER + "-" + suffix;
+                    updateServerQualifier(deployment, testClass, newAppServerQualifier);
                 } else {
-                    String newAuthServerQualifier = StringPropertyReplacer.replaceProperties(containerQualifier);
-                    if (!newAuthServerQualifier.equals(containerQualifier)) {
-                        updateAuthServerQualifier(deployment, testClass, newAuthServerQualifier);
+                    String newServerQualifier = StringPropertyReplacer.replaceProperties(containerQualifier);
+                    if (!newServerQualifier.equals(containerQualifier)) {
+                        updateServerQualifier(deployment, testClass, newServerQualifier);
                     }
                 }
 
@@ -88,9 +110,9 @@ public class DeploymentTargetModifier extends AnnotationDeploymentScenarioGenera
         }
     }
 
-    private void updateAuthServerQualifier(DeploymentDescription deployment, TestClass testClass, String newAuthServerQualifier) {
-        log.infof("Setting target container for deployment %s.%s: %s", testClass.getName(), deployment.getName(), newAuthServerQualifier);
-        deployment.setTarget(new TargetDescription(newAuthServerQualifier));
+    private void updateServerQualifier(DeploymentDescription deployment, TestClass testClass, String newServerQualifier) {
+        log.infof("Setting target container for deployment %s.%s: %s", testClass.getName(), deployment.getName(), newServerQualifier);
+        deployment.setTarget(new TargetDescription(newServerQualifier));
     }
 
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/KeycloakArquillianExtension.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/KeycloakArquillianExtension.java
index 4671ccb..8872c37 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/KeycloakArquillianExtension.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/KeycloakArquillianExtension.java
@@ -70,7 +70,8 @@ public class KeycloakArquillianExtension implements LoadableExtension {
                 .observer(AppServerTestEnricher.class)
                 .observer(H2TestEnricher.class);
         builder
-                .service(TestExecutionDecider.class, MigrationTestExecutionDecider.class);
+                .service(TestExecutionDecider.class, MigrationTestExecutionDecider.class)
+                .service(TestExecutionDecider.class, AdapterTestExecutionDecider.class);
 
         builder
                 .override(ResourceProvider.class, URLResourceProvider.class, URLProvider.class)
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/migration/MigrationTestExecutionDecider.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/migration/MigrationTestExecutionDecider.java
index b8aa42c..f11e909 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/migration/MigrationTestExecutionDecider.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/migration/MigrationTestExecutionDecider.java
@@ -57,7 +57,7 @@ public class MigrationTestExecutionDecider implements TestExecutionDecider {
 
     @Override
     public int precedence() {
-        return 1;
+        return 2;
     }
 
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/URLProvider.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/URLProvider.java
index f9d557b..6570a2a 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/URLProvider.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/provider/URLProvider.java
@@ -33,7 +33,9 @@ import java.lang.annotation.Annotation;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
+import org.keycloak.testsuite.arquillian.ContainerInfo;
 
 public class URLProvider extends URLResourceProvider {
 
@@ -106,7 +108,13 @@ public class URLProvider extends URLResourceProvider {
                 return suiteContext.get().getAuthServerInfo().getContextRoot();
             }
             if (AppServerContext.class.isAssignableFrom(a.annotationType())) {
-                return testContext.get().getAppServerInfo().getContextRoot();
+                ContainerInfo appServerInfo = testContext.get().getAppServerInfo();
+                if (appServerInfo != null) return appServerInfo.getContextRoot();
+                
+                List<ContainerInfo> appServerBackendsInfo = testContext.get().getAppServerBackendsInfo();
+                if (appServerBackendsInfo.isEmpty()) throw new IllegalStateException("Both testContext's appServerInfo and appServerBackendsInfo not set.");
+                
+                return appServerBackendsInfo.get(0).getContextRoot();
             }
         }
 
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/TestContext.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/TestContext.java
index 3ba7082..2f47a82 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/TestContext.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/TestContext.java
@@ -17,13 +17,16 @@
 package org.keycloak.testsuite.arquillian;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
 
 import org.keycloak.admin.client.Keycloak;
 import org.keycloak.representations.idm.RealmRepresentation;
+import static org.keycloak.testsuite.arquillian.AppServerTestEnricher.getAppServerQualifiers;
 import org.keycloak.testsuite.client.KeycloakTestingClient;
 import org.keycloak.testsuite.util.TestCleanup;
 
@@ -80,25 +83,45 @@ public final class TestContext {
     public List<ContainerInfo> getAppServerBackendsInfo() {
         return appServerBackendsInfo;
     }
+    
+    public void setAppServerBackendsInfo(List<ContainerInfo> appServerBackendsInfo) {
+        Collections.sort(appServerBackendsInfo);
+        this.appServerBackendsInfo.addAll(appServerBackendsInfo);
+    }
 
     public Class getTestClass() {
         return testClass;
     }
 
     public boolean isAdapterTest() {
-        return appServerInfo != null;
+        return getAppServerQualifiers(testClass) != null;
+    }
+
+    public boolean isAdapterContainerEnabled() {
+        if (!isAdapterTest()) return false; //no adapter test
+        if (appServerInfo == null) return false;
+        return getAppServerQualifiers(testClass).contains(appServerInfo.getQualifier());
     }
 
+    public boolean isAdapterContainerEnabledCluster() {
+        if (!isAdapterTest()) return false; //no adapter test
+        if (appServerBackendsInfo.isEmpty()) return false; //no adapter clustered test
+        
+        List<String> appServerQualifiers = getAppServerQualifiers(testClass);
+        
+        String qualifier = appServerBackendsInfo.stream()
+                .map(ContainerInfo::getQualifier)
+                .collect(Collectors.joining(";"));
+        
+        return appServerQualifiers.contains(qualifier);
+    }
+    
     public boolean isRelativeAdapterTest() {
         return isAdapterTest()
                 && appServerInfo.getQualifier().equals(
                         suiteContext.getAuthServerInfo().getQualifier()); // app server == auth server
     }
 
-    public boolean isClusteredAdapterTest() {
-        return isAdapterTest() && !appServerBackendsInfo.isEmpty();
-    }
-
     public SuiteContext getSuiteContext() {
         return suiteContext;
     }
@@ -106,7 +129,7 @@ public final class TestContext {
     @Override
     public String toString() {
         return "TEST CONTEXT: " + getTestClass().getCanonicalName() + "\n"
-                + (isAdapterTest() ? "App server container: " + getAppServerInfo() + "\n" : "");
+                + (isAdapterTest() ? "Activated @AppServerContainer(" + getAppServerQualifiers(testClass) + ")\n" : "");
     }
 
     public Keycloak getAdminClient() {
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/IOUtil.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/IOUtil.java
index 1707ef7..ca0da18 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/IOUtil.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/IOUtil.java
@@ -199,7 +199,7 @@ public class IOUtil {
                 return;
             }
 
-            log.info("Removing node " + removeNode);
+            log.trace("Removing node " + removeNode);
             parentElement.removeChild(removeElement);
 
         }
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/WaitUtils.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/WaitUtils.java
index 30ec1a8..7d40f68 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/WaitUtils.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/WaitUtils.java
@@ -16,6 +16,9 @@
  */
 package org.keycloak.testsuite.util;
 
+import java.time.Duration;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 import org.jboss.arquillian.graphene.wait.ElementBuilder;
 import org.openqa.selenium.By;
 import org.openqa.selenium.TimeoutException;
@@ -25,11 +28,6 @@ import org.openqa.selenium.htmlunit.HtmlUnitDriver;
 import org.openqa.selenium.support.ui.FluentWait;
 import org.openqa.selenium.support.ui.WebDriverWait;
 
-import java.util.Collections;
-import java.util.concurrent.TimeUnit;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
 import static org.jboss.arquillian.graphene.Graphene.waitGui;
 import static org.keycloak.testsuite.util.DroneUtils.getCurrentDriver;
 import static org.openqa.selenium.support.ui.ExpectedConditions.*;
@@ -102,7 +100,7 @@ public final class WaitUtils {
         // Ensure the URL is "stable", i.e. is not changing anymore; if it'd changing, some redirects are probably still in progress
         for (int maxRedirects = 2; maxRedirects > 0; maxRedirects--) {
             String currentUrl = driver.getCurrentUrl();
-            FluentWait<WebDriver> wait = new FluentWait<>(driver).withTimeout(250, TimeUnit.MILLISECONDS);
+            FluentWait<WebDriver> wait = new FluentWait<>(driver).withTimeout(Duration.ofMillis(250));
             try {
                 wait.until(not(urlToBe(currentUrl)));
             }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java
index b0203a3..e5aafc6 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/AbstractKeycloakTest.java
@@ -258,7 +258,7 @@ public abstract class AbstractKeycloakTest {
 
     protected void deleteAllCookiesForRealm(String realmName) {
         // masterRealmPage.navigateTo();
-        driver.navigate().to(oauth.AUTH_SERVER_ROOT + "/realms/" + realmName + "/account"); // Because IE webdriver freezes when loading a JSON page (realm page), we need to use this alternative
+        driver.navigate().to(OAuthClient.AUTH_SERVER_ROOT + "/realms/" + realmName + "/account"); // Because IE webdriver freezes when loading a JSON page (realm page), we need to use this alternative
         log.info("deleting cookies in '" + realmName + "' realm");
         driver.manage().deleteAllCookies();
     }
@@ -319,7 +319,6 @@ public abstract class AbstractKeycloakTest {
         log.debug("importing realm: " + realm.getRealm());
         try { // TODO - figure out a way how to do this without try-catch
             RealmResource realmResource = adminClient.realms().realm(realm.getRealm());
-            RealmRepresentation rRep = realmResource.toRepresentation();
             log.debug("realm already exists on server, re-importing");
             realmResource.remove();
         } catch (NotFoundException nfe) {
@@ -491,4 +490,4 @@ public abstract class AbstractKeycloakTest {
         administration.reloadIfRequired();
         client.close();
     }
-}
\ No newline at end of file
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractAdapterTest.java
index 1145947..d337345 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractAdapterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractAdapterTest.java
@@ -49,10 +49,12 @@ import java.util.Map;
 import java.util.concurrent.TimeoutException;
 
 /**
- *
+ * <code>@AppServerContainer</code> is needed for stopping recursion in 
+ * AppServerTestEnricher.getNearestSuperclassWithAnnotation
+ * 
  * @author tkyjovsk
  */
-@AppServerContainer
+@AppServerContainer("")
 public abstract class AbstractAdapterTest extends AbstractAuthTest {
 
     @Page
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/cluster/AbstractSAMLAdapterClusterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/cluster/AbstractSAMLAdapterClusterTest.java
index fdd0a0c..f3975f0 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/cluster/AbstractSAMLAdapterClusterTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/cluster/AbstractSAMLAdapterClusterTest.java
@@ -64,7 +64,7 @@ import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.not;
 import static org.junit.Assert.assertThat;
 import static org.keycloak.testsuite.admin.Users.setPasswordFor;
-import static org.keycloak.testsuite.arquillian.AppServerTestEnricher.getNearestSuperclassWithAnnotation;
+import static org.keycloak.testsuite.arquillian.AppServerTestEnricher.getNearestSuperclassWithAppServerAnnotation;
 import static org.keycloak.testsuite.auth.page.AuthRealm.DEMO;
 import static org.keycloak.testsuite.util.IOUtil.loadRealm;
 import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
@@ -301,7 +301,7 @@ public abstract class AbstractSAMLAdapterClusterTest extends AbstractServletsAda
     }
 
     private String getAppServerId() {
-        Class<?> annotatedClass = getNearestSuperclassWithAnnotation(this.getClass(), AppServerContainer.class);
+        Class<?> annotatedClass = getNearestSuperclassWithAppServerAnnotation(this.getClass());
 
         return (annotatedClass == null ? "<cannot-find-@AppServerContainer>"
                 : annotatedClass.getAnnotation(AppServerContainer.class).value());
diff --git a/travis-run-tests.sh b/travis-run-tests.sh
index 532eaf9..bf59b2a 100755
--- a/travis-run-tests.sh
+++ b/travis-run-tests.sh
@@ -77,10 +77,10 @@ fi
 
 if [ $1 == "crossdc" ]; then
     cd testsuite/integration-arquillian
-    mvn install -B -nsu -Pauth-servers-crossdc-jboss,auth-server-wildfly,cache-server-infinispan -DskipTests
+    mvn install -B -nsu -Pauth-servers-crossdc-jboss,auth-server-wildfly,cache-server-infinispan,app-server-wildfly -DskipTests
 
     cd tests/base
-    mvn clean test -B -nsu -Pcache-server-infinispan,auth-servers-crossdc-jboss,auth-server-wildfly -Dtest=*.crossdc.**.* 2>&1 |
+    mvn clean test -B -nsu -Pcache-server-infinispan,auth-servers-crossdc-jboss,auth-server-wildfly,app-server-wildfly -Dtest=*.crossdc.**.* 2>&1 |
         java -cp ../../../utils/target/classes org.keycloak.testsuite.LogTrimmer
     BASE_TESTS_STATUS=${PIPESTATUS[0]}