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); + } } }