From 1fb04cb83a605717cc9fd237ea62de11875f4e68 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Wed, 18 Jun 2025 11:04:36 +0200 Subject: [PATCH] Add support for configuring SslInfo in WebTestClient Prior to this commit, there was no easy way to configure an SslInfo instance for use with WebTestClient. To address that, this commit introduces a new sslInfo(SslInfo) method in WebTestClient.MockServerSpec, which can be used as follows. var client = WebTestClient.bindToApplicationContext(context) .sslInfo(new MockSslInfo("mock ID")) // ... .build(); Closes gh-35042 --- .../server/AbstractMockServerSpec.java | 12 +++++- .../server/DefaultWebTestClientBuilder.java | 20 ++++++---- .../reactive/server/HttpHandlerConnector.java | 25 +++++++++++- .../web/reactive/server/WebTestClient.java | 9 +++++ .../samples/bind/ApplicationContextTests.java | 38 +++++++++++++++++++ 5 files changed, 94 insertions(+), 10 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/AbstractMockServerSpec.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/AbstractMockServerSpec.java index ce065fc44df..dc305f7ccc9 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/AbstractMockServerSpec.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/AbstractMockServerSpec.java @@ -22,6 +22,7 @@ import java.util.List; import org.jspecify.annotations.Nullable; +import org.springframework.http.server.reactive.SslInfo; import org.springframework.util.CollectionUtils; import org.springframework.web.server.WebFilter; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; @@ -32,6 +33,7 @@ import org.springframework.web.server.session.WebSessionManager; * Base class for implementations of {@link WebTestClient.MockServerSpec}. * * @author Rossen Stoyanchev + * @author Sam Brannen * @since 5.0 * @param a self reference to the builder type */ @@ -42,6 +44,8 @@ abstract class AbstractMockServerSpec> private @Nullable WebSessionManager sessionManager; + private @Nullable SslInfo sslInfo; + private @Nullable List configurers; @@ -66,6 +70,12 @@ abstract class AbstractMockServerSpec> return self(); } + @Override + public T sslInfo(SslInfo sslInfo) { + this.sslInfo = sslInfo; + return self(); + } + @Override public T apply(MockServerConfigurer configurer) { configurer.afterConfigureAdded(this); @@ -91,7 +101,7 @@ abstract class AbstractMockServerSpec> if (!CollectionUtils.isEmpty(this.configurers)) { this.configurers.forEach(configurer -> configurer.beforeServerCreated(builder)); } - return new DefaultWebTestClientBuilder(builder); + return new DefaultWebTestClientBuilder(builder, this.sslInfo); } /** diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java index 33fb49495bc..49141b60989 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java @@ -32,6 +32,7 @@ import org.springframework.http.client.reactive.JdkClientHttpConnector; import org.springframework.http.client.reactive.JettyClientHttpConnector; import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.http.codec.ClientCodecConfigurer; +import org.springframework.http.server.reactive.SslInfo; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -50,6 +51,7 @@ import org.springframework.web.util.UriBuilderFactory; * Default implementation of {@link WebTestClient.Builder}. * * @author Rossen Stoyanchev + * @author Sam Brannen * @since 5.0 */ class DefaultWebTestClientBuilder implements WebTestClient.Builder { @@ -78,6 +80,8 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder { private @Nullable ClientHttpConnector connector; + private @Nullable SslInfo sslInfo; + private @Nullable String baseUrl; private @Nullable UriBuilderFactory uriBuilderFactory; @@ -103,21 +107,21 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder { /** Determine connector via classpath detection. */ DefaultWebTestClientBuilder() { - this(null, null); + this(null, null, null); } /** Use HttpHandlerConnector with mock server. */ - DefaultWebTestClientBuilder(WebHttpHandlerBuilder httpHandlerBuilder) { - this(httpHandlerBuilder, null); + DefaultWebTestClientBuilder(WebHttpHandlerBuilder httpHandlerBuilder, @Nullable SslInfo sslInfo) { + this(httpHandlerBuilder, null, sslInfo); } /** Use given connector. */ DefaultWebTestClientBuilder(ClientHttpConnector connector) { - this(null, connector); + this(null, connector, null); } - DefaultWebTestClientBuilder( - @Nullable WebHttpHandlerBuilder httpHandlerBuilder, @Nullable ClientHttpConnector connector) { + private DefaultWebTestClientBuilder(@Nullable WebHttpHandlerBuilder httpHandlerBuilder, + @Nullable ClientHttpConnector connector, @Nullable SslInfo sslInfo) { Assert.isTrue(httpHandlerBuilder == null || connector == null, "Expected WebHttpHandlerBuilder or ClientHttpConnector but not both."); @@ -127,6 +131,7 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder { "To use WebTestClient, please add spring-webflux to the test classpath."); this.connector = connector; + this.sslInfo = sslInfo; this.httpHandlerBuilder = (httpHandlerBuilder != null ? httpHandlerBuilder.clone() : null); } @@ -134,6 +139,7 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder { DefaultWebTestClientBuilder(DefaultWebTestClientBuilder other) { this.httpHandlerBuilder = (other.httpHandlerBuilder != null ? other.httpHandlerBuilder.clone() : null); this.connector = other.connector; + this.sslInfo = other.sslInfo; this.responseTimeout = other.responseTimeout; this.baseUrl = other.baseUrl; @@ -284,7 +290,7 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder { ClientHttpConnector connectorToUse = this.connector; if (connectorToUse == null) { if (this.httpHandlerBuilder != null) { - connectorToUse = new HttpHandlerConnector(this.httpHandlerBuilder.build()); + connectorToUse = new HttpHandlerConnector(this.httpHandlerBuilder.build(), this.sslInfo); } } if (connectorToUse == null) { diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/HttpHandlerConnector.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/HttpHandlerConnector.java index 38e98712971..2702b73f685 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/HttpHandlerConnector.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/HttpHandlerConnector.java @@ -21,6 +21,7 @@ import java.util.function.Function; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -40,9 +41,11 @@ import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.HttpHeadResponseDecorator; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.http.server.reactive.SslInfo; import org.springframework.mock.http.client.reactive.MockClientHttpRequest; import org.springframework.mock.http.client.reactive.MockClientHttpResponse; import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest.BodyBuilder; import org.springframework.mock.http.server.reactive.MockServerHttpResponse; import org.springframework.util.Assert; import org.springframework.util.MultiValueMap; @@ -56,6 +59,7 @@ import org.springframework.util.MultiValueMap; * {@link MockServerHttpRequest} and {@link MockServerHttpResponse}. * * @author Rossen Stoyanchev + * @author Sam Brannen * @since 5.0 */ public class HttpHandlerConnector implements ClientHttpConnector { @@ -64,13 +68,26 @@ public class HttpHandlerConnector implements ClientHttpConnector { private final HttpHandler handler; + private final @Nullable SslInfo sslInfo; + /** - * Constructor with the {@link HttpHandler} to handle requests with. + * Construct an {@code HttpHandlerConnector} with the supplied {@link HttpHandler} + * to handle requests with. */ public HttpHandlerConnector(HttpHandler handler) { + this(handler, null); + } + + /** + * Construct an {@code HttpHandlerConnector} with the supplied {@link SslInfo} + * and {@link HttpHandler} to handle requests with. + * @since 7.0 + */ + public HttpHandlerConnector(HttpHandler handler, @Nullable SslInfo sslInfo) { Assert.notNull(handler, "HttpHandler is required"); this.handler = handler; + this.sslInfo = sslInfo; } @@ -136,7 +153,11 @@ public class HttpHandlerConnector implements ClientHttpConnector { URI uri = request.getURI(); HttpHeaders headers = request.getHeaders(); MultiValueMap cookies = request.getCookies(); - return MockServerHttpRequest.method(method, uri).headers(headers).cookies(cookies).body(body); + BodyBuilder builder = MockServerHttpRequest.method(method, uri).headers(headers).cookies(cookies); + if (this.sslInfo != null) { + builder.sslInfo(this.sslInfo); + } + return builder.body(body); } private ServerHttpResponse prepareResponse(ServerHttpResponse response, ServerHttpRequest request) { diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java index 355df777997..ed69598e68d 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java @@ -40,6 +40,7 @@ import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.http.client.reactive.ClientHttpRequest; import org.springframework.http.codec.ClientCodecConfigurer; import org.springframework.http.codec.ServerCodecConfigurer; +import org.springframework.http.server.reactive.SslInfo; import org.springframework.test.json.JsonComparator; import org.springframework.test.json.JsonCompareMode; import org.springframework.test.json.JsonComparison; @@ -275,6 +276,14 @@ public interface WebTestClient { */ T webSessionManager(WebSessionManager sessionManager); + /** + * Provide SSL session information and certificates for the mock server. + * @param sslInfo the {@link SslInfo} to use + * @since 7.0 + * @see org.springframework.mock.http.server.reactive.MockSslInfo + */ + T sslInfo(SslInfo sslInfo); + /** * Shortcut for pre-packaged customizations to the mock server setup. * @param configurer the configurer to apply diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/ApplicationContextTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/ApplicationContextTests.java index 6b629af00ad..493631b7fdc 100644 --- a/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/ApplicationContextTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/samples/bind/ApplicationContextTests.java @@ -17,27 +17,37 @@ package org.springframework.test.web.reactive.server.samples.bind; import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.mock.http.server.reactive.MockSslInfo; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.reactive.config.EnableWebFlux; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; /** * Sample tests demonstrating "mock" server tests binding to server infrastructure * declared in a Spring ApplicationContext. * * @author Rossen Stoyanchev + * @author Sam Brannen * @since 5.0 */ @SpringJUnitConfig class ApplicationContextTests { + private static final String SSL_SESSION_ID = "sslSessionId"; + + @Autowired ApplicationContext context; @@ -52,6 +62,19 @@ class ApplicationContextTests { .expectBody(String.class).isEqualTo("It works!"); } + @Test // gh-35042 + void buildWithSslInfo() { + var client = WebTestClient.bindToApplicationContext(context) + .sslInfo(new MockSslInfo("test123")) + .webFilter(new SslSessionIdFilter()) + .build(); + + client.get().uri("/sslInfo") + .exchange() + .expectStatus().isOk() + .expectBody(String.class).isEqualTo("Session ID: test123"); + } + @Configuration @EnableWebFlux @@ -66,6 +89,21 @@ class ApplicationContextTests { String test() { return "It works!"; } + + @GetMapping("/sslInfo") + String sslInfo(ServerHttpRequest request) { + return "Session ID: " + request.getAttributes().get(SSL_SESSION_ID); + } + } + + private static class SslSessionIdFilter implements WebFilter { + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + var request = exchange.getRequest(); + request.getAttributes().put(SSL_SESSION_ID, request.getSslInfo().getSessionId()); + return chain.filter(exchange); + } } }