Move web server auto-configure classes into spring-boot-web-server
This commit is contained in:
committed by
Phillip Webb
parent
dee54a8b99
commit
4471c0e644
@@ -0,0 +1,187 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.web.server.autoconfigure.reactive;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.ssl.DefaultSslBundleRegistry;
|
||||
import org.springframework.boot.ssl.NoSuchSslBundleException;
|
||||
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
|
||||
import org.springframework.boot.web.server.ConfigurableWebServerFactory;
|
||||
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
|
||||
import org.springframework.boot.web.server.reactive.ConfigurableReactiveWebServerFactory;
|
||||
import org.springframework.boot.web.server.reactive.MockReactiveWebServerFactory;
|
||||
import org.springframework.boot.web.server.reactive.ReactiveWebServerFactory;
|
||||
import org.springframework.boot.web.server.reactive.context.AnnotationConfigReactiveWebServerApplicationContext;
|
||||
import org.springframework.context.ApplicationContextException;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.server.reactive.HttpHandler;
|
||||
import org.springframework.web.server.adapter.ForwardedHeaderTransformer;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Base class for testing sub-classes of {@link ReactiveWebServerConfiguration}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Raheela Aslam
|
||||
* @author Madhura Bhave
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
// @DirtiesUrlFactories
|
||||
public abstract class AbstractReactiveWebServerAutoConfigurationTests {
|
||||
|
||||
private final ReactiveWebApplicationContextRunner mockServerRunner;
|
||||
|
||||
protected final ReactiveWebApplicationContextRunner serverRunner;
|
||||
|
||||
protected AbstractReactiveWebServerAutoConfigurationTests(Class<?> serverAutoConfiguration) {
|
||||
ReactiveWebApplicationContextRunner common = new ReactiveWebApplicationContextRunner(
|
||||
AnnotationConfigReactiveWebServerApplicationContext::new)
|
||||
.withConfiguration(AutoConfigurations.of(serverAutoConfiguration));
|
||||
this.serverRunner = common.withPropertyValues("server.port=0")
|
||||
.withUserConfiguration(HttpHandlerConfiguration.class);
|
||||
this.mockServerRunner = common.withUserConfiguration(MockWebServerConfiguration.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createFromConfigClass() {
|
||||
this.mockServerRunner.withUserConfiguration(MockWebServerConfiguration.class, HttpHandlerConfiguration.class)
|
||||
.run((context) -> {
|
||||
assertThat(context.getBeansOfType(ReactiveWebServerFactory.class)).hasSize(1);
|
||||
assertThat(context.getBeansOfType(WebServerFactoryCustomizer.class)).hasSizeGreaterThanOrEqualTo(1);
|
||||
assertThat(context.getBeansOfType(ReactiveWebServerFactoryCustomizer.class)).hasSize(1);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingHttpHandler() {
|
||||
this.mockServerRunner.withUserConfiguration(MockWebServerConfiguration.class)
|
||||
.run((context) -> assertThat(context.getStartupFailure()).isInstanceOf(ApplicationContextException.class)
|
||||
.rootCause()
|
||||
.hasMessageContaining("missing HttpHandler bean"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipleHttpHandler() {
|
||||
this.mockServerRunner
|
||||
.withUserConfiguration(MockWebServerConfiguration.class, HttpHandlerConfiguration.class,
|
||||
TooManyHttpHandlers.class)
|
||||
.run((context) -> assertThat(context.getStartupFailure()).isInstanceOf(ApplicationContextException.class)
|
||||
.rootCause()
|
||||
.hasMessageContaining("multiple HttpHandler beans : httpHandler,additionalHttpHandler"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void customizeReactiveWebServer() {
|
||||
this.mockServerRunner
|
||||
.withUserConfiguration(MockWebServerConfiguration.class, HttpHandlerConfiguration.class,
|
||||
ReactiveWebServerCustomization.class)
|
||||
.run((context) -> assertThat(context.getBean(MockReactiveWebServerFactory.class).getPort())
|
||||
.isEqualTo(9000));
|
||||
}
|
||||
|
||||
@Test
|
||||
void webServerFailsWithInvalidSslBundle() {
|
||||
this.serverRunner.withUserConfiguration(HttpHandlerConfiguration.class)
|
||||
.withBean(WebServerFactoryCustomizer.class,
|
||||
() -> (WebServerFactoryCustomizer<ConfigurableWebServerFactory>) (factory) -> factory
|
||||
.setSslBundles(new DefaultSslBundleRegistry()))
|
||||
.withPropertyValues("server.ssl.bundle=test-bundle")
|
||||
.run((context) -> {
|
||||
assertThat(context).hasFailed();
|
||||
assertThat(context.getStartupFailure().getCause()).isInstanceOf(NoSuchSslBundleException.class)
|
||||
.withFailMessage("test");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardedHeaderTransformerShouldBeConfigured() {
|
||||
this.mockServerRunner.withUserConfiguration(HttpHandlerConfiguration.class)
|
||||
.withPropertyValues("server.forward-headers-strategy=framework", "server.port=0")
|
||||
.run((context) -> assertThat(context).hasSingleBean(ForwardedHeaderTransformer.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardedHeaderTransformerWhenStrategyNotFilterShouldNotBeConfigured() {
|
||||
this.mockServerRunner.withUserConfiguration(HttpHandlerConfiguration.class)
|
||||
.withPropertyValues("server.forward-headers-strategy=native", "server.port=0")
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(ForwardedHeaderTransformer.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardedHeaderTransformerWhenAlreadyRegisteredShouldBackOff() {
|
||||
this.mockServerRunner
|
||||
.withUserConfiguration(ForwardedHeaderTransformerConfiguration.class, HttpHandlerConfiguration.class)
|
||||
.withPropertyValues("server.forward-headers-strategy=framework", "server.port=0")
|
||||
.run((context) -> assertThat(context).hasSingleBean(ForwardedHeaderTransformer.class));
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class HttpHandlerConfiguration {
|
||||
|
||||
@Bean
|
||||
HttpHandler httpHandler() {
|
||||
return mock(HttpHandler.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TooManyHttpHandlers {
|
||||
|
||||
@Bean
|
||||
HttpHandler additionalHttpHandler() {
|
||||
return mock(HttpHandler.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class ReactiveWebServerCustomization {
|
||||
|
||||
@Bean
|
||||
WebServerFactoryCustomizer<ConfigurableReactiveWebServerFactory> reactiveWebServerCustomizer() {
|
||||
return (factory) -> factory.setPort(9000);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class MockWebServerConfiguration {
|
||||
|
||||
@Bean
|
||||
MockReactiveWebServerFactory mockReactiveWebServerFactory() {
|
||||
return new MockReactiveWebServerFactory();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class ForwardedHeaderTransformerConfiguration {
|
||||
|
||||
@Bean
|
||||
ForwardedHeaderTransformer testForwardedHeaderTransformer() {
|
||||
return new ForwardedHeaderTransformer();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
/*
|
||||
* Copyright 2012-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.web.server.autoconfigure.servlet;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletContext;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.ServletRequest;
|
||||
import jakarta.servlet.ServletResponse;
|
||||
import jakarta.servlet.http.HttpServlet;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.websocket.server.ServerContainer;
|
||||
import jakarta.websocket.server.ServerEndpoint;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
|
||||
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
|
||||
import org.springframework.boot.web.server.WebServer;
|
||||
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
|
||||
import org.springframework.boot.web.server.context.WebServerApplicationContext;
|
||||
import org.springframework.boot.web.server.servlet.ConfigurableServletWebServerFactory;
|
||||
import org.springframework.boot.web.server.servlet.CookieSameSiteSupplier;
|
||||
import org.springframework.boot.web.server.servlet.MockServletWebServerFactory;
|
||||
import org.springframework.boot.web.server.servlet.ServletWebServerFactory;
|
||||
import org.springframework.boot.web.server.servlet.context.AnnotationConfigServletWebServerApplicationContext;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.boot.web.servlet.ServletContextInitializer;
|
||||
import org.springframework.boot.web.servlet.ServletRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.RequestEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.HttpClientErrorException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.filter.ForwardedHeaderFilter;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
* Base class for testing sub-classes of {@link ServletWebServerConfiguration}.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Phillip Webb
|
||||
* @author Stephane Nicoll
|
||||
* @author Raheela Aslam
|
||||
* @author Madhura Bhave
|
||||
*/
|
||||
public abstract class AbstractServletWebServerAutoConfigurationTests {
|
||||
|
||||
private final WebApplicationContextRunner mockServerRunner;
|
||||
|
||||
protected final WebApplicationContextRunner serverRunner;
|
||||
|
||||
protected AbstractServletWebServerAutoConfigurationTests(Class<?> serverAutoConfiguration) {
|
||||
WebApplicationContextRunner common = new WebApplicationContextRunner(
|
||||
AnnotationConfigServletWebServerApplicationContext::new)
|
||||
.withConfiguration(AutoConfigurations.of(serverAutoConfiguration));
|
||||
this.serverRunner = common.withPropertyValues("server.port=0");
|
||||
this.mockServerRunner = common.withUserConfiguration(MockWebServerConfiguration.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createFromConfigClass() {
|
||||
this.mockServerRunner
|
||||
.run((context) -> assertThat(context).hasSingleBean(ServletWebServerFactoryCustomizer.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void webServerHasNoServletContext() {
|
||||
this.mockServerRunner.withUserConfiguration(EnsureWebServerHasNoServletContext.class)
|
||||
.run((context) -> assertThat(context).hasNotFailed());
|
||||
}
|
||||
|
||||
@Test
|
||||
void webServerFactoryCustomizerBeansAreCalledToCustomizeWebServerFactory() {
|
||||
this.mockServerRunner
|
||||
.withBean(WebServerFactoryCustomizer.class,
|
||||
() -> (WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>) (factory) -> factory
|
||||
.setPort(9000))
|
||||
.run((context) -> assertThat(context.getBean(MockServletWebServerFactory.class).getPort()).isEqualTo(9000));
|
||||
}
|
||||
|
||||
@Test
|
||||
void initParametersAreConfiguredOnTheServletContext() {
|
||||
this.mockServerRunner
|
||||
.withPropertyValues("server.servlet.context-parameters.a:alpha",
|
||||
"server.servlet.context-parameters.b:bravo")
|
||||
.run((context) -> {
|
||||
ServletContext servletContext = context.getServletContext();
|
||||
assertThat(servletContext.getInitParameter("a")).isEqualTo("alpha");
|
||||
assertThat(servletContext.getInitParameter("b")).isEqualTo("bravo");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardedHeaderFilterShouldBeConfigured() {
|
||||
this.mockServerRunner.withPropertyValues("server.forward-headers-strategy=framework").run((context) -> {
|
||||
assertThat(context).hasSingleBean(FilterRegistrationBean.class);
|
||||
Filter filter = context.getBean(FilterRegistrationBean.class).getFilter();
|
||||
assertThat(filter).isInstanceOf(ForwardedHeaderFilter.class);
|
||||
assertThat(filter).extracting("relativeRedirects").isEqualTo(false);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardedHeaderFilterWhenStrategyNotFilterShouldNotBeConfigured() {
|
||||
this.mockServerRunner.withPropertyValues("server.forward-headers-strategy=native")
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(FilterRegistrationBean.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardedHeaderFilterWhenFilterAlreadyRegisteredShouldBackOff() {
|
||||
this.mockServerRunner.withUserConfiguration(ForwardedHeaderFilterConfiguration.class)
|
||||
.withPropertyValues("server.forward-headers-strategy=framework")
|
||||
.run((context) -> assertThat(context).hasSingleBean(FilterRegistrationBean.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void cookieSameSiteSuppliersAreApplied() {
|
||||
this.mockServerRunner.withUserConfiguration(CookieSameSiteSupplierConfiguration.class).run((context) -> {
|
||||
ConfigurableServletWebServerFactory webServerFactory = context
|
||||
.getBean(ConfigurableServletWebServerFactory.class);
|
||||
assertThat(webServerFactory.getSettings().getCookieSameSiteSuppliers()).hasSize(2);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void webSocketServerContainerIsAvailableFromServletContext() {
|
||||
this.serverRunner.run((context) -> {
|
||||
Object serverContainer = context.getServletContext()
|
||||
.getAttribute("jakarta.websocket.server.ServerContainer");
|
||||
assertThat(serverContainer).isInstanceOf(ServerContainer.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void webSocketUpgradeDoesNotPreventAFilterFromRejectingTheRequest() {
|
||||
this.serverRunner
|
||||
.withBean("testEndpointRegistrar", ServletContextInitializer.class, () -> TestEndpoint::register)
|
||||
.withUserConfiguration(UnauthorizedFilterConfiguration.class)
|
||||
.run((context) -> {
|
||||
TestEndpoint.register(context.getServletContext());
|
||||
WebServer webServer = ((WebServerApplicationContext) context.getSourceApplicationContext())
|
||||
.getWebServer();
|
||||
int port = webServer.getPort();
|
||||
RestTemplate rest = new RestTemplate();
|
||||
RequestEntity<Void> request = RequestEntity.get("http://localhost:" + port)
|
||||
.header("Upgrade", "websocket")
|
||||
.header("Connection", "upgrade")
|
||||
.header("Sec-WebSocket-Version", "13")
|
||||
.header("Sec-WebSocket-Key", "key")
|
||||
.build();
|
||||
assertThatExceptionOfType(HttpClientErrorException.Unauthorized.class)
|
||||
.isThrownBy(() -> rest.exchange(request, Void.class));
|
||||
});
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnExpression("true")
|
||||
static class MockWebServerConfiguration {
|
||||
|
||||
@Bean
|
||||
ServletWebServerFactory webServerFactory() {
|
||||
return new MockServletWebServerFactory();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Component
|
||||
static class EnsureWebServerHasNoServletContext implements BeanPostProcessor {
|
||||
|
||||
@Override
|
||||
public Object postProcessBeforeInitialization(Object bean, String beanName) {
|
||||
if (bean instanceof ConfigurableServletWebServerFactory) {
|
||||
MockServletWebServerFactory webServerFactory = (MockServletWebServerFactory) bean;
|
||||
assertThat(webServerFactory.getServletContext()).isNull();
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object postProcessAfterInitialization(Object bean, String beanName) {
|
||||
return bean;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class ForwardedHeaderFilterConfiguration {
|
||||
|
||||
@Bean
|
||||
FilterRegistrationBean<ForwardedHeaderFilter> testForwardedHeaderFilter() {
|
||||
ForwardedHeaderFilter filter = new ForwardedHeaderFilter();
|
||||
return new FilterRegistrationBean<>(filter);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CookieSameSiteSupplierConfiguration {
|
||||
|
||||
@Bean
|
||||
CookieSameSiteSupplier cookieSameSiteSupplier1() {
|
||||
return CookieSameSiteSupplier.ofLax().whenHasName("test1");
|
||||
}
|
||||
|
||||
@Bean
|
||||
CookieSameSiteSupplier cookieSameSiteSupplier2() {
|
||||
return CookieSameSiteSupplier.ofNone().whenHasName("test2");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ServerEndpoint("/")
|
||||
public static class TestEndpoint {
|
||||
|
||||
static void register(ServletContext context) {
|
||||
try {
|
||||
ServerContainer serverContainer = (ServerContainer) context
|
||||
.getAttribute("jakarta.websocket.server.ServerContainer");
|
||||
if (serverContainer != null) {
|
||||
serverContainer.addEndpoint(TestEndpoint.class);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
// Continue
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class UnauthorizedFilterConfiguration {
|
||||
|
||||
@Bean
|
||||
FilterRegistrationBean<Filter> unauthorizedFilter() {
|
||||
FilterRegistrationBean<Filter> registration = new FilterRegistrationBean<>(new Filter() {
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
((HttpServletResponse) response).sendError(HttpStatus.UNAUTHORIZED.value());
|
||||
}
|
||||
|
||||
});
|
||||
registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
|
||||
registration.addUrlPatterns("/*");
|
||||
registration.setDispatcherTypes(DispatcherType.REQUEST);
|
||||
return registration;
|
||||
}
|
||||
|
||||
@Bean
|
||||
ServletRegistrationBean<HttpServlet> basicServlet() {
|
||||
ServletRegistrationBean<HttpServlet> registration = new ServletRegistrationBean<>(new HttpServlet() {
|
||||
});
|
||||
registration.addUrlMappings("/");
|
||||
return registration;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user