mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-09 23:12:06 -03:30
Create clustering test cases for OIDC flows (#40623)
Closes #39965 Signed-off-by: Ryan Emerson <remerson@redhat.com> Signed-off-by: Michal Hajas <mhajas@redhat.com> Co-authored-by: Michal Hajas <mhajas@redhat.com>
This commit is contained in:
parent
4f05b62e99
commit
9543008899
@ -18,5 +18,9 @@
|
||||
<artifactId>keycloak-test-framework-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.vertx</groupId>
|
||||
<artifactId>vertx-http-proxy</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@ -1,26 +1,72 @@
|
||||
package org.keycloak.testframework.clustering;
|
||||
|
||||
import org.keycloak.testframework.server.ClusteredKeycloakServer;
|
||||
import org.keycloak.testframework.server.KeycloakUrls;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
import io.vertx.httpproxy.ProxyContext;
|
||||
import io.vertx.httpproxy.ProxyInterceptor;
|
||||
import io.vertx.httpproxy.ProxyResponse;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.testframework.server.ClusteredKeycloakServer;
|
||||
import org.keycloak.testframework.server.KeycloakUrls;
|
||||
|
||||
import io.vertx.core.Future;
|
||||
import io.vertx.core.Vertx;
|
||||
import io.vertx.core.http.HttpClient;
|
||||
import io.vertx.core.http.HttpServer;
|
||||
import io.vertx.httpproxy.HttpProxy;
|
||||
|
||||
public class LoadBalancer {
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(LoadBalancer.class);
|
||||
public static final String HOSTNAME = "http://localhost:9999";
|
||||
|
||||
private final ClusteredKeycloakServer server;
|
||||
private final HashMap<Integer, KeycloakUrls> urls = new HashMap<>();
|
||||
private final HashMap<Integer, Origin> urls = new HashMap<>();
|
||||
private final Vertx vertx;
|
||||
private final HttpProxy proxy;
|
||||
|
||||
public LoadBalancer(ClusteredKeycloakServer server) {
|
||||
this.server = server;
|
||||
|
||||
this.vertx = Vertx.vertx();
|
||||
HttpClient proxyClient = vertx.createHttpClient();
|
||||
proxy = HttpProxy.reverseProxy(proxyClient);
|
||||
proxy.addInterceptor(new ProxyInterceptor() {
|
||||
@Override
|
||||
public Future<ProxyResponse> handleProxyRequest(ProxyContext context) {
|
||||
LOGGER.debugf("Proxy request intercepted: %s", context.request().getURI());
|
||||
return ProxyInterceptor.super.handleProxyRequest(context);
|
||||
}
|
||||
});
|
||||
node(0);
|
||||
|
||||
HttpServer proxyServer = vertx.createHttpServer();
|
||||
proxyServer.requestHandler(proxy).listen(9999, "localhost");
|
||||
}
|
||||
|
||||
public KeycloakUrls node(int nodeIndex) {
|
||||
if (nodeIndex >= server.clusterSize()) {
|
||||
throw new IllegalArgumentException("Node index out of bounds. Requested nodeIndex: %d, cluster size: %d".formatted(server.clusterSize(), nodeIndex));
|
||||
public void node(int index) {
|
||||
Origin origin = origin(index);
|
||||
LOGGER.debugf("Setting proxy origin to: %s:%d", origin.host, origin.port);
|
||||
proxy.origin(origin.port, origin.host);
|
||||
}
|
||||
|
||||
public KeycloakUrls nodeUrls(int index) {
|
||||
return origin(index).urls;
|
||||
}
|
||||
|
||||
private Origin origin(int index) {
|
||||
if (index >= server.clusterSize()) {
|
||||
throw new IllegalArgumentException("Node index out of bounds. Requested nodeIndex: %d, cluster size: %d".formatted(server.clusterSize(), index));
|
||||
}
|
||||
return urls.computeIfAbsent(nodeIndex, i -> new KeycloakUrls(server.getBaseUrl(i), server.getManagementBaseUrl(i)));
|
||||
return urls.computeIfAbsent(index, i ->
|
||||
new Origin("localhost", server.getBasePort(i), new KeycloakUrls(server.getBaseUrl(i), server.getManagementBaseUrl(i)))
|
||||
);
|
||||
}
|
||||
|
||||
public int clusterSize() {
|
||||
return server.clusterSize();
|
||||
public void close() {
|
||||
Future.await(vertx.close());
|
||||
}
|
||||
|
||||
record Origin(String host, int port, KeycloakUrls urls) {
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,10 +4,13 @@ import org.keycloak.testframework.annotations.InjectLoadBalancer;
|
||||
import org.keycloak.testframework.injection.InstanceContext;
|
||||
import org.keycloak.testframework.injection.RequestedInstance;
|
||||
import org.keycloak.testframework.injection.Supplier;
|
||||
import org.keycloak.testframework.injection.SupplierOrder;
|
||||
import org.keycloak.testframework.server.ClusteredKeycloakServer;
|
||||
import org.keycloak.testframework.server.KeycloakServer;
|
||||
import org.keycloak.testframework.server.KeycloakServerConfigBuilder;
|
||||
import org.keycloak.testframework.server.KeycloakServerConfigInterceptor;
|
||||
|
||||
public class LoadBalancerSupplier implements Supplier<LoadBalancer, InjectLoadBalancer> {
|
||||
public class LoadBalancerSupplier implements Supplier<LoadBalancer, InjectLoadBalancer>, KeycloakServerConfigInterceptor<LoadBalancer, InjectLoadBalancer> {
|
||||
|
||||
@Override
|
||||
public LoadBalancer getValue(InstanceContext<LoadBalancer, InjectLoadBalancer> instanceContext) {
|
||||
@ -20,8 +23,23 @@ public class LoadBalancerSupplier implements Supplier<LoadBalancer, InjectLoadBa
|
||||
throw new IllegalStateException("Load balancer can only be used with ClusteredKeycloakServer");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close(InstanceContext<LoadBalancer, InjectLoadBalancer> instanceContext) {
|
||||
instanceContext.getValue().close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean compatible(InstanceContext<LoadBalancer, InjectLoadBalancer> a, RequestedInstance<LoadBalancer, InjectLoadBalancer> b) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int order() {
|
||||
return SupplierOrder.BEFORE_REALM;
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeycloakServerConfigBuilder intercept(KeycloakServerConfigBuilder serverConfig, InstanceContext<LoadBalancer, InjectLoadBalancer> instanceContext) {
|
||||
return serverConfig.option("hostname", LoadBalancer.HOSTNAME);
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,15 +17,12 @@
|
||||
|
||||
package org.keycloak.testframework.server;
|
||||
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
import io.quarkus.bootstrap.utils.BuildToolHelper;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.it.utils.DockerKeycloakDistribution;
|
||||
import org.keycloak.testframework.clustering.LoadBalancer;
|
||||
import org.keycloak.testframework.database.JBossLogConsumer;
|
||||
import org.testcontainers.images.RemoteDockerImage;
|
||||
import org.testcontainers.utility.DockerImageName;
|
||||
@ -122,7 +119,7 @@ public class ClusteredKeycloakServer implements KeycloakServer {
|
||||
|
||||
@Override
|
||||
public String getBaseUrl() {
|
||||
return getBaseUrl(0);
|
||||
return LoadBalancer.HOSTNAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -130,8 +127,12 @@ public class ClusteredKeycloakServer implements KeycloakServer {
|
||||
return getManagementBaseUrl(0);
|
||||
}
|
||||
|
||||
public int getBasePort(int index) {
|
||||
return containers[index].getMappedPort(REQUEST_PORT);
|
||||
}
|
||||
|
||||
public String getBaseUrl(int index) {
|
||||
return "http://localhost:%d".formatted(containers[index].getMappedPort(REQUEST_PORT));
|
||||
return "http://localhost:%d".formatted(getBasePort(index));
|
||||
}
|
||||
|
||||
public String getManagementBaseUrl(int index) {
|
||||
|
||||
@ -37,6 +37,4 @@ public class ClusteredKeycloakServerSupplier extends AbstractKeycloakServerSuppl
|
||||
protected String cache() {
|
||||
return "ispn";
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -41,5 +41,27 @@
|
||||
<groupId>org.keycloak.testframework</groupId>
|
||||
<artifactId>keycloak-test-framework-db-postgres</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak.testframework</groupId>
|
||||
<artifactId>keycloak-test-framework-oauth</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak.testframework</groupId>
|
||||
<artifactId>keycloak-test-framework-ui</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<systemPropertyVariables>
|
||||
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
|
||||
<java.util.concurrent.ForkJoinPool.common.threadFactory>io.quarkus.bootstrap.forkjoin.QuarkusForkJoinWorkerThreadFactory</java.util.concurrent.ForkJoinPool.common.threadFactory>
|
||||
</systemPropertyVariables>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@ -0,0 +1,92 @@
|
||||
package org.keycloak.tests.compatibility;
|
||||
|
||||
import org.htmlunit.WebClient;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.keycloak.testframework.annotations.InjectLoadBalancer;
|
||||
import org.keycloak.testframework.annotations.InjectUser;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
import org.keycloak.testframework.clustering.LoadBalancer;
|
||||
import org.keycloak.testframework.oauth.OAuthClient;
|
||||
import org.keycloak.testframework.oauth.annotations.InjectOAuthClient;
|
||||
import org.keycloak.testframework.realm.ManagedUser;
|
||||
import org.keycloak.testframework.realm.UserConfig;
|
||||
import org.keycloak.testframework.realm.UserConfigBuilder;
|
||||
import org.keycloak.testframework.ui.annotations.InjectWebDriver;
|
||||
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
|
||||
import org.keycloak.testsuite.util.oauth.AuthorizationEndpointResponse;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import org.openqa.selenium.htmlunit.HtmlUnitDriver;
|
||||
|
||||
@KeycloakIntegrationTest
|
||||
public class ClusteredOAuthClientTest {
|
||||
|
||||
@InjectUser(config = OAuthUserConfig.class)
|
||||
ManagedUser user;
|
||||
|
||||
@InjectLoadBalancer
|
||||
LoadBalancer loadBalancer;
|
||||
|
||||
@InjectOAuthClient
|
||||
OAuthClient oauth;
|
||||
|
||||
@InjectWebDriver
|
||||
WebDriver driver;
|
||||
|
||||
@AfterEach
|
||||
public void cleanup() {
|
||||
loadBalancer.node(0);
|
||||
driver.navigate().to("about:blank");
|
||||
if (driver instanceof HtmlUnitDriver htmlUnitDriver) {
|
||||
WebClient webClient = htmlUnitDriver.getWebClient();
|
||||
webClient.getCache().clear();
|
||||
webClient.getCookieManager().clearCookies();
|
||||
webClient.reset();
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({"0, 1", "1, 0"})
|
||||
public void testAccessTokenRefresh(int grantNode, int refreshNode) {
|
||||
loadBalancer.node(grantNode);
|
||||
AccessTokenResponse accessTokenResponse = oauth.doPasswordGrantRequest(user.getUsername(), user.getPassword());
|
||||
|
||||
loadBalancer.node(refreshNode);
|
||||
AccessTokenResponse refreshResponse = oauth.doRefreshTokenRequest(accessTokenResponse.getRefreshToken());
|
||||
Assertions.assertTrue(refreshResponse.isSuccess());
|
||||
Assertions.assertNotEquals(accessTokenResponse.getAccessToken(), refreshResponse.getAccessToken());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"0, 0, 1",
|
||||
"0, 1, 0",
|
||||
"0, 1, 1",
|
||||
"1, 0, 0",
|
||||
"1, 1, 0",
|
||||
"1, 0, 1",
|
||||
})
|
||||
public void testLoginLogout(int loginNode, int tokenNode, int logoutNode) {
|
||||
loadBalancer.node(loginNode);
|
||||
AuthorizationEndpointResponse authResponse = oauth.doLogin(user.getUsername(), user.getPassword());
|
||||
Assertions.assertTrue(authResponse.isRedirected());
|
||||
|
||||
loadBalancer.node(tokenNode);
|
||||
AccessTokenResponse accessTokenResponse = oauth.doPasswordGrantRequest(user.getUsername(), user.getPassword());
|
||||
Assertions.assertTrue(accessTokenResponse.isSuccess());
|
||||
|
||||
loadBalancer.node(logoutNode);
|
||||
oauth.logoutForm().idTokenHint(accessTokenResponse.getIdToken()).open();
|
||||
}
|
||||
|
||||
public static class OAuthUserConfig implements UserConfig {
|
||||
@Override
|
||||
public UserConfigBuilder configure(UserConfigBuilder user) {
|
||||
return user.username("myuser").name("First", "Last")
|
||||
.email("test@local")
|
||||
.password("password");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,42 +0,0 @@
|
||||
/*
|
||||
* Copyright 2025 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.tests.compatibility;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.junit.jupiter.api.Assumptions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.keycloak.testframework.annotations.InjectLoadBalancer;
|
||||
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
|
||||
import org.keycloak.testframework.clustering.LoadBalancer;
|
||||
|
||||
@KeycloakIntegrationTest
|
||||
public class MixedVersionClusterTest {
|
||||
|
||||
@InjectLoadBalancer
|
||||
LoadBalancer loadBalancer;
|
||||
|
||||
@Test
|
||||
public void testUrls() throws InterruptedException {
|
||||
// TODO annotation based to skip if running in non-clustered mode.
|
||||
Assumptions.assumeTrue(loadBalancer.clusterSize() == 2);
|
||||
System.out.println("url0->" + loadBalancer.node(0).getBaseUrl());
|
||||
System.out.println("url1->" + loadBalancer.node(1).getBaseUrl());
|
||||
Thread.sleep(TimeUnit.MINUTES.toMillis(1));
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,6 @@
|
||||
kc.test.server=cluster
|
||||
# As clustering is dependent on JDBC_PING, we must use a real database for cluster tests
|
||||
kc.test.database=postgres
|
||||
|
||||
kc.test.log.level=WARN
|
||||
|
||||
@ -9,4 +11,5 @@ kc.test.log.category."org.keycloak.tests".level=INFO
|
||||
kc.test.log.category."testinfo".level=INFO
|
||||
kc.test.log.category."org.keycloak".level=WARN
|
||||
kc.test.log.category."managed.keycloak".level=WARN
|
||||
kc.test.log.category."managed.db".level=WARN
|
||||
kc.test.log.category."managed.db".level=WARN
|
||||
kc.test.log.category."org.keycloak.testframework.clustering".level=WARN
|
||||
Loading…
x
Reference in New Issue
Block a user