diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index dbfebf19b..42b1d2cf5 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -61,7 +61,7 @@ jobs:
echo "Running modules: ${MODULES_ARG}"
echo "MODULES_MAVEN_PARAM=[\" -pl ${MODULES_ARG} -Dall-modules\"]" >> $GITHUB_OUTPUT
else
- echo "MODULES_MAVEN_PARAM=[' -P root-modules,spring-modules,http-modules,test-tooling-modules', ' -P security-modules,sql-db-modules,messaging-modules,websockets-modules,monitoring-modules']" >> $GITHUB_OUTPUT
+ echo "MODULES_MAVEN_PARAM=[' -P root-modules,cache-modules,spring-modules,http-modules,test-tooling-modules', ' -P security-modules,sql-db-modules,messaging-modules,websockets-modules,monitoring-modules']" >> $GITHUB_OUTPUT
fi
outputs:
MODULES_MAVEN_PARAM: ${{ steps.prepare-modules-mvn-param.outputs.MODULES_MAVEN_PARAM }}
@@ -73,7 +73,7 @@ jobs:
strategy:
matrix:
java: [ 17 ]
- cli: [ 3.2.8.Final ]
+ cli: [ 3.2.9.Final ]
module-mvn-args: ${{ fromJSON(needs.prepare-jvm-latest-modules-mvn-param.outputs.MODULES_MAVEN_PARAM) }}
steps:
- uses: actions/checkout@v3
@@ -120,7 +120,7 @@ jobs:
strategy:
matrix:
java: [ 11 ]
- cli: [ 3.2.8.Final ]
+ cli: [ 3.2.9.Final ]
steps:
- uses: actions/checkout@v3
- name: Reclaim Disk Space
diff --git a/README.md b/README.md
index 878cd1d47..a4278f6bc 100644
--- a/README.md
+++ b/README.md
@@ -47,6 +47,7 @@ The following subsections will introduce how to deploy and run the test suite in
If you have a look the main `pom.xml` you will notice that there are several profiles or in other words the test suite is a maven monorepo that you can compile and verify at once or by topics. Let's review the main profiles:
* root-modules: talk about Quarkus "core stuff" as configuration or properties. Is a basic stuff that should work as a pre-requisite to other modules.
+* cache-modules: cover Quarkus application data caching
* http-modules: talk about HTTP extensions and no-application endpoints like `/q/health`
* security-modules: cover all security stuff like OAuth, JWT, OpenId, Keycloak etc
* messaging-modules: is focus on brokers as Kafka or Artemis-AMQP
@@ -1084,6 +1085,15 @@ It covers different usages:
3. from a blocking endpoint
4. from a reactive endpoint
+### `cache/redis`
+
+Verifies the `quarkus-redis-cache` extension using `@CacheResult`, `@CacheInvalidate`, `@CacheInvalidateAll` and `@CacheKey`.
+It covers different usages:
+1. from an application scoped service
+2. from a request scoped service
+
+Also verify that Qute correctly indicate that does not work with remote cache.
+
### `cache/spring`
Verifies the `quarkus-spring-cache` extension using `@Cacheable`, `@CacheEvict` and `@CachePut`.
diff --git a/cache/redis/pom.xml b/cache/redis/pom.xml
new file mode 100644
index 000000000..51f1e140e
--- /dev/null
+++ b/cache/redis/pom.xml
@@ -0,0 +1,36 @@
+
+
+ 4.0.0
+
+ io.quarkus.ts.qe
+ parent
+ 1.0.0-SNAPSHOT
+ ../..
+
+ cache-redis
+ jar
+ Quarkus QE TS: Cache: Redis
+
+
+ io.quarkus
+ quarkus-cache
+
+
+ io.quarkus
+ quarkus-redis-cache
+
+
+ io.quarkus
+ quarkus-resteasy-reactive
+
+
+ io.quarkus
+ quarkus-qute
+
+ Added dependency to check https://github.com/quarkusio/quarkus/issues/35680 <-->
+
+ io.quarkus
+ quarkus-mailer
+
+
+
diff --git a/cache/redis/src/main/java/io/quarkus/ts/cache/redis/ApplicationScopeService.java b/cache/redis/src/main/java/io/quarkus/ts/cache/redis/ApplicationScopeService.java
new file mode 100644
index 000000000..baf9fbebb
--- /dev/null
+++ b/cache/redis/src/main/java/io/quarkus/ts/cache/redis/ApplicationScopeService.java
@@ -0,0 +1,7 @@
+package io.quarkus.ts.cache.caffeine;
+
+import jakarta.enterprise.context.ApplicationScoped;
+
+@ApplicationScoped
+public class ApplicationScopeService extends BaseServiceWithCache {
+}
diff --git a/cache/redis/src/main/java/io/quarkus/ts/cache/redis/BaseServiceWithCache.java b/cache/redis/src/main/java/io/quarkus/ts/cache/redis/BaseServiceWithCache.java
new file mode 100644
index 000000000..c93a2e55d
--- /dev/null
+++ b/cache/redis/src/main/java/io/quarkus/ts/cache/redis/BaseServiceWithCache.java
@@ -0,0 +1,38 @@
+package io.quarkus.ts.cache.caffeine;
+
+import io.quarkus.cache.CacheInvalidate;
+import io.quarkus.cache.CacheInvalidateAll;
+import io.quarkus.cache.CacheKey;
+import io.quarkus.cache.CacheResult;
+
+public abstract class BaseServiceWithCache {
+
+ private static final String CACHE_NAME = "service-cache";
+
+ private static int counter = 0;
+
+ @CacheResult(cacheName = CACHE_NAME)
+ public String getValue() {
+ return "Value: " + counter++;
+ }
+
+ @CacheInvalidate(cacheName = CACHE_NAME)
+ public void invalidate() {
+ // do nothing
+ }
+
+ @CacheResult(cacheName = CACHE_NAME)
+ public String getValueWithPrefix(@CacheKey String prefix) {
+ return prefix + ": " + counter++;
+ }
+
+ @CacheInvalidate(cacheName = CACHE_NAME)
+ public void invalidateWithPrefix(@CacheKey String prefix) {
+ // do nothing
+ }
+
+ @CacheInvalidateAll(cacheName = CACHE_NAME)
+ public void invalidateAll() {
+ // do nothing
+ }
+}
diff --git a/cache/redis/src/main/java/io/quarkus/ts/cache/redis/RequestScopeService.java b/cache/redis/src/main/java/io/quarkus/ts/cache/redis/RequestScopeService.java
new file mode 100644
index 000000000..8e12392b5
--- /dev/null
+++ b/cache/redis/src/main/java/io/quarkus/ts/cache/redis/RequestScopeService.java
@@ -0,0 +1,7 @@
+package io.quarkus.ts.cache.caffeine;
+
+import jakarta.enterprise.context.RequestScoped;
+
+@RequestScoped
+public class RequestScopeService extends BaseServiceWithCache {
+}
diff --git a/cache/redis/src/main/java/io/quarkus/ts/cache/redis/ServiceWithCacheResource.java b/cache/redis/src/main/java/io/quarkus/ts/cache/redis/ServiceWithCacheResource.java
new file mode 100644
index 000000000..e71870276
--- /dev/null
+++ b/cache/redis/src/main/java/io/quarkus/ts/cache/redis/ServiceWithCacheResource.java
@@ -0,0 +1,65 @@
+package io.quarkus.ts.cache.caffeine;
+
+import jakarta.inject.Inject;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+
+@Path("/services")
+public class ServiceWithCacheResource {
+
+ public static final String APPLICATION_SCOPE_SERVICE_PATH = "application-scope";
+ public static final String REQUEST_SCOPE_SERVICE_PATH = "request-scope";
+
+ @Inject
+ ApplicationScopeService applicationScopeService;
+
+ @Inject
+ RequestScopeService requestScopeService;
+
+ @GET
+ @Path("/{service}")
+ @Produces(MediaType.TEXT_PLAIN)
+ public String getValueFromService(@PathParam("service") String service) {
+ return lookupServiceByPathParam(service).getValue();
+ }
+
+ @POST
+ @Path("/{service}/invalidate-cache")
+ public void invalidateCacheFromService(@PathParam("service") String service) {
+ lookupServiceByPathParam(service).invalidate();
+ }
+
+ @POST
+ @Path("/{service}/invalidate-cache-all")
+ public void invalidateCacheAllFromService(@PathParam("service") String service) {
+ lookupServiceByPathParam(service).invalidateAll();
+ }
+
+ @GET
+ @Path("/{service}/using-prefix/{prefix}")
+ @Produces(MediaType.TEXT_PLAIN)
+ public String getValueUsingPrefixFromService(@PathParam("service") String service, @PathParam("prefix") String prefix) {
+ return lookupServiceByPathParam(service).getValueWithPrefix(prefix);
+ }
+
+ @POST
+ @Path("/{service}/using-prefix/{prefix}/invalidate-cache")
+ public void invalidateCacheUsingPrefixFromService(@PathParam("service") String service,
+ @PathParam("prefix") String prefix) {
+ lookupServiceByPathParam(service).invalidateWithPrefix(prefix);
+ }
+
+ private BaseServiceWithCache lookupServiceByPathParam(String service) {
+ if (APPLICATION_SCOPE_SERVICE_PATH.equals(service)) {
+ return applicationScopeService;
+ } else if (REQUEST_SCOPE_SERVICE_PATH.equals(service)) {
+ return requestScopeService;
+ }
+
+ throw new IllegalArgumentException("Service " + service + " is not recognised");
+ }
+}
diff --git a/cache/redis/src/main/java/io/quarkus/ts/cache/redis/TemplateCacheResource.java b/cache/redis/src/main/java/io/quarkus/ts/cache/redis/TemplateCacheResource.java
new file mode 100644
index 000000000..0e2bb2a76
--- /dev/null
+++ b/cache/redis/src/main/java/io/quarkus/ts/cache/redis/TemplateCacheResource.java
@@ -0,0 +1,31 @@
+package io.quarkus.ts.cache.caffeine;
+
+import jakarta.inject.Inject;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+
+import io.quarkus.qute.Template;
+
+@Path("template")
+public class TemplateCacheResource {
+
+ @Inject
+ Template cached;
+
+ /**
+ * Check for remote cache with Qute template. Qute should not use remote cache.
+ * See https://github.com/quarkusio/quarkus/issues/35680#issuecomment-1711153725
+ *
+ * @return Should return error contains `not supported for remote caches`
+ */
+ @GET
+ @Path("error")
+ public String getQuteTemplate() {
+ try {
+ return cached.render();
+ } catch (IllegalStateException e) {
+ return e.getMessage();
+ }
+ }
+
+}
diff --git a/cache/redis/src/main/resources/application.properties b/cache/redis/src/main/resources/application.properties
new file mode 100644
index 000000000..e69de29bb
diff --git a/cache/redis/src/main/resources/templates/cached.html b/cache/redis/src/main/resources/templates/cached.html
new file mode 100644
index 000000000..55584772d
--- /dev/null
+++ b/cache/redis/src/main/resources/templates/cached.html
@@ -0,0 +1 @@
+{#cached}This cached template won't be working with remote cache like redis.{/cached}
diff --git a/cache/redis/src/test/java/io/quarkus/ts/cache/redis/OpenShiftRedisCacheIT.java b/cache/redis/src/test/java/io/quarkus/ts/cache/redis/OpenShiftRedisCacheIT.java
new file mode 100644
index 000000000..e96cf029d
--- /dev/null
+++ b/cache/redis/src/test/java/io/quarkus/ts/cache/redis/OpenShiftRedisCacheIT.java
@@ -0,0 +1,7 @@
+package io.quarkus.ts.cache.redis;
+
+import io.quarkus.test.scenarios.OpenShiftScenario;
+
+@OpenShiftScenario
+public class OpenShiftRedisCacheIT extends RedisCacheIT {
+}
diff --git a/cache/redis/src/test/java/io/quarkus/ts/cache/redis/RedisCacheIT.java b/cache/redis/src/test/java/io/quarkus/ts/cache/redis/RedisCacheIT.java
new file mode 100644
index 000000000..c5ef69b48
--- /dev/null
+++ b/cache/redis/src/test/java/io/quarkus/ts/cache/redis/RedisCacheIT.java
@@ -0,0 +1,187 @@
+package io.quarkus.ts.cache.redis;
+
+import static io.quarkus.ts.cache.caffeine.ServiceWithCacheResource.APPLICATION_SCOPE_SERVICE_PATH;
+import static io.quarkus.ts.cache.caffeine.ServiceWithCacheResource.REQUEST_SCOPE_SERVICE_PATH;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import io.quarkus.test.bootstrap.DefaultService;
+import io.quarkus.test.bootstrap.RestService;
+import io.quarkus.test.scenarios.QuarkusScenario;
+import io.quarkus.test.services.Container;
+import io.quarkus.test.services.QuarkusApplication;
+
+@QuarkusScenario
+public class RedisCacheIT {
+
+ private static final String SERVICE_APPLICATION_SCOPE_PATH = "/services/" + APPLICATION_SCOPE_SERVICE_PATH;
+ private static final String SERVICE_REQUEST_SCOPE_PATH = "/services/" + REQUEST_SCOPE_SERVICE_PATH;
+
+ private static final String PREFIX_ONE = "prefix1";
+ private static final String PREFIX_TWO = "prefix2";
+
+ private static final int REDIS_PORT = 6379;
+
+ @Container(image = "${redis.image}", port = REDIS_PORT, expectedLog = "Ready to accept connections")
+ static DefaultService redis = new DefaultService().withProperty("ALLOW_EMPTY_PASSWORD", "YES");
+
+ @QuarkusApplication
+ static RestService app = new RestService()
+ .withProperty("quarkus.redis.hosts",
+ () -> {
+ String redisHost = redis.getURI().withScheme("redis").getRestAssuredStyleUri();
+ return String.format("%s:%d", redisHost, redis.getURI().getPort());
+ });
+
+ /**
+ * Check whether the `@CacheResult` annotation works when used in a service.
+ */
+ @ParameterizedTest
+ @ValueSource(strings = { SERVICE_APPLICATION_SCOPE_PATH, SERVICE_REQUEST_SCOPE_PATH })
+ public void shouldGetTheSameValueAlwaysWhenGettingValueFromPath(String path) {
+ // We call the service endpoint
+ String value = getFromPath(path);
+
+ // At this point, the cache is populated and we should get the same value from the cache
+ assertEquals(value, getFromPath(path), "Value was different which means cache is not working");
+ }
+
+ /**
+ * Check whether the `@CacheInvalidate` annotation invalidates the cache when used in a service.
+ */
+ @ParameterizedTest
+ @ValueSource(strings = { SERVICE_APPLICATION_SCOPE_PATH, SERVICE_REQUEST_SCOPE_PATH })
+ public void shouldGetDifferentValueWhenInvalidateCacheFromPath(String path) {
+ // We call the service endpoint
+ String value = getFromPath(path);
+
+ // invalidate the cache
+ invalidateCacheFromPath(path);
+
+ // Then the value should be different as we have invalidated the cache.
+ assertNotEquals(value, getFromPath(path), "Value was equal which means cache invalidate didn't work");
+ }
+
+ /**
+ * Check whether the `@CacheResult` annotation works when used in a service.
+ */
+ @ParameterizedTest
+ @ValueSource(strings = { SERVICE_APPLICATION_SCOPE_PATH, SERVICE_REQUEST_SCOPE_PATH })
+ public void shouldGetTheSameValueForSamePrefixesWhenGettingValueFromPath(String path) {
+ // We call the service endpoint
+ String value = getValueFromPathUsingPrefix(path, PREFIX_ONE);
+
+ // At this point, the cache is populated and we should get the same value from the cache
+ assertEquals(value, getValueFromPathUsingPrefix(path, PREFIX_ONE),
+ "Value was different which means cache is not working");
+ // But different value using another prefix
+ assertNotEquals(value, getValueFromPathUsingPrefix(path, PREFIX_TWO),
+ "Value was equal which means @CacheKey didn't work");
+ }
+
+ /**
+ * Check whether the `@CacheInvalidate` annotation does not invalidate all the caches
+ */
+ @ParameterizedTest
+ @ValueSource(strings = { SERVICE_APPLICATION_SCOPE_PATH, SERVICE_REQUEST_SCOPE_PATH })
+ public void shouldGetTheSameValuesEvenAfterCallingToCacheInvalidateFromPath(String path) {
+ // We call the service endpoints
+ String valueOfPrefix1 = getValueFromPathUsingPrefix(path, PREFIX_ONE);
+ String valueOfPrefix2 = getValueFromPathUsingPrefix(path, PREFIX_TWO);
+
+ // invalidate the cache: this should not invalidate all the keys
+ invalidateCacheFromPath(path);
+
+ // At this point, the cache is populated and we should get the same value for both prefixes
+ assertEquals(valueOfPrefix1, getValueFromPathUsingPrefix(path, PREFIX_ONE));
+ assertEquals(valueOfPrefix2, getValueFromPathUsingPrefix(path, PREFIX_TWO));
+ }
+
+ /**
+ * Check whether the `@CacheInvalidate` and `@CacheKey` annotations work as expected.
+ */
+ @ParameterizedTest
+ @ValueSource(strings = { SERVICE_APPLICATION_SCOPE_PATH, SERVICE_REQUEST_SCOPE_PATH })
+ public void shouldGetDifferentValueWhenInvalidateCacheOnlyForOnePrefixFromPath(String path) {
+ // We call the service endpoints
+ String valueOfPrefix1 = getValueFromPathUsingPrefix(path, PREFIX_ONE);
+ String valueOfPrefix2 = getValueFromPathUsingPrefix(path, PREFIX_TWO);
+
+ // invalidate the cache: this should not invalidate all the keys
+ invalidateCacheWithPrefixFromPath(path, PREFIX_ONE);
+
+ // The cache was invalidated only for prefix1, so the value should be different
+ assertNotEquals(valueOfPrefix1, getValueFromPathUsingPrefix(path, PREFIX_ONE));
+ // The cache was not invalidated for prefix2, so the value should be the same
+ assertEquals(valueOfPrefix2, getValueFromPathUsingPrefix(path, PREFIX_TWO));
+ }
+
+ /**
+ * Check whether the `@CacheInvalidateAll` annotation works as expected.
+ */
+ @ParameterizedTest
+ @ValueSource(strings = { SERVICE_APPLICATION_SCOPE_PATH, SERVICE_REQUEST_SCOPE_PATH })
+ public void shouldGetDifferentValueWhenInvalidateAllTheCacheFromPath(String path) {
+ // We call the service endpoints
+ String value = getFromPath(path);
+ String valueOfPrefix1 = getValueFromPathUsingPrefix(path, PREFIX_ONE);
+ String valueOfPrefix2 = getValueFromPathUsingPrefix(path, PREFIX_TWO);
+
+ // invalidate all the cache
+ invalidateCacheAllFromPath(path);
+
+ // Then, all the values should be different:
+ assertNotEquals(value, getFromPath(path));
+ assertNotEquals(valueOfPrefix1, getValueFromPathUsingPrefix(path, PREFIX_ONE));
+ assertNotEquals(valueOfPrefix2, getValueFromPathUsingPrefix(path, PREFIX_TWO));
+ }
+
+ /**
+ * Check if the usage of Qute and redis throw expected error
+ */
+ @Test
+ @Tag("QUARKUS-3715")
+ public void quteShouldThrowError() {
+ assertThat(getFromPath("/template/error"), containsString("not supported for remote caches"));
+ }
+
+ private void invalidateCacheAllFromPath(String path) {
+ postFromPath(path + "/invalidate-cache-all");
+ }
+
+ private void invalidateCacheWithPrefixFromPath(String path, String prefix) {
+ postFromPath(path + "/using-prefix/" + prefix + "/invalidate-cache");
+ }
+
+ private void invalidateCacheFromPath(String path) {
+ postFromPath(path + "/invalidate-cache");
+ }
+
+ private String getValueFromPathUsingPrefix(String path, String prefix) {
+ return getFromPath(path + "/using-prefix/" + prefix);
+ }
+
+ private String getFromPath(String path) {
+ return app.given()
+ .when().get(path)
+ .then()
+ .statusCode(HttpStatus.SC_OK)
+ .extract().asString();
+ }
+
+ private void postFromPath(String path) {
+ app.given()
+ .when().post(path)
+ .then()
+ .statusCode(HttpStatus.SC_NO_CONTENT);
+ }
+
+}
diff --git a/cache/redis/src/test/resources/test.properties b/cache/redis/src/test/resources/test.properties
new file mode 100644
index 000000000..a72083e30
--- /dev/null
+++ b/cache/redis/src/test/resources/test.properties
@@ -0,0 +1 @@
+ts.redis.openshift.use-internal-service-as-url=true
diff --git a/http/http-advanced-reactive/src/main/java/io/quarkus/ts/http/advanced/reactive/CookiesResource.java b/http/http-advanced-reactive/src/main/java/io/quarkus/ts/http/advanced/reactive/CookiesResource.java
index 5d3d03644..b9a17e53c 100644
--- a/http/http-advanced-reactive/src/main/java/io/quarkus/ts/http/advanced/reactive/CookiesResource.java
+++ b/http/http-advanced-reactive/src/main/java/io/quarkus/ts/http/advanced/reactive/CookiesResource.java
@@ -1,5 +1,7 @@
package io.quarkus.ts.http.advanced.reactive;
+import java.util.Map;
+
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.CookieParam;
import jakarta.ws.rs.FormParam;
@@ -56,6 +58,19 @@ public Response getSameSiteAttributeFromFormParam(@FormParam(TEST_COOKIE) String
return responseBuilder.cookie(newCookie).build();
}
+ @GET
+ @Path("newcookie-serialization")
+ public Map getRequestCookies(HttpHeaders httpHeaders) {
+ NewCookie cookie = new NewCookie.Builder(TEST_COOKIE).value("test-cookie-value").build();
+ return Map.of(cookie.getName(), cookie);
+ }
+
+ @GET
+ @Path("cookie-serialization")
+ public Map test(HttpHeaders httpHeaders) {
+ return httpHeaders.getCookies();
+ }
+
public static String toRawCookie(String sameSite) {
if (sameSite == null || sameSite.isEmpty()) {
return String.format("%s=\"test-cookie-value\";Version=\"1\";", TEST_COOKIE);
diff --git a/http/http-advanced-reactive/src/main/java/io/quarkus/ts/http/advanced/reactive/InterceptedResource.java b/http/http-advanced-reactive/src/main/java/io/quarkus/ts/http/advanced/reactive/InterceptedResource.java
new file mode 100644
index 000000000..28440613a
--- /dev/null
+++ b/http/http-advanced-reactive/src/main/java/io/quarkus/ts/http/advanced/reactive/InterceptedResource.java
@@ -0,0 +1,123 @@
+package io.quarkus.ts.http.advanced.reactive;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.annotation.Annotation;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.reflect.Type;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+
+import jakarta.ws.rs.ConstrainedTo;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.NameBinding;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.RuntimeType;
+import jakarta.ws.rs.WebApplicationException;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.MultivaluedMap;
+import jakarta.ws.rs.ext.MessageBodyWriter;
+import jakarta.ws.rs.ext.Provider;
+import jakarta.ws.rs.ext.WriterInterceptor;
+import jakarta.ws.rs.ext.WriterInterceptorContext;
+
+@Path("/intercepted")
+public class InterceptedResource {
+
+ /**
+ * Interceptors write their message to this list, when they are invoked
+ * It is a bit dumb way, but it is the easier to get indicators if interceptors were invoked to the client
+ */
+ public static List interceptorMessages = new ArrayList<>();
+
+ @WithWriterInterceptor
+ @GET
+ public InterceptedString getInterceptedString() {
+ return new InterceptedString("foo");
+ }
+
+ @GET()
+ @Path("/messages")
+ @Produces(MediaType.TEXT_PLAIN)
+ public String getMessages() {
+ StringBuilder outputMessage = new StringBuilder();
+ for (String string : interceptorMessages) {
+ outputMessage.append(string);
+ }
+ return outputMessage.toString();
+ }
+
+ public static class InterceptedString {
+ public String name;
+
+ public InterceptedString(String name) {
+ this.name = name;
+ }
+ }
+
+ /**
+ * This annotation binds the providers to only intercept the method in this class.
+ * Otherwise, they would be global and intercept all endpoints across the entire application.
+ */
+ @NameBinding
+ @Target({ ElementType.METHOD, ElementType.TYPE })
+ @Retention(RetentionPolicy.RUNTIME)
+ public @interface WithWriterInterceptor {
+
+ }
+
+ @Provider
+ public static class InterceptedStringHandler implements MessageBodyWriter {
+ @Override
+ public boolean isWriteable(Class> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
+ return type == InterceptedString.class;
+ }
+
+ @Override
+ public void writeTo(InterceptedString interceptedString, Class> type, Type genericType, Annotation[] annotations,
+ MediaType mediaType,
+ MultivaluedMap httpHeaders, OutputStream entityStream)
+ throws IOException, WebApplicationException {
+ entityStream.write((interceptedString.name).getBytes(StandardCharsets.UTF_8));
+ }
+ }
+
+ @Provider
+ @WithWriterInterceptor
+ public static class UnconstrainedWriterInterceptor implements WriterInterceptor {
+ @Override
+ public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException {
+ InterceptedResource.interceptorMessages.add("Unconstrained interceptor ");
+ context.proceed();
+ }
+ }
+
+ @Provider
+ @ConstrainedTo(RuntimeType.CLIENT)
+ @WithWriterInterceptor
+ public static class ClientWriterInterceptor implements WriterInterceptor {
+
+ @Override
+ public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException {
+ InterceptedResource.interceptorMessages.add("Client interceptor ");
+ context.proceed();
+ }
+ }
+
+ @Provider
+ @ConstrainedTo(RuntimeType.SERVER)
+ @WithWriterInterceptor
+ public static class ServerWriterInterceptor implements WriterInterceptor {
+
+ @Override
+ public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException {
+ InterceptedResource.interceptorMessages.add("Server interceptor ");
+ context.proceed();
+ }
+ }
+}
diff --git a/http/http-advanced-reactive/src/main/resources/application.properties b/http/http-advanced-reactive/src/main/resources/application.properties
index bc40751d2..87a9528cc 100644
--- a/http/http-advanced-reactive/src/main/resources/application.properties
+++ b/http/http-advanced-reactive/src/main/resources/application.properties
@@ -60,6 +60,8 @@ quarkus.keycloak.policy-enforcer.paths.grpc.path=/api/grpc/*
quarkus.keycloak.policy-enforcer.paths.grpc.enforcement-mode=DISABLED
quarkus.keycloak.policy-enforcer.paths.client.path=/api/client/*
quarkus.keycloak.policy-enforcer.paths.client.enforcement-mode=DISABLED
+quarkus.keycloak.policy-enforcer.paths.intercepted.path=/api/intercepted*
+quarkus.keycloak.policy-enforcer.paths.intercepted.enforcement-mode=DISABLED
quarkus.oidc.client-id=test-application-client
quarkus.oidc.credentials.secret=test-application-client-secret
# tolerate 1 minute of clock skew between the Keycloak server and the application
diff --git a/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/BaseHttpAdvancedReactiveIT.java b/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/BaseHttpAdvancedReactiveIT.java
index 5b01aa253..2452f8585 100644
--- a/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/BaseHttpAdvancedReactiveIT.java
+++ b/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/BaseHttpAdvancedReactiveIT.java
@@ -427,6 +427,24 @@ public void constraintsExist() throws JsonProcessingException {
Assertions.assertEquals("^[A-Za-z]+$", validation.get("pattern").asText());
}
+ @Test
+ @Tag("https://github.com/quarkusio/quarkus/pull/36664")
+ public void interceptedTest() {
+ // make server to generate a response so interceptors might intercept it
+ // ignore response, we will read interceptors result later
+ getApp().given()
+ .get(ROOT_PATH + "/intercepted")
+ .thenReturn();
+
+ String response = getApp().given()
+ .get(ROOT_PATH + "/intercepted/messages")
+ .thenReturn().getBody().asString();
+
+ Assertions.assertTrue(response.contains("Unconstrained"), "Unconstrained interceptor should be invoked");
+ Assertions.assertTrue(response.contains("Server"), "Server interceptor should be invoked");
+ Assertions.assertFalse(response.contains("Client"), "Client interceptor should not be invoked");
+ }
+
private void assertAcceptedMediaTypeEqualsResponseBody(String acceptedMediaType) {
getApp()
.given()
diff --git a/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/CookiesIT.java b/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/CookiesIT.java
index fadc9ea1c..de0094489 100644
--- a/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/CookiesIT.java
+++ b/http/http-advanced-reactive/src/test/java/io/quarkus/ts/http/advanced/reactive/CookiesIT.java
@@ -1,11 +1,12 @@
package io.quarkus.ts.http.advanced.reactive;
import static io.quarkus.ts.http.advanced.reactive.CookiesResource.TEST_COOKIE;
-import static io.restassured.RestAssured.get;
import static io.restassured.RestAssured.given;
import static io.restassured.matcher.RestAssuredMatchers.detailedCookie;
+import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
+import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import io.quarkus.test.bootstrap.RestService;
@@ -52,6 +53,28 @@ void testSameSiteAttributeAddedByVertxHttpExt() {
.cookie("vertx", detailedCookie().sameSite(sameSite).secured(true));
}
+ @Test
+ @Tag("QUARKUS-3736")
+ void testNewCookiesSerialization() {
+ given()
+ .get("/cookie/newcookie-serialization")
+ .then()
+ .statusCode(200)
+ .body(containsString(String.format("\"name\":\"%s\"", TEST_COOKIE)),
+ containsString("\"value\":\"test-cookie-value\""));
+ }
+
+ @Test
+ @Tag("QUARKUS-3736")
+ void testCookiesSerialization() {
+ given().cookie(String.format("%s=\"test-cookie-value\";", TEST_COOKIE))
+ .get("/cookie/cookie-serialization")
+ .then()
+ .statusCode(200)
+ .body(containsString(String.format("\"name\":\"%s\"", TEST_COOKIE)),
+ containsString("\"value\":\"test-cookie-value\""));
+ }
+
private static void assertSameSiteAttribute(String sameSite) {
ValidatableResponse response;
diff --git a/http/http-advanced/src/main/java/io/quarkus/ts/http/advanced/InterceptedResource.java b/http/http-advanced/src/main/java/io/quarkus/ts/http/advanced/InterceptedResource.java
new file mode 100644
index 000000000..008e04408
--- /dev/null
+++ b/http/http-advanced/src/main/java/io/quarkus/ts/http/advanced/InterceptedResource.java
@@ -0,0 +1,123 @@
+package io.quarkus.ts.http.advanced;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.lang.annotation.Annotation;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.reflect.Type;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+
+import jakarta.ws.rs.ConstrainedTo;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.NameBinding;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.RuntimeType;
+import jakarta.ws.rs.WebApplicationException;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.MultivaluedMap;
+import jakarta.ws.rs.ext.MessageBodyWriter;
+import jakarta.ws.rs.ext.Provider;
+import jakarta.ws.rs.ext.WriterInterceptor;
+import jakarta.ws.rs.ext.WriterInterceptorContext;
+
+@Path("/intercepted")
+public class InterceptedResource {
+
+ /**
+ * Interceptors write their message to this list, when they are invoked
+ * It is a bit dumb way, but it is the easier to get indicators if interceptors were invoked to the client
+ */
+ public static List interceptorMessages = new ArrayList<>();
+
+ @WithWriterInterceptor
+ @GET
+ public InterceptedString getInterceptedString() {
+ return new InterceptedString("foo");
+ }
+
+ @GET()
+ @Path("/messages")
+ @Produces(MediaType.TEXT_PLAIN)
+ public String getMessages() {
+ StringBuilder outputMessage = new StringBuilder();
+ for (String string : interceptorMessages) {
+ outputMessage.append(string);
+ }
+ return outputMessage.toString();
+ }
+
+ public static class InterceptedString {
+ public String name;
+
+ public InterceptedString(String name) {
+ this.name = name;
+ }
+ }
+
+ /**
+ * This annotation binds the providers to only intercept the method in this class.
+ * Otherwise, they would be global and intercept all endpoints across the entire application.
+ */
+ @NameBinding
+ @Target({ ElementType.METHOD, ElementType.TYPE })
+ @Retention(RetentionPolicy.RUNTIME)
+ public @interface WithWriterInterceptor {
+
+ }
+
+ @Provider
+ public static class InterceptedStringHandler implements MessageBodyWriter {
+ @Override
+ public boolean isWriteable(Class> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
+ return type == InterceptedString.class;
+ }
+
+ @Override
+ public void writeTo(InterceptedString interceptedString, Class> type, Type genericType, Annotation[] annotations,
+ MediaType mediaType,
+ MultivaluedMap httpHeaders, OutputStream entityStream)
+ throws IOException, WebApplicationException {
+ entityStream.write((interceptedString.name).getBytes(StandardCharsets.UTF_8));
+ }
+ }
+
+ @Provider
+ @WithWriterInterceptor
+ public static class UnconstrainedWriterInterceptor implements WriterInterceptor {
+ @Override
+ public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException {
+ InterceptedResource.interceptorMessages.add("Unconstrained interceptor ");
+ context.proceed();
+ }
+ }
+
+ @Provider
+ @ConstrainedTo(RuntimeType.CLIENT)
+ @WithWriterInterceptor
+ public static class ClientWriterInterceptor implements WriterInterceptor {
+
+ @Override
+ public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException {
+ InterceptedResource.interceptorMessages.add("Client interceptor ");
+ context.proceed();
+ }
+ }
+
+ @Provider
+ @ConstrainedTo(RuntimeType.SERVER)
+ @WithWriterInterceptor
+ public static class ServerWriterInterceptor implements WriterInterceptor {
+
+ @Override
+ public void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException {
+ InterceptedResource.interceptorMessages.add("Server interceptor ");
+ context.proceed();
+ }
+ }
+}
diff --git a/http/http-advanced/src/main/java/io/quarkus/ts/http/advanced/SseResource.java b/http/http-advanced/src/main/java/io/quarkus/ts/http/advanced/SseResource.java
new file mode 100644
index 000000000..79b327a45
--- /dev/null
+++ b/http/http-advanced/src/main/java/io/quarkus/ts/http/advanced/SseResource.java
@@ -0,0 +1,77 @@
+package io.quarkus.ts.http.advanced;
+
+import java.util.Arrays;
+import java.util.concurrent.locks.LockSupport;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.client.ClientBuilder;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Context;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.sse.OutboundSseEvent;
+import jakarta.ws.rs.sse.Sse;
+import jakarta.ws.rs.sse.SseEventSink;
+import jakarta.ws.rs.sse.SseEventSource;
+
+import org.eclipse.microprofile.config.ConfigProvider;
+
+@Path("/sse")
+public class SseResource {
+ @Context
+ Sse sse;
+
+ @GET
+ @Path("/client")
+ public String sseClient() {
+ try {
+ return consumeSse();
+ }
+ // in case that https://github.com/quarkusio/quarkus/issues/36402 throws java.lang.RuntimeException: java.lang.ClassNotFoundException:
+ // catch it and return the error message
+ catch (RuntimeException exception) {
+ return exception.getMessage();
+ }
+ }
+
+ private String consumeSse() {
+ StringBuilder response = new StringBuilder();
+ int port = ConfigProvider.getConfig().getValue("quarkus.http.port", Integer.class);
+
+ /*
+ * Client connects to itself (to server endpoint running on same app),
+ * because for https://github.com/quarkusio/quarkus/issues/36402 to reproduce client must run on native app.
+ * Which cannot be done in test code itself.
+ * This method acts just as a client
+ */
+ WebTarget target = ClientBuilder.newClient().target("http://localhost:" + port + "/api/sse/server");
+ SseEventSource updateSource = SseEventSource.target(target).build();
+ updateSource.register(ev -> {
+ response.append("event: ").append(ev.getName()).append(" ").append(ev.readData());
+ response.append("\n");
+
+ }, thr -> {
+ response.append("Error in SSE, message: ").append(thr.getMessage()).append("\n");
+ response.append(Arrays.toString(thr.getStackTrace()));
+ });
+ updateSource.open();
+
+ LockSupport.parkNanos(1_000_000_000L);
+ return response.toString();
+ }
+
+ @GET
+ @Path("/server")
+ @Produces(MediaType.SERVER_SENT_EVENTS)
+ public void sendSseEvents(@Context SseEventSink eventSink) {
+ eventSink.send(createEvent("test234", "test"));
+ }
+
+ private OutboundSseEvent createEvent(String name, String data) {
+ return sse.newEventBuilder()
+ .name(name)
+ .data(data)
+ .build();
+ }
+}
diff --git a/http/http-advanced/src/main/resources/application.properties b/http/http-advanced/src/main/resources/application.properties
index 904607fef..da1dfa3e3 100644
--- a/http/http-advanced/src/main/resources/application.properties
+++ b/http/http-advanced/src/main/resources/application.properties
@@ -45,12 +45,16 @@ quarkus.keycloak.policy-enforcer.paths.version.enforcement-mode=DISABLED
# Application endpoints
quarkus.keycloak.policy-enforcer.paths.hello.path=/api/hello/*
quarkus.keycloak.policy-enforcer.paths.hello.enforcement-mode=DISABLED
+quarkus.keycloak.policy-enforcer.paths.sse.path=/api/sse/*
+quarkus.keycloak.policy-enforcer.paths.sse.enforcement-mode=DISABLED
quarkus.keycloak.policy-enforcer.paths.details.path=/api/details/*
quarkus.keycloak.policy-enforcer.paths.details.enforcement-mode=DISABLED
quarkus.keycloak.policy-enforcer.paths.grpc.path=/api/grpc/*
quarkus.keycloak.policy-enforcer.paths.grpc.enforcement-mode=DISABLED
quarkus.keycloak.policy-enforcer.paths.client.path=/api/client/*
quarkus.keycloak.policy-enforcer.paths.client.enforcement-mode=DISABLED
+quarkus.keycloak.policy-enforcer.paths.intercepted.path=/api/intercepted*
+quarkus.keycloak.policy-enforcer.paths.intercepted.enforcement-mode=DISABLED
quarkus.oidc.client-id=test-application-client
quarkus.oidc.credentials.secret=test-application-client-secret
# tolerate 1 minute of clock skew between the Keycloak server and the application
diff --git a/http/http-advanced/src/test/java/io/quarkus/ts/http/advanced/BaseHttpAdvancedIT.java b/http/http-advanced/src/test/java/io/quarkus/ts/http/advanced/BaseHttpAdvancedIT.java
index 0f943a88d..d28e33aef 100644
--- a/http/http-advanced/src/test/java/io/quarkus/ts/http/advanced/BaseHttpAdvancedIT.java
+++ b/http/http-advanced/src/test/java/io/quarkus/ts/http/advanced/BaseHttpAdvancedIT.java
@@ -8,6 +8,8 @@
import static org.hamcrest.Matchers.in;
import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import java.net.URISyntaxException;
@@ -60,6 +62,7 @@ public abstract class BaseHttpAdvancedIT {
private static final String PASSWORD = "password";
private static final String KEY_STORE_PATH = "META-INF/resources/server.keystore";
private static final int ASSERT_TIMEOUT_SECONDS = 10;
+ private static final String SSE_ERROR_MESSAGE = "java.lang.ClassNotFoundException: Provider for jakarta.ws.rs.sse.SseEventSource.Builder cannot be found";
protected abstract RestService getApp();
@@ -242,6 +245,34 @@ public void keepRequestScopeValuesAfterEventPropagation() {
"Unexpected requestScope custom context value");
}
+ @Test
+ @Tag("https://github.com/quarkusio/quarkus/issues/36402")
+ public void sseConnectionTest() {
+ String response = getApp().given().get("/api/sse/client").thenReturn().body().asString();
+
+ assertFalse(response.contains(SSE_ERROR_MESSAGE),
+ "SSE failed, https://github.com/quarkusio/quarkus/issues/36402 not fixed");
+ assertTrue(response.contains("event: test234 test"), "SSE failed, unknown bug. Response: " + response);
+ }
+
+ @Test
+ @Tag("https://github.com/quarkusio/quarkus/pull/36664")
+ public void interceptedTest() {
+ // make server to generate a response so interceptors might intercept it
+ // ignore response, we will read interceptors result later
+ getApp().given()
+ .get(ROOT_PATH + "/intercepted")
+ .thenReturn();
+
+ String response = getApp().given()
+ .get(ROOT_PATH + "/intercepted/messages")
+ .thenReturn().getBody().asString();
+
+ Assertions.assertTrue(response.contains("Unconstrained"), "Unconstrained interceptor should be invoked");
+ Assertions.assertTrue(response.contains("Server"), "Server interceptor should be invoked");
+ Assertions.assertFalse(response.contains("Client"), "Client interceptor should not be invoked");
+ }
+
protected Protocol getProtocol() {
return Protocol.HTTPS;
}
diff --git a/nosql-db/mongodb-reactive/src/test/java/io/quarkus/ts/nosqldb/mongodb/reactive/OpenShiftMongoClientReactiveIT.java b/nosql-db/mongodb-reactive/src/test/java/io/quarkus/ts/nosqldb/mongodb/reactive/OpenShiftMongoClientReactiveIT.java
index c56027d24..0c0d7ec4a 100644
--- a/nosql-db/mongodb-reactive/src/test/java/io/quarkus/ts/nosqldb/mongodb/reactive/OpenShiftMongoClientReactiveIT.java
+++ b/nosql-db/mongodb-reactive/src/test/java/io/quarkus/ts/nosqldb/mongodb/reactive/OpenShiftMongoClientReactiveIT.java
@@ -9,7 +9,6 @@
import io.quarkus.test.services.QuarkusApplication;
@OpenShiftScenario
-@DisabledIfSystemProperty(named = "ts.arm.missing.services.excludes", matches = "true", disabledReason = "https://github.com/quarkus-qe/quarkus-test-suite/issues/1146")
@DisabledIfSystemProperty(named = "ts.s390x.missing.services.excludes", matches = "true", disabledReason = "bitnami/mongodb container not available on s390x.")
public class OpenShiftMongoClientReactiveIT extends AbstractMongoClientReactiveIT {
diff --git a/nosql-db/mongodb-reactive/src/test/resources/mongo-openshift-deployment-template.yml b/nosql-db/mongodb-reactive/src/test/resources/mongo-openshift-deployment-template.yml
new file mode 100644
index 000000000..88a1e7f0b
--- /dev/null
+++ b/nosql-db/mongodb-reactive/src/test/resources/mongo-openshift-deployment-template.yml
@@ -0,0 +1,46 @@
+---
+apiVersion: "v1"
+kind: "List"
+items:
+- apiVersion: "v1"
+ kind: "Service"
+ metadata:
+ name: "${SERVICE_NAME}"
+ spec:
+ ports:
+ - name: "http"
+ port: ${INTERNAL_PORT}
+ targetPort: ${INTERNAL_PORT}
+ selector:
+ deploymentconfig: "${SERVICE_NAME}"
+ type: "ClusterIP"
+- apiVersion: "apps.openshift.io/v1"
+ kind: "DeploymentConfig"
+ metadata:
+ name: "${SERVICE_NAME}"
+ spec:
+ replicas: 1
+ selector:
+ deploymentconfig: "${SERVICE_NAME}"
+ template:
+ metadata:
+ labels:
+ deploymentconfig: "${SERVICE_NAME}"
+ spec:
+ volumes:
+ - name: mongo-db-data-volume
+ emptyDir: { }
+ containers:
+ - image: "${IMAGE}"
+ args: [${ARGS}]
+ imagePullPolicy: "IfNotPresent"
+ name: "${SERVICE_NAME}"
+ ports:
+ - containerPort: ${INTERNAL_PORT}
+ name: "http"
+ protocol: "TCP"
+ volumeMounts:
+ - name: mongo-db-data-volume
+ mountPath: /data/db
+ triggers:
+ - type: "ConfigChange"
diff --git a/nosql-db/mongodb-reactive/src/test/resources/test.properties b/nosql-db/mongodb-reactive/src/test/resources/test.properties
index 0b033fb40..e11c97950 100644
--- a/nosql-db/mongodb-reactive/src/test/resources/test.properties
+++ b/nosql-db/mongodb-reactive/src/test/resources/test.properties
@@ -1,3 +1,5 @@
ts.app.log.enable=true
ts.database.log.enable=true
ts.database.openshift.use-internal-service-as-url=true
+# store MongoDB data in 'emptyDir' volume to work-around write permission issue
+ts.database.openshift.template=/mongo-openshift-deployment-template.yml
diff --git a/nosql-db/mongodb/src/test/java/io/quarkus/ts/nosqldb/mongodb/OpenShiftMongoClientIT.java b/nosql-db/mongodb/src/test/java/io/quarkus/ts/nosqldb/mongodb/OpenShiftMongoClientIT.java
index b05bcb4ba..6b2dbaf8a 100644
--- a/nosql-db/mongodb/src/test/java/io/quarkus/ts/nosqldb/mongodb/OpenShiftMongoClientIT.java
+++ b/nosql-db/mongodb/src/test/java/io/quarkus/ts/nosqldb/mongodb/OpenShiftMongoClientIT.java
@@ -5,7 +5,6 @@
import io.quarkus.test.scenarios.OpenShiftScenario;
@OpenShiftScenario
-@DisabledIfSystemProperty(named = "ts.arm.missing.services.excludes", matches = "true", disabledReason = "https://github.com/quarkus-qe/quarkus-test-suite/issues/1146")
@DisabledIfSystemProperty(named = "ts.s390x.missing.services.excludes", matches = "true", disabledReason = "bitnami/mongodb container not available on s390x.")
public class OpenShiftMongoClientIT extends MongoClientIT {
}
diff --git a/nosql-db/mongodb/src/test/resources/mongo-openshift-deployment-template.yml b/nosql-db/mongodb/src/test/resources/mongo-openshift-deployment-template.yml
new file mode 100644
index 000000000..88a1e7f0b
--- /dev/null
+++ b/nosql-db/mongodb/src/test/resources/mongo-openshift-deployment-template.yml
@@ -0,0 +1,46 @@
+---
+apiVersion: "v1"
+kind: "List"
+items:
+- apiVersion: "v1"
+ kind: "Service"
+ metadata:
+ name: "${SERVICE_NAME}"
+ spec:
+ ports:
+ - name: "http"
+ port: ${INTERNAL_PORT}
+ targetPort: ${INTERNAL_PORT}
+ selector:
+ deploymentconfig: "${SERVICE_NAME}"
+ type: "ClusterIP"
+- apiVersion: "apps.openshift.io/v1"
+ kind: "DeploymentConfig"
+ metadata:
+ name: "${SERVICE_NAME}"
+ spec:
+ replicas: 1
+ selector:
+ deploymentconfig: "${SERVICE_NAME}"
+ template:
+ metadata:
+ labels:
+ deploymentconfig: "${SERVICE_NAME}"
+ spec:
+ volumes:
+ - name: mongo-db-data-volume
+ emptyDir: { }
+ containers:
+ - image: "${IMAGE}"
+ args: [${ARGS}]
+ imagePullPolicy: "IfNotPresent"
+ name: "${SERVICE_NAME}"
+ ports:
+ - containerPort: ${INTERNAL_PORT}
+ name: "http"
+ protocol: "TCP"
+ volumeMounts:
+ - name: mongo-db-data-volume
+ mountPath: /data/db
+ triggers:
+ - type: "ConfigChange"
diff --git a/nosql-db/mongodb/src/test/resources/test.properties b/nosql-db/mongodb/src/test/resources/test.properties
index 0b033fb40..e11c97950 100644
--- a/nosql-db/mongodb/src/test/resources/test.properties
+++ b/nosql-db/mongodb/src/test/resources/test.properties
@@ -1,3 +1,5 @@
ts.app.log.enable=true
ts.database.log.enable=true
ts.database.openshift.use-internal-service-as-url=true
+# store MongoDB data in 'emptyDir' volume to work-around write permission issue
+ts.database.openshift.template=/mongo-openshift-deployment-template.yml
diff --git a/pom.xml b/pom.xml
index 8ccae1483..e9f5d68cd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -18,8 +18,8 @@
3.1.0
quarkus-bom
io.quarkus
- 3.2.8.Final
- 3.2.8.Final
+ 3.2.9.Final
+ 3.2.9.Final
1.3.0.Beta28
2.4.0
4.5.14
@@ -256,8 +256,8 @@
docker.io/gvenzl/oracle-free:23-slim-faststart
quay.io/quarkusqeteam/db2:11.5.7.0
- docker.io/bitnami/mongodb:5.0
- docker.io/library/redis:6.0
+ docker.io/library/mongo:5.0
+ docker.io/library/redis:7.2
quay.io/ocpmetal/wiremock
docker.io/bitnami/consul:1.15.2
@@ -403,14 +403,12 @@
lifecycle-application
external-applications
scheduling/quartz
- infinispan-client
super-size/many-extensions
quarkus-cli
logging/jboss
- cache/caffeine
qute/multimodule
qute/synchronous
qute/reactive
@@ -432,17 +430,30 @@
lifecycle-application
external-applications
scheduling/quartz
- infinispan-client
super-size/many-extensions
quarkus-cli
logging/jboss
- cache/caffeine
build-time-analytics
+
+ cache-modules
+
+ true
+
+ all-modules
+
+
+
+ env-info
+ cache/caffeine
+ cache/redis
+ infinispan-client
+
+
http-modules
@@ -787,6 +798,8 @@
registry.redhat.io/rhel8/mariadb-105
registry.redhat.io/amq7/amq-streams-kafka-27-rhel7
1.7.0
+
+ docker.io/library/mongo@sha256:6f851e31a1b317c6fa681b7dad5f94c622f1c3588477f3b769579dc5462ee659
diff --git a/security/basic/src/test/java/io/quarkus/ts/openshift/security/basic/callback/AbstractSecurityCallbackTest.java b/security/basic/src/test/java/io/quarkus/ts/openshift/security/basic/callback/AbstractSecurityCallbackTest.java
new file mode 100644
index 000000000..5ed4274b3
--- /dev/null
+++ b/security/basic/src/test/java/io/quarkus/ts/openshift/security/basic/callback/AbstractSecurityCallbackTest.java
@@ -0,0 +1,102 @@
+package io.quarkus.ts.openshift.security.basic.callback;
+
+import java.security.Permission;
+import java.security.Principal;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+import jakarta.enterprise.inject.spi.CDI;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Tag;
+
+import io.quarkus.security.credential.Credential;
+import io.quarkus.security.identity.SecurityIdentity;
+import io.quarkus.test.scenarios.annotations.DisabledOnNative;
+import io.quarkus.test.security.TestIdentityAssociation;
+import io.smallrye.mutiny.Uni;
+
+@DisabledOnNative
+@Tag("https://github.com/quarkusio/quarkus/issues/36601")
+/*
+ * This set of tests are testing QuarkusSecurityTestExtension callbacks @BeforeEach and @AfterEach
+ * These should not interfere with CDI in when tests are not annotated with @TestSecurity
+ * Way to measure if these callbacks are doing something is by testing TestIdentity in
+ * CDI.current().select(TestIdentityAssociation.class)
+ * This should be set to value in BeforeEach and then set to null in AfterEach in case, @TestSecurity is present
+ * and not touched otherwise.
+ * To properly test handling in these methods, this test package uses a testClass for every testCase,
+ * to isolate calls of *Each methods
+ */
+abstract public class AbstractSecurityCallbackTest {
+ protected static final SecurityIdentity MOCK_SECURITY_IDENTITY = new SecurityIdentity() {
+ @Override
+ public Principal getPrincipal() {
+ return new Principal() {
+ @Override
+ public String getName() {
+ return "";
+ }
+ };
+ }
+
+ @Override
+ public boolean isAnonymous() {
+ return true;
+ }
+
+ @Override
+ public Set getRoles() {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public boolean hasRole(String role) {
+ return false;
+ }
+
+ @Override
+ public T getCredential(Class credentialType) {
+ return null;
+ }
+
+ @Override
+ public Set getCredentials() {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public T getAttribute(String name) {
+ return null;
+ }
+
+ @Override
+ public Map getAttributes() {
+ return Collections.emptyMap();
+ }
+
+ @Override
+ public Uni checkPermission(Permission permission) {
+ return Uni.createFrom().item(false);
+ }
+ };
+
+ protected static void assertTestIdentityIsNull() {
+ Assertions.assertNull(CDI.current().select(TestIdentityAssociation.class).get().getTestIdentity(),
+ "TestIdentity should be null");
+ }
+
+ protected static void assertTestIdentityIsNotNull() {
+ Assertions.assertNotNull(CDI.current().select(TestIdentityAssociation.class).get().getTestIdentity(),
+ "TestIdentity should have value");
+ }
+
+ protected static void setTestIdentityToValue() {
+ CDI.current().select(TestIdentityAssociation.class).get().setTestIdentity(MOCK_SECURITY_IDENTITY);
+ }
+
+ protected static void setTestIdentityToNull() {
+ CDI.current().select(TestIdentityAssociation.class).get().setTestIdentity(null);
+ }
+}
diff --git a/security/basic/src/test/java/io/quarkus/ts/openshift/security/basic/callback/TestSecurityDisabledTest.java b/security/basic/src/test/java/io/quarkus/ts/openshift/security/basic/callback/TestSecurityDisabledTest.java
new file mode 100644
index 000000000..d04dba2b3
--- /dev/null
+++ b/security/basic/src/test/java/io/quarkus/ts/openshift/security/basic/callback/TestSecurityDisabledTest.java
@@ -0,0 +1,49 @@
+package io.quarkus.ts.openshift.security.basic.callback;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.test.junit.QuarkusTest;
+
+@QuarkusTest
+public class TestSecurityDisabledTest extends AbstractSecurityCallbackTest {
+
+ @BeforeAll
+ public static void checkBeforeAll() {
+ // this is called before QuarkusSecurityTestExtension.beforeEach, so testIdentity should still be null
+ assertTestIdentityIsNull();
+ }
+
+ @BeforeEach
+ public void checkBeforeEach() {
+ // this is called after QuarkusSecurityTestExtension.beforeEach, with no @TestSecurity, this should be still null
+ assertTestIdentityIsNull();
+ }
+
+ @Test
+ public void checkTest() {
+ // QuarkusSecurityTestExtension.beforeEach should not set test identity
+ assertTestIdentityIsNull();
+
+ // set testIdentity, so we can later check, if it is set to null or not
+ setTestIdentityToValue();
+ }
+
+ @AfterEach
+ public void checkAfterEach() {
+ // testIdentity was set in test, it should be still set
+ assertTestIdentityIsNotNull();
+ }
+
+ @AfterAll
+ public static void checkAfterAll() {
+ // testIdentity was set in test, it should be still set
+ assertTestIdentityIsNotNull();
+
+ // reset testIdentity to null, so it won't break other tests
+ setTestIdentityToNull();
+ }
+}
diff --git a/security/basic/src/test/java/io/quarkus/ts/openshift/security/basic/callback/TestSecurityEnabledTest.java b/security/basic/src/test/java/io/quarkus/ts/openshift/security/basic/callback/TestSecurityEnabledTest.java
new file mode 100644
index 000000000..61e969915
--- /dev/null
+++ b/security/basic/src/test/java/io/quarkus/ts/openshift/security/basic/callback/TestSecurityEnabledTest.java
@@ -0,0 +1,37 @@
+package io.quarkus.ts.openshift.security.basic.callback;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.test.junit.QuarkusTest;
+import io.quarkus.test.security.TestSecurity;
+
+@QuarkusTest
+public class TestSecurityEnabledTest extends AbstractSecurityCallbackTest {
+ @BeforeAll
+ public static void checkBeforeAll() {
+ // this is called before QuarkusSecurityTestExtension.beforeEach, so testIdentity should still be null
+ assertTestIdentityIsNull();
+ }
+
+ @BeforeEach
+ public void checkBeforeEach() {
+ // this is called after QuarkusSecurityTestExtension.beforeEach, so testIdentity should be set
+ assertTestIdentityIsNotNull();
+ }
+
+ @Test
+ @TestSecurity(user = "myUser")
+ public void checkTestItself() {
+ // QuarkusSecurityTestExtension.beforeEach should set test identity
+ assertTestIdentityIsNotNull();
+ }
+
+ @AfterAll
+ public static void checkAfterAll() {
+ // this is called after QuarkusSecurityTestExtension.afterEach, so testIdentity should be set to null again
+ assertTestIdentityIsNull();
+ }
+}
diff --git a/security/keycloak-oidc-client-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/ping/clients/TokenPropagationPongClient.java b/security/keycloak-oidc-client-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/ping/clients/TokenPropagationPongClient.java
index 37e6576b7..9e734e6fb 100644
--- a/security/keycloak-oidc-client-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/ping/clients/TokenPropagationPongClient.java
+++ b/security/keycloak-oidc-client-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/ping/clients/TokenPropagationPongClient.java
@@ -10,13 +10,14 @@
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
+import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
-import io.quarkus.oidc.token.propagation.AccessToken;
import io.quarkus.ts.security.keycloak.oidcclient.extended.restclient.model.Score;
+import io.quarkus.ts.security.keycloak.oidcclient.extended.restclient.ping.filters.DefaultTokenRequestFilter;
@RegisterRestClient
-@AccessToken
+@RegisterProvider(DefaultTokenRequestFilter.class)
@Path("/rest-pong")
public interface TokenPropagationPongClient {
diff --git a/security/keycloak-oidc-client-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/ping/filters/CustomTokenRequestFilter.java b/security/keycloak-oidc-client-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/ping/filters/CustomTokenRequestFilter.java
new file mode 100644
index 000000000..5334997ca
--- /dev/null
+++ b/security/keycloak-oidc-client-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/ping/filters/CustomTokenRequestFilter.java
@@ -0,0 +1,15 @@
+package io.quarkus.ts.security.keycloak.oidcclient.extended.restclient.ping.filters;
+
+import io.quarkus.oidc.token.propagation.AccessTokenRequestFilter;
+
+public class CustomTokenRequestFilter extends AccessTokenRequestFilter {
+ @Override
+ protected String getClientName() {
+ return "exchange-token";
+ }
+
+ @Override
+ protected boolean isExchangeToken() {
+ return true;
+ }
+}
diff --git a/security/keycloak-oidc-client-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/ping/filters/DefaultTokenRequestFilter.java b/security/keycloak-oidc-client-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/ping/filters/DefaultTokenRequestFilter.java
new file mode 100644
index 000000000..d3ad622a8
--- /dev/null
+++ b/security/keycloak-oidc-client-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/ping/filters/DefaultTokenRequestFilter.java
@@ -0,0 +1,14 @@
+package io.quarkus.ts.security.keycloak.oidcclient.extended.restclient.ping.filters;
+
+import io.quarkus.oidc.token.propagation.AccessTokenRequestFilter;
+
+/**
+ * This class is required for
+ * {@link io.quarkus.ts.security.keycloak.oidcclient.extended.restclient.ping.clients.TokenPropagationPongClient}
+ * It would not be required normally, but having {@link CustomTokenRequestFilter} causes AmbiguousResolutionException when
+ * getting a default filter.
+ * So this class is necessary to have unambiguous filter for TokenPropagatingPongClient.
+ * TODO: remove once issue is solved https://github.com/quarkusio/quarkus/issues/36994
+ */
+public class DefaultTokenRequestFilter extends AccessTokenRequestFilter {
+}
diff --git a/security/keycloak-oidc-client-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/principal/FilteredTokenResource.java b/security/keycloak-oidc-client-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/principal/FilteredTokenResource.java
new file mode 100644
index 000000000..b867bf85c
--- /dev/null
+++ b/security/keycloak-oidc-client-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/principal/FilteredTokenResource.java
@@ -0,0 +1,22 @@
+package io.quarkus.ts.security.keycloak.oidcclient.extended.restclient.principal;
+
+import jakarta.inject.Inject;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+
+import org.eclipse.microprofile.rest.client.inject.RestClient;
+
+import io.quarkus.ts.security.keycloak.oidcclient.extended.restclient.principal.clients.TokenPropagationFilteredClient;
+
+@Path("/token-propagation-filter")
+public class FilteredTokenResource {
+
+ @Inject
+ @RestClient
+ TokenPropagationFilteredClient tokenPropagationFilterClient;
+
+ @GET
+ public String getUserName() {
+ return tokenPropagationFilterClient.getUserName();
+ }
+}
diff --git a/security/keycloak-oidc-client-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/principal/PrincipalResource.java b/security/keycloak-oidc-client-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/principal/PrincipalResource.java
new file mode 100644
index 000000000..196147e7d
--- /dev/null
+++ b/security/keycloak-oidc-client-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/principal/PrincipalResource.java
@@ -0,0 +1,22 @@
+package io.quarkus.ts.security.keycloak.oidcclient.extended.restclient.principal;
+
+import java.security.Principal;
+
+import jakarta.inject.Inject;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+
+import io.quarkus.security.Authenticated;
+
+@Path("/principal")
+@Authenticated
+public class PrincipalResource {
+
+ @Inject
+ Principal principal;
+
+ @GET
+ public String principalName() {
+ return principal.getName();
+ }
+}
diff --git a/security/keycloak-oidc-client-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/principal/clients/TokenPropagationFilteredClient.java b/security/keycloak-oidc-client-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/principal/clients/TokenPropagationFilteredClient.java
new file mode 100644
index 000000000..e95cecd55
--- /dev/null
+++ b/security/keycloak-oidc-client-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/principal/clients/TokenPropagationFilteredClient.java
@@ -0,0 +1,20 @@
+package io.quarkus.ts.security.keycloak.oidcclient.extended.restclient.principal.clients;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+
+import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
+import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
+import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
+
+import io.quarkus.ts.security.keycloak.oidcclient.extended.restclient.ping.filters.CustomTokenRequestFilter;
+
+@RegisterRestClient
+@RegisterClientHeaders
+@Path("/principal")
+@RegisterProvider(CustomTokenRequestFilter.class)
+public interface TokenPropagationFilteredClient {
+
+ @GET
+ String getUserName();
+}
diff --git a/security/keycloak-oidc-client-extended/src/main/resources/application.properties b/security/keycloak-oidc-client-extended/src/main/resources/application.properties
index cffc8f631..08a07d7d9 100644
--- a/security/keycloak-oidc-client-extended/src/main/resources/application.properties
+++ b/security/keycloak-oidc-client-extended/src/main/resources/application.properties
@@ -24,6 +24,13 @@ quarkus.oidc-client.test-user.grant.type=password
quarkus.oidc-client.test-user.grant-options.password.username=test-user
quarkus.oidc-client.test-user.grant-options.password.password=test-user
+
+## Exchange token client
+quarkus.oidc-client.exchange-token.auth-server-url=${quarkus.oidc.auth-server-url}
+quarkus.oidc-client.exchange-token.client-id=test-application-client
+quarkus.oidc-client.exchange-token.credentials.secret=test-application-client-secret
+quarkus.oidc-client.exchange-token.grant.type=exchange
+
# RestClient
io.quarkus.ts.security.keycloak.oidcclient.extended.restclient.ping.clients.PongClient/mp-rest/url=http://localhost:${quarkus.http.port}
io.quarkus.ts.security.keycloak.oidcclient.extended.restclient.ping.clients.PongClient/mp-rest/scope=jakarta.inject.Singleton
@@ -38,5 +45,7 @@ io.quarkus.ts.security.keycloak.oidcclient.extended.restclient.ping.clients.Auto
io.quarkus.ts.security.keycloak.oidcclient.extended.restclient.ping.clients.TokenPropagationPongClient/mp-rest/url=http://localhost:${quarkus.http.port}
+io.quarkus.ts.security.keycloak.oidcclient.extended.restclient.principal.clients.TokenPropagationFilteredClient/mp-rest/url=http://localhost:${quarkus.http.port}
+
#OpenAPI
quarkus.smallrye-openapi.store-schema-directory=target/generated/jakarta-rest/
diff --git a/security/keycloak-oidc-client-extended/src/test/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/TokenPropagationFilterIT.java b/security/keycloak-oidc-client-extended/src/test/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/TokenPropagationFilterIT.java
new file mode 100644
index 000000000..d3a751d7a
--- /dev/null
+++ b/security/keycloak-oidc-client-extended/src/test/java/io/quarkus/ts/security/keycloak/oidcclient/extended/restclient/TokenPropagationFilterIT.java
@@ -0,0 +1,22 @@
+package io.quarkus.ts.security.keycloak.oidcclient.extended.restclient;
+
+import static io.restassured.RestAssured.given;
+import static org.hamcrest.CoreMatchers.containsString;
+
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.test.scenarios.QuarkusScenario;
+
+@QuarkusScenario
+public class TokenPropagationFilterIT extends BaseOidcIT {
+
+ @Test
+ public void usernameTest() {
+ given()
+ .auth().oauth2(createToken())
+ .when().get("/token-propagation-filter")
+ .then().statusCode(HttpStatus.SC_OK)
+ .body(containsString(USER));
+ }
+}
diff --git a/security/keycloak-oidc-client-reactive-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/ping/clients/TokenPropagationPongClient.java b/security/keycloak-oidc-client-reactive-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/ping/clients/TokenPropagationPongClient.java
index 7856d0bd0..aac3cad1f 100644
--- a/security/keycloak-oidc-client-reactive-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/ping/clients/TokenPropagationPongClient.java
+++ b/security/keycloak-oidc-client-reactive-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/ping/clients/TokenPropagationPongClient.java
@@ -13,11 +13,11 @@
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
-import io.quarkus.oidc.token.propagation.reactive.AccessTokenRequestReactiveFilter;
import io.quarkus.ts.security.keycloak.oidcclient.reactive.extended.model.Score;
+import io.quarkus.ts.security.keycloak.oidcclient.reactive.extended.ping.filters.DefaultTokenRequestFilter;
@RegisterRestClient
-@RegisterProvider(AccessTokenRequestReactiveFilter.class)
+@RegisterProvider(DefaultTokenRequestFilter.class)
@Path("/rest-pong")
public interface TokenPropagationPongClient {
diff --git a/security/keycloak-oidc-client-reactive-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/ping/filters/CustomTokenRequestFilter.java b/security/keycloak-oidc-client-reactive-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/ping/filters/CustomTokenRequestFilter.java
new file mode 100644
index 000000000..10386027b
--- /dev/null
+++ b/security/keycloak-oidc-client-reactive-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/ping/filters/CustomTokenRequestFilter.java
@@ -0,0 +1,15 @@
+package io.quarkus.ts.security.keycloak.oidcclient.reactive.extended.ping.filters;
+
+import io.quarkus.oidc.token.propagation.reactive.AccessTokenRequestReactiveFilter;
+
+public class CustomTokenRequestFilter extends AccessTokenRequestReactiveFilter {
+ @Override
+ protected String getClientName() {
+ return "exchange-token";
+ }
+
+ @Override
+ protected boolean isExchangeToken() {
+ return true;
+ }
+}
diff --git a/security/keycloak-oidc-client-reactive-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/ping/filters/DefaultTokenRequestFilter.java b/security/keycloak-oidc-client-reactive-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/ping/filters/DefaultTokenRequestFilter.java
new file mode 100644
index 000000000..b12d38b62
--- /dev/null
+++ b/security/keycloak-oidc-client-reactive-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/ping/filters/DefaultTokenRequestFilter.java
@@ -0,0 +1,9 @@
+package io.quarkus.ts.security.keycloak.oidcclient.reactive.extended.ping.filters;
+
+import io.quarkus.oidc.token.propagation.reactive.AccessTokenRequestReactiveFilter;
+
+/**
+ * TODO: remove once issue is solved https://github.com/quarkusio/quarkus/issues/36994
+ */
+public class DefaultTokenRequestFilter extends AccessTokenRequestReactiveFilter {
+}
diff --git a/security/keycloak-oidc-client-reactive-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/principal/FilteredTokenResource.java b/security/keycloak-oidc-client-reactive-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/principal/FilteredTokenResource.java
new file mode 100644
index 000000000..ea1d9526d
--- /dev/null
+++ b/security/keycloak-oidc-client-reactive-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/principal/FilteredTokenResource.java
@@ -0,0 +1,22 @@
+package io.quarkus.ts.security.keycloak.oidcclient.reactive.extended.principal;
+
+import jakarta.inject.Inject;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+
+import org.eclipse.microprofile.rest.client.inject.RestClient;
+
+import io.quarkus.ts.security.keycloak.oidcclient.reactive.extended.principal.clients.TokenPropagationFilteredClient;
+
+@Path("/token-propagation-filter")
+public class FilteredTokenResource {
+
+ @Inject
+ @RestClient
+ TokenPropagationFilteredClient tokenPropagationFilterClient;
+
+ @GET
+ public String getUserName() {
+ return tokenPropagationFilterClient.getUserName();
+ }
+}
diff --git a/security/keycloak-oidc-client-reactive-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/principal/PrincipalResource.java b/security/keycloak-oidc-client-reactive-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/principal/PrincipalResource.java
new file mode 100644
index 000000000..0734a9676
--- /dev/null
+++ b/security/keycloak-oidc-client-reactive-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/principal/PrincipalResource.java
@@ -0,0 +1,22 @@
+package io.quarkus.ts.security.keycloak.oidcclient.reactive.extended.principal;
+
+import java.security.Principal;
+
+import jakarta.inject.Inject;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+
+import io.quarkus.security.Authenticated;
+
+@Path("/principal")
+@Authenticated
+public class PrincipalResource {
+
+ @Inject
+ Principal principal;
+
+ @GET
+ public String principalName() {
+ return principal.getName();
+ }
+}
diff --git a/security/keycloak-oidc-client-reactive-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/principal/clients/TokenPropagationFilteredClient.java b/security/keycloak-oidc-client-reactive-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/principal/clients/TokenPropagationFilteredClient.java
new file mode 100644
index 000000000..e7e1ddc4d
--- /dev/null
+++ b/security/keycloak-oidc-client-reactive-extended/src/main/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/principal/clients/TokenPropagationFilteredClient.java
@@ -0,0 +1,20 @@
+package io.quarkus.ts.security.keycloak.oidcclient.reactive.extended.principal.clients;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+
+import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders;
+import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
+import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
+
+import io.quarkus.ts.security.keycloak.oidcclient.reactive.extended.ping.filters.CustomTokenRequestFilter;
+
+@RegisterRestClient
+@RegisterClientHeaders
+@Path("/principal")
+@RegisterProvider(CustomTokenRequestFilter.class)
+public interface TokenPropagationFilteredClient {
+
+ @GET
+ String getUserName();
+}
diff --git a/security/keycloak-oidc-client-reactive-extended/src/main/resources/application.properties b/security/keycloak-oidc-client-reactive-extended/src/main/resources/application.properties
index 9066bef98..42fc152a0 100644
--- a/security/keycloak-oidc-client-reactive-extended/src/main/resources/application.properties
+++ b/security/keycloak-oidc-client-reactive-extended/src/main/resources/application.properties
@@ -24,6 +24,12 @@ quarkus.oidc-client.test-user.grant.type=password
quarkus.oidc-client.test-user.grant-options.password.username=test-user
quarkus.oidc-client.test-user.grant-options.password.password=test-user
+## Exchange token client
+quarkus.oidc-client.exchange-token.auth-server-url=${quarkus.oidc.auth-server-url}
+quarkus.oidc-client.exchange-token.client-id=test-application-client
+quarkus.oidc-client.exchange-token.credentials.secret=test-application-client-secret
+quarkus.oidc-client.exchange-token.grant.type=exchange
+
# RestClient
io.quarkus.ts.security.keycloak.oidcclient.reactive.extended.ping.clients.PongClient/mp-rest/url=http://localhost:${quarkus.http.port}
io.quarkus.ts.security.keycloak.oidcclient.reactive.extended.ping.clients.PongClient/mp-rest/scope=jakarta.inject.Singleton
@@ -38,5 +44,7 @@ io.quarkus.ts.security.keycloak.oidcclient.reactive.extended.ping.clients.AutoAc
io.quarkus.ts.security.keycloak.oidcclient.reactive.extended.ping.clients.TokenPropagationPongClient/mp-rest/url=http://localhost:${quarkus.http.port}
+io.quarkus.ts.security.keycloak.oidcclient.reactive.extended.principal.clients.TokenPropagationFilteredClient/mp-rest/url=http://localhost:${quarkus.http.port}
+
#OpenAPI
quarkus.smallrye-openapi.store-schema-directory=target/generated/jakarta-rest/
diff --git a/security/keycloak-oidc-client-reactive-extended/src/test/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/TokenPropagationFilterIT.java b/security/keycloak-oidc-client-reactive-extended/src/test/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/TokenPropagationFilterIT.java
new file mode 100644
index 000000000..6c7a2b980
--- /dev/null
+++ b/security/keycloak-oidc-client-reactive-extended/src/test/java/io/quarkus/ts/security/keycloak/oidcclient/reactive/extended/TokenPropagationFilterIT.java
@@ -0,0 +1,22 @@
+package io.quarkus.ts.security.keycloak.oidcclient.reactive.extended;
+
+import static io.restassured.RestAssured.given;
+import static org.hamcrest.CoreMatchers.containsString;
+
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.test.scenarios.QuarkusScenario;
+
+@QuarkusScenario
+public class TokenPropagationFilterIT extends BaseOidcIT {
+
+ @Test
+ public void usernameTest() {
+ given()
+ .auth().oauth2(createToken())
+ .when().get("/token-propagation-filter")
+ .then().statusCode(HttpStatus.SC_OK)
+ .body(containsString(USER));
+ }
+}