Skip to content

Commit

Permalink
Improve DevServices test integration
Browse files Browse the repository at this point in the history
Also improce the KeyCloak test client, and modify a KC test to use
DevServices.

This adds a new DevServicesContext which can be injected into
QuarkusTest and QuarkusIntegrationTest, as well as TestResourceManager
implementation (to allow the dev service to be configured).
  • Loading branch information
stuartwdouglas authored and sberyozkin committed Aug 26, 2021
1 parent 5930131 commit fc697fc
Show file tree
Hide file tree
Showing 17 changed files with 173 additions and 170 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
Expand All @@ -25,6 +26,7 @@
import io.quarkus.bootstrap.classloading.QuarkusClassLoader;
import io.quarkus.builder.BuildResult;
import io.quarkus.deployment.builditem.ApplicationClassNameBuildItem;
import io.quarkus.deployment.builditem.DevServicesLauncherConfigResultBuildItem;
import io.quarkus.deployment.builditem.GeneratedClassBuildItem;
import io.quarkus.deployment.builditem.GeneratedResourceBuildItem;
import io.quarkus.deployment.builditem.MainClassBuildItem;
Expand Down Expand Up @@ -221,6 +223,16 @@ public ClassLoader getClassLoader() {
return runtimeClassLoader;
}

@Override
public Map<String, String> getDevServicesProperties() {
DevServicesLauncherConfigResultBuildItem result = buildResult
.consumeOptional(DevServicesLauncherConfigResultBuildItem.class);
if (result == null) {
return Collections.emptyMap();
}
return new HashMap<>(result.getConfig());
}

private Map<String, byte[]> extractTransformers(Set<String> eagerClasses) {
Map<String, byte[]> ret = new HashMap<>();
TransformedClassesBuildItem transformers = buildResult.consume(TransformedClassesBuildItem.class);
Expand Down
19 changes: 19 additions & 0 deletions docs/src/main/asciidoc/getting-started-testing.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1235,3 +1235,22 @@ The `settings.json` placed in the root of your project directory or in the works
=== IntelliJ JUnit template

Nothing needed in IntelliJ because the IDE will pick the `systemPropertyVariables` from the surefire plugin configuration in `pom.xml`.

== Testing Dev Services

By default tests should just work with link:dev-services[Dev Services], however from some use cases you may need access to
the automatically configured properties in your tests.

You can do this with `io.quarkus.test.common.DevServicesContext`, which can be injected directly into any `@QuarkusTest`
or `@QuarkusIntegrationTest`. All you need to do is define a field of type `DevServicesContext` and it will be automatically
injected. Using this you can retrieve any properties that have been set. Generally this is used to directly connect to a
resource from the test itself, e.g. to connect to kafka to send messages to the application under test.

Injection is also supported into objects that implement `io.quarkus.test.common.DevServicesContext.ContextAware`. If you
have a field that implements `io.quarkus.test.common.DevServicesContext.ContextAware` Quarkus will call the
`setIntegrationTestContext` method to pass the context into this object. This allows client logic to be encapsulated in
a utility class.

`QuarkusTestResourceLifecycleManager` implementations can also implement `ContextAware` to get access to these properties,
which allows you to setup the resource before Quarkus starts (e.g. configure a KeyCloak instance, add data to a database etc).

Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ public class DevServicesConfig {
@ConfigItem
public Optional<String> realmPath;

/**
* The JAVA_OPTS passed to the keycloak JVM
*/
@ConfigItem
public Optional<String> javaOpts;
/**
* The Keycloak realm.
* This property will be used to create the realm if the realm file pointed to by the 'realm-path' property does not exist.
Expand Down Expand Up @@ -134,6 +139,7 @@ public boolean equals(Object o) {
&& Objects.equals(realmPath, that.realmPath)
&& Objects.equals(realmName, that.realmName)
&& Objects.equals(users, that.users)
&& Objects.equals(javaOpts, that.javaOpts)
&& Objects.equals(roles, that.roles);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ public class KeycloakDevServicesProcessor {
private static final String KEYCLOAK_URL_KEY = "keycloak.url";

private static final int KEYCLOAK_EXPOSED_PORT = 8080;
private static final String JAVA_OPTS = "JAVA_OPTS";
private static final String KEYCLOAK_DOCKER_REALM_PATH = "/tmp/realm.json";
private static final String KEYCLOAK_USER_PROP = "KEYCLOAK_USER";
private static final String KEYCLOAK_PASSWORD_PROP = "KEYCLOAK_PASSWORD";
Expand Down Expand Up @@ -228,7 +229,7 @@ private StartResult startContainer(boolean useSharedContainer) {
QuarkusOidcContainer oidcContainer = new QuarkusOidcContainer(dockerImageName,
capturedDevServicesConfiguration.port,
useSharedContainer,
capturedDevServicesConfiguration.realmPath);
capturedDevServicesConfiguration.realmPath, capturedDevServicesConfiguration.javaOpts);

oidcContainer.start();

Expand Down Expand Up @@ -263,13 +264,15 @@ private static class QuarkusOidcContainer extends GenericContainer {
private final Optional<String> realm;
private boolean realmFileExists;
private String hostName = null;
private final Optional<String> javaOpts;

public QuarkusOidcContainer(DockerImageName dockerImageName, OptionalInt fixedExposedPort, boolean useSharedNetwork,
Optional<String> realm) {
Optional<String> realm, Optional<String> javaOpts) {
super(dockerImageName);
this.fixedExposedPort = fixedExposedPort;
this.useSharedNetwork = useSharedNetwork;
this.realm = realm;
this.javaOpts = javaOpts;
}

@Override
Expand All @@ -293,6 +296,9 @@ protected void configure() {
addEnv(KEYCLOAK_USER_PROP, KEYCLOAK_ADMIN_USER);
addEnv(KEYCLOAK_PASSWORD_PROP, KEYCLOAK_ADMIN_PASSWORD);
addEnv(KEYCLOAK_VENDOR_PROP, KEYCLOAK_DB_VENDOR);
if (javaOpts.isPresent()) {
addEnv(JAVA_OPTS, javaOpts.get());
}

if (realm.isPresent()) {
if (Thread.currentThread().getContextClassLoader().getResource(realm.get()) != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ public interface StartupAction {

ClassLoader getClassLoader();

Map<String, String> getDevServicesProperties();

/**
* Runs the application by running the main method of the main class. As this is a blocking method a new
* thread is created to run this task.
Expand Down
96 changes: 1 addition & 95 deletions integration-tests/oidc-token-propagation/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@
<name>Quarkus - Integration Tests - OpenID Connect Token Propagation</name>
<description>Module that contains OpenID Connect Token Propagation tests</description>

<properties>
<keycloak.url>http://localhost:8180/auth</keycloak.url>
</properties>

<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
Expand All @@ -30,7 +26,7 @@
<!-- test dependencies -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<artifactId>quarkus-test-keycloak-server</artifactId>
<scope>test</scope>
</dependency>
<dependency>
Expand Down Expand Up @@ -157,18 +153,12 @@
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skip>false</skip>
<systemPropertyVariables>
<keycloak.url>${keycloak.url}</keycloak.url>
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<configuration>
<skip>false</skip>
<systemPropertyVariables>
<keycloak.url>${keycloak.url}</keycloak.url>
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>
Expand All @@ -186,90 +176,6 @@
</build>
</profile>

<profile>
<id>docker-keycloak</id>
<activation>
<property>
<name>start-containers</name>
</property>
</activation>
<properties>
<keycloak.url>http://localhost:8180/auth</keycloak.url>
</properties>
<build>
<plugins>
<plugin>
<groupId>io.fabric8</groupId>
<artifactId>docker-maven-plugin</artifactId>
<configuration>
<images>
<image>
<name>${keycloak.docker.image}</name>
<alias>quarkus-test-keycloak</alias>
<run>
<ports>
<port>8180:8080</port>
</ports>
<env>
<KEYCLOAK_USER>admin</KEYCLOAK_USER>
<KEYCLOAK_PASSWORD>admin</KEYCLOAK_PASSWORD>
<JAVA_OPTS>-Dkeycloak.profile.feature.token_exchange=enabled -Dkeycloak.profile=preview</JAVA_OPTS>
</env>
<log>
<prefix>Keycloak:</prefix>
<date>default</date>
<color>cyan</color>
</log>
<wait>
<!-- good docs found at: http://dmp.fabric8.io/#build-healthcheck -->
<http>
<url>http://localhost:8180</url>
</http>
<time>100000</time>
</wait>
</run>
</image>
</images>
<allContainers>true</allContainers>
</configuration>
<executions>
<execution>
<id>docker-start</id>
<phase>compile</phase>
<goals>
<goal>stop</goal>
<goal>start</goal>
</goals>
</execution>
<execution>
<id>docker-stop</id>
<phase>post-integration-test</phase>
<goals>
<goal>stop</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<executions>
<execution>
<id>docker-prune</id>
<phase>generate-resources</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>${basedir}/../../.github/docker-prune.sh</executable>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>

</profiles>

</project>
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
quarkus.oidc.auth-server-url=${keycloak.url}/realms/quarkus/
%prod.quarkus.oidc.auth-server-url=${keycloak.url}/realms/quarkus/
quarkus.oidc.client-id=quarkus-app
quarkus.oidc.credentials.secret=secret

quarkus.keycloak.devservices.java-opts=-Dkeycloak.profile.feature.token_exchange=enabled -Dkeycloak.profile=preview

quarkus.oidc-client.auth-server-url=${quarkus.oidc.auth-server-url}
quarkus.oidc-client.client-id=${quarkus.oidc.client-id}
quarkus.oidc-client.credentials.secret=${quarkus.oidc.credentials.secret}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package io.quarkus.it.keycloak;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
Expand All @@ -16,13 +18,14 @@
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.util.JsonSerialization;

import io.quarkus.test.common.DevServicesContext;
import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
import io.restassured.RestAssured;

public class KeycloakRealmResourceManager implements QuarkusTestResourceLifecycleManager {
public class KeycloakRealmResourceManager implements QuarkusTestResourceLifecycleManager, DevServicesContext.ContextAware {

private static final String KEYCLOAK_SERVER_URL = System.getProperty("keycloak.url", "http://localhost:8180/auth");
private static final String KEYCLOAK_REALM = "quarkus";
String authServerUrl;

@Override
public Map<String, String> start() {
Expand All @@ -38,33 +41,41 @@ public Map<String, String> start() {
realm.getUsers().add(createUser("bob", "user"));

try {
//dev services sets up a realm for us, we delete it because we want different settings
RestAssured
.given()
.auth().oauth2(getAdminAccessToken())
.contentType("application/json")
.when()
.delete(authServerUrl + "/admin/realms/quarkus").then()
.statusCode(204);
RestAssured
.given()
.auth().oauth2(getAdminAccessToken())
.contentType("application/json")
.body(JsonSerialization.writeValueAsBytes(realm))
.when()
.post(KEYCLOAK_SERVER_URL + "/admin/realms").then()
.post(authServerUrl + "/admin/realms").then()
.statusCode(201);
} catch (IOException e) {
throw new RuntimeException(e);
}
return Collections.emptyMap();
}

private static String getAdminAccessToken() {
private String getAdminAccessToken() {
return RestAssured
.given()
.param("grant_type", "password")
.param("username", "admin")
.param("password", "admin")
.param("client_id", "admin-cli")
.when()
.post(KEYCLOAK_SERVER_URL + "/realms/master/protocol/openid-connect/token")
.post(authServerUrl + "/realms/master/protocol/openid-connect/token")
.as(AccessTokenResponse.class).getToken();
}

private static RealmRepresentation createRealm(String name) {
private RealmRepresentation createRealm(String name) {
RealmRepresentation realm = new RealmRepresentation();

realm.setRealm(name);
Expand All @@ -85,7 +96,7 @@ private static RealmRepresentation createRealm(String name) {
return realm;
}

private static ClientRepresentation createClient(String clientId) {
private ClientRepresentation createClient(String clientId) {
ClientRepresentation client = new ClientRepresentation();

client.setClientId(clientId);
Expand All @@ -98,7 +109,7 @@ private static ClientRepresentation createClient(String clientId) {
return client;
}

private static UserRepresentation createUser(String username, String... realmRoles) {
private UserRepresentation createUser(String username, String... realmRoles) {
UserRepresentation user = new UserRepresentation();

user.setUsername(username);
Expand All @@ -125,19 +136,17 @@ public void stop() {
.given()
.auth().oauth2(getAdminAccessToken())
.when()
.delete(KEYCLOAK_SERVER_URL + "/admin/realms/" + KEYCLOAK_REALM).then().statusCode(204);
.delete(authServerUrl + "/admin/realms/" + KEYCLOAK_REALM).then().statusCode(204);
}

public static String getAccessToken(String userName) {
return RestAssured
.given()
.param("grant_type", "password")
.param("username", userName)
.param("password", userName)
.param("client_id", "quarkus-app")
.param("client_secret", "secret")
.when()
.post(KEYCLOAK_SERVER_URL + "/realms/" + KEYCLOAK_REALM + "/protocol/openid-connect/token")
.as(AccessTokenResponse.class).getToken();
@Override
public void setIntegrationTestContext(DevServicesContext context) {
try {
var uri = new URI(context.devServicesProperties().get("quarkus.oidc.auth-server-url"));
authServerUrl = new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(), "/auth", null, null)
.toString();
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.quarkus.it.keycloak;

import io.quarkus.test.junit.QuarkusIntegrationTest;

@QuarkusIntegrationTest
public class OidcTokenPropagationITCase extends OidcTokenPropagationTest {
}
Loading

0 comments on commit fc697fc

Please sign in to comment.