Create spring-boot-webmvc module
This commit is contained in:
committed by
Phillip Webb
parent
e83f4eed4e
commit
dfd5e56956
@@ -0,0 +1,288 @@
|
||||
/*
|
||||
* 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.webmvc.autoconfigure;
|
||||
|
||||
import jakarta.servlet.MultipartConfigElement;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
|
||||
import org.springframework.boot.web.servlet.MultipartConfigFactory;
|
||||
import org.springframework.boot.web.servlet.ServletRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.event.ContextRefreshedEvent;
|
||||
import org.springframework.util.unit.DataSize;
|
||||
import org.springframework.web.multipart.MultipartHttpServletRequest;
|
||||
import org.springframework.web.multipart.MultipartResolver;
|
||||
import org.springframework.web.servlet.DispatcherServlet;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link DispatcherServletAutoConfiguration}.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Andy Wilkinson
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
class DispatcherServletAutoConfigurationTests {
|
||||
|
||||
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(DispatcherServletAutoConfiguration.class));
|
||||
|
||||
@Test
|
||||
void registrationProperties() {
|
||||
this.contextRunner.run((context) -> {
|
||||
assertThat(context.getBean(DispatcherServlet.class)).isNotNull();
|
||||
ServletRegistrationBean<?> registration = context.getBean(ServletRegistrationBean.class);
|
||||
assertThat(registration.getUrlMappings()).containsExactly("/");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void registrationNonServletBean() {
|
||||
this.contextRunner.withUserConfiguration(NonServletConfiguration.class).run((context) -> {
|
||||
assertThat(context).doesNotHaveBean(ServletRegistrationBean.class);
|
||||
assertThat(context).doesNotHaveBean(DispatcherServlet.class);
|
||||
assertThat(context).doesNotHaveBean(DispatcherServletPath.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void registrationOverrideWithDefaultDispatcherServletNameResultsInSingleDispatcherServlet() {
|
||||
this.contextRunner.withUserConfiguration(CustomDispatcherServletSameName.class).run((context) -> {
|
||||
ServletRegistrationBean<?> registration = context.getBean(ServletRegistrationBean.class);
|
||||
assertThat(registration.getUrlMappings()).containsExactly("/");
|
||||
assertThat(registration.getServletName()).isEqualTo("dispatcherServlet");
|
||||
assertThat(context).getBeanNames(DispatcherServlet.class).hasSize(1);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void registrationOverrideWithNonDefaultDispatcherServletNameResultsInTwoDispatcherServlets() {
|
||||
this.contextRunner
|
||||
.withUserConfiguration(CustomDispatcherServletDifferentName.class, CustomDispatcherServletPath.class)
|
||||
.run((context) -> {
|
||||
ServletRegistrationBean<?> registration = context.getBean(ServletRegistrationBean.class);
|
||||
assertThat(registration.getUrlMappings()).containsExactly("/");
|
||||
assertThat(registration.getServletName()).isEqualTo("dispatcherServlet");
|
||||
assertThat(context).getBeanNames(DispatcherServlet.class).hasSize(2);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void registrationOverrideWithAutowiredServlet() {
|
||||
this.contextRunner.withUserConfiguration(CustomAutowiredRegistration.class).run((context) -> {
|
||||
ServletRegistrationBean<?> registration = context.getBean(ServletRegistrationBean.class);
|
||||
assertThat(registration.getUrlMappings()).containsExactly("/foo");
|
||||
assertThat(registration.getServletName()).isEqualTo("customDispatcher");
|
||||
assertThat(context).hasSingleBean(DispatcherServlet.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void servletPath() {
|
||||
this.contextRunner.withPropertyValues("spring.mvc.servlet.path:/spring").run((context) -> {
|
||||
assertThat(context.getBean(DispatcherServlet.class)).isNotNull();
|
||||
ServletRegistrationBean<?> registration = context.getBean(ServletRegistrationBean.class);
|
||||
assertThat(registration.getUrlMappings()).containsExactly("/spring/*");
|
||||
assertThat(registration.getMultipartConfig()).isNull();
|
||||
assertThat(context.getBean(DispatcherServletPath.class).getPath()).isEqualTo("/spring");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void dispatcherServletPathWhenCustomDispatcherServletSameNameShouldReturnConfiguredServletPath() {
|
||||
this.contextRunner.withUserConfiguration(CustomDispatcherServletSameName.class)
|
||||
.withPropertyValues("spring.mvc.servlet.path:/spring")
|
||||
.run((context) -> assertThat(context.getBean(DispatcherServletPath.class).getPath()).isEqualTo("/spring"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void dispatcherServletPathNotCreatedWhenDefaultDispatcherServletNotAvailable() {
|
||||
this.contextRunner
|
||||
.withUserConfiguration(CustomDispatcherServletDifferentName.class, NonServletConfiguration.class)
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(DispatcherServletPath.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void dispatcherServletPathNotCreatedWhenCustomRegistrationBeanPresent() {
|
||||
this.contextRunner.withUserConfiguration(CustomDispatcherServletRegistration.class)
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(DispatcherServletPath.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void multipartConfig() {
|
||||
this.contextRunner.withUserConfiguration(MultipartConfiguration.class).run((context) -> {
|
||||
ServletRegistrationBean<?> registration = context.getBean(ServletRegistrationBean.class);
|
||||
assertThat(registration.getMultipartConfig()).isNotNull();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void renamesMultipartResolver() {
|
||||
this.contextRunner.withUserConfiguration(MultipartResolverConfiguration.class).run((context) -> {
|
||||
DispatcherServlet dispatcherServlet = context.getBean(DispatcherServlet.class);
|
||||
dispatcherServlet.onApplicationEvent(new ContextRefreshedEvent(context));
|
||||
assertThat(dispatcherServlet.getMultipartResolver()).isInstanceOf(MockMultipartResolver.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void dispatcherServletDefaultConfig() {
|
||||
this.contextRunner.run((context) -> {
|
||||
DispatcherServlet dispatcherServlet = context.getBean(DispatcherServlet.class);
|
||||
assertThat(dispatcherServlet).extracting("dispatchOptionsRequest").isEqualTo(true);
|
||||
assertThat(dispatcherServlet).extracting("dispatchTraceRequest").isEqualTo(false);
|
||||
assertThat(dispatcherServlet).extracting("enableLoggingRequestDetails").isEqualTo(false);
|
||||
assertThat(dispatcherServlet).extracting("publishEvents").isEqualTo(true);
|
||||
assertThat(context.getBean("dispatcherServletRegistration")).hasFieldOrPropertyWithValue("loadOnStartup",
|
||||
-1);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void dispatcherServletCustomConfig() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("spring.mvc.dispatch-options-request:false", "spring.mvc.dispatch-trace-request:true",
|
||||
"spring.mvc.publish-request-handled-events:false", "spring.mvc.servlet.load-on-startup=5")
|
||||
.run((context) -> {
|
||||
DispatcherServlet dispatcherServlet = context.getBean(DispatcherServlet.class);
|
||||
assertThat(dispatcherServlet).extracting("dispatchOptionsRequest").isEqualTo(false);
|
||||
assertThat(dispatcherServlet).extracting("dispatchTraceRequest").isEqualTo(true);
|
||||
assertThat(dispatcherServlet).extracting("publishEvents").isEqualTo(false);
|
||||
assertThat(context.getBean("dispatcherServletRegistration"))
|
||||
.hasFieldOrPropertyWithValue("loadOnStartup", 5);
|
||||
});
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class MultipartConfiguration {
|
||||
|
||||
@Bean
|
||||
MultipartConfigElement multipartConfig() {
|
||||
MultipartConfigFactory factory = new MultipartConfigFactory();
|
||||
factory.setMaxFileSize(DataSize.ofKilobytes(128));
|
||||
factory.setMaxRequestSize(DataSize.ofKilobytes(128));
|
||||
return factory.createMultipartConfig();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CustomDispatcherServletDifferentName {
|
||||
|
||||
@Bean
|
||||
DispatcherServlet customDispatcherServlet() {
|
||||
return new DispatcherServlet();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CustomDispatcherServletPath {
|
||||
|
||||
@Bean
|
||||
DispatcherServletPath dispatcherServletPath() {
|
||||
return mock(DispatcherServletPath.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CustomAutowiredRegistration {
|
||||
|
||||
@Bean
|
||||
ServletRegistrationBean<?> dispatcherServletRegistration(DispatcherServlet dispatcherServlet) {
|
||||
ServletRegistrationBean<DispatcherServlet> registration = new ServletRegistrationBean<>(dispatcherServlet,
|
||||
"/foo");
|
||||
registration.setName("customDispatcher");
|
||||
return registration;
|
||||
}
|
||||
|
||||
@Bean
|
||||
DispatcherServletPath dispatcherServletPath() {
|
||||
return mock(DispatcherServletPath.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class NonServletConfiguration {
|
||||
|
||||
@Bean
|
||||
String dispatcherServlet() {
|
||||
return "spring";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class MultipartResolverConfiguration {
|
||||
|
||||
@Bean
|
||||
MultipartResolver getMultipartResolver() {
|
||||
return new MockMultipartResolver();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CustomDispatcherServletSameName {
|
||||
|
||||
@Bean(name = DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
|
||||
DispatcherServlet dispatcherServlet() {
|
||||
return new DispatcherServlet();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CustomDispatcherServletRegistration {
|
||||
|
||||
@Bean(name = DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
|
||||
ServletRegistrationBean<DispatcherServlet> dispatcherServletRegistration(DispatcherServlet dispatcherServlet) {
|
||||
ServletRegistrationBean<DispatcherServlet> registration = new ServletRegistrationBean<>(dispatcherServlet,
|
||||
"/foo");
|
||||
registration.setName("customDispatcher");
|
||||
return registration;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class MockMultipartResolver implements MultipartResolver {
|
||||
|
||||
@Override
|
||||
public boolean isMultipart(HttpServletRequest request) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanupMultipart(MultipartHttpServletRequest request) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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.webmvc.autoconfigure;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link DispatcherServletPath}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class DispatcherServletPathTests {
|
||||
|
||||
@Test
|
||||
void getRelativePathReturnsRelativePath() {
|
||||
assertThat(((DispatcherServletPath) () -> "spring").getRelativePath("boot")).isEqualTo("spring/boot");
|
||||
assertThat(((DispatcherServletPath) () -> "spring/").getRelativePath("boot")).isEqualTo("spring/boot");
|
||||
assertThat(((DispatcherServletPath) () -> "spring").getRelativePath("/boot")).isEqualTo("spring/boot");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPrefixWhenHasSimplePathReturnPath() {
|
||||
assertThat(((DispatcherServletPath) () -> "spring").getPrefix()).isEqualTo("spring");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPrefixWhenHasPatternRemovesPattern() {
|
||||
assertThat(((DispatcherServletPath) () -> "spring/*.do").getPrefix()).isEqualTo("spring");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPathWhenPathEndsWithSlashRemovesSlash() {
|
||||
assertThat(((DispatcherServletPath) () -> "spring/").getPrefix()).isEqualTo("spring");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getServletUrlMappingWhenPathIsEmptyReturnsSlash() {
|
||||
assertThat(((DispatcherServletPath) () -> "").getServletUrlMapping()).isEqualTo("/");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getServletUrlMappingWhenPathIsSlashReturnsSlash() {
|
||||
assertThat(((DispatcherServletPath) () -> "/").getServletUrlMapping()).isEqualTo("/");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getServletUrlMappingWhenPathContainsStarReturnsPath() {
|
||||
assertThat(((DispatcherServletPath) () -> "spring/*.do").getServletUrlMapping()).isEqualTo("spring/*.do");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getServletUrlMappingWhenHasPathNotEndingSlashReturnsSlashStarPattern() {
|
||||
assertThat(((DispatcherServletPath) () -> "spring/boot").getServletUrlMapping()).isEqualTo("spring/boot/*");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getServletUrlMappingWhenHasPathEndingWithSlashReturnsSlashStarPattern() {
|
||||
assertThat(((DispatcherServletPath) () -> "spring/boot/").getServletUrlMapping()).isEqualTo("spring/boot/*");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* 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.webmvc.autoconfigure;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.web.servlet.DispatcherServlet;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Tests for {@link DispatcherServletRegistrationBean}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class DispatcherServletRegistrationBeanTests {
|
||||
|
||||
@Test
|
||||
void createWhenPathIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new DispatcherServletRegistrationBean(new DispatcherServlet(), null))
|
||||
.withMessageContaining("'path' must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPathReturnsPath() {
|
||||
DispatcherServletRegistrationBean bean = new DispatcherServletRegistrationBean(new DispatcherServlet(),
|
||||
"/test");
|
||||
assertThat(bean.getPath()).isEqualTo("/test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getUrlMappingsReturnsSinglePathMappedPattern() {
|
||||
DispatcherServletRegistrationBean bean = new DispatcherServletRegistrationBean(new DispatcherServlet(),
|
||||
"/test");
|
||||
assertThat(bean.getUrlMappings()).containsOnly("/test/*");
|
||||
}
|
||||
|
||||
@Test
|
||||
void setUrlMappingsCannotBeCalled() {
|
||||
DispatcherServletRegistrationBean bean = new DispatcherServletRegistrationBean(new DispatcherServlet(),
|
||||
"/test");
|
||||
assertThatExceptionOfType(UnsupportedOperationException.class)
|
||||
.isThrownBy(() -> bean.setUrlMappings(Collections.emptyList()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void addUrlMappingsCannotBeCalled() {
|
||||
DispatcherServletRegistrationBean bean = new DispatcherServletRegistrationBean(new DispatcherServlet(),
|
||||
"/test");
|
||||
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> bean.addUrlMappings("/test"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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.webmvc.autoconfigure;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.testsupport.classpath.resources.WithResource;
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.mock.env.MockEnvironment;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link JspTemplateAvailabilityProvider}.
|
||||
*
|
||||
* @author Yunkun Huang
|
||||
*/
|
||||
class JspTemplateAvailabilityProviderTests {
|
||||
|
||||
private final JspTemplateAvailabilityProvider provider = new JspTemplateAvailabilityProvider();
|
||||
|
||||
private final ResourceLoader resourceLoader = new DefaultResourceLoader();
|
||||
|
||||
private final MockEnvironment environment = new MockEnvironment();
|
||||
|
||||
@Test
|
||||
void availabilityOfTemplateThatDoesNotExist() {
|
||||
assertThat(isTemplateAvailable("whatever")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithResource(name = "custom-templates/custom.jsp")
|
||||
void availabilityOfTemplateWithCustomPrefix() {
|
||||
this.environment.setProperty("spring.mvc.view.prefix", "classpath:/custom-templates/");
|
||||
assertThat(isTemplateAvailable("custom.jsp")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithResource(name = "suffixed.java-server-pages")
|
||||
void availabilityOfTemplateWithCustomSuffix() {
|
||||
this.environment.setProperty("spring.mvc.view.suffix", ".java-server-pages");
|
||||
assertThat(isTemplateAvailable("suffixed")).isTrue();
|
||||
}
|
||||
|
||||
private boolean isTemplateAvailable(String view) {
|
||||
return this.provider.isTemplateAvailable(view, this.environment, getClass().getClassLoader(),
|
||||
this.resourceLoader);
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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.webmvc.autoconfigure;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import org.assertj.core.util.Throwables;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.context.properties.bind.BindException;
|
||||
import org.springframework.boot.context.properties.bind.Bindable;
|
||||
import org.springframework.boot.context.properties.bind.Binder;
|
||||
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
|
||||
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
* Tests for {@link WebMvcProperties}.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
class WebMvcPropertiesTests {
|
||||
|
||||
private final WebMvcProperties properties = new WebMvcProperties();
|
||||
|
||||
@Test
|
||||
void servletPathWhenEndsWithSlashHasValidMappingAndPrefix() {
|
||||
bind("spring.mvc.servlet.path", "/foo/");
|
||||
assertThat(this.properties.getServlet().getServletMapping()).isEqualTo("/foo/*");
|
||||
assertThat(this.properties.getServlet().getServletPrefix()).isEqualTo("/foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
void servletPathWhenDoesNotEndWithSlashHasValidMappingAndPrefix() {
|
||||
bind("spring.mvc.servlet.path", "/foo");
|
||||
assertThat(this.properties.getServlet().getServletMapping()).isEqualTo("/foo/*");
|
||||
assertThat(this.properties.getServlet().getServletPrefix()).isEqualTo("/foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
void servletPathWhenHasWildcardThrowsException() {
|
||||
assertThatExceptionOfType(BindException.class).isThrownBy(() -> bind("spring.mvc.servlet.path", "/*"))
|
||||
.withRootCauseInstanceOf(IllegalArgumentException.class)
|
||||
.satisfies((ex) -> assertThat(Throwables.getRootCause(ex)).hasMessage("'path' must not contain wildcards"));
|
||||
}
|
||||
|
||||
private void bind(String name, String value) {
|
||||
bind(Collections.singletonMap(name, value));
|
||||
}
|
||||
|
||||
private void bind(Map<String, String> map) {
|
||||
ConfigurationPropertySource source = new MapConfigurationPropertySource(map);
|
||||
new Binder(source).bind("spring.mvc", Bindable.ofInstance(this.properties));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
/*
|
||||
* 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.webmvc.autoconfigure;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.assertj.core.api.ThrowingConsumer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider;
|
||||
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders;
|
||||
import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext;
|
||||
import org.springframework.boot.test.context.runner.ContextConsumer;
|
||||
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
|
||||
import org.springframework.boot.test.system.CapturedOutput;
|
||||
import org.springframework.boot.test.system.OutputCaptureExtension;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.assertj.MockMvcTester;
|
||||
import org.springframework.web.servlet.ViewResolver;
|
||||
import org.springframework.web.servlet.view.AbstractView;
|
||||
import org.springframework.web.servlet.view.InternalResourceView;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link WelcomePageHandlerMapping}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
@ExtendWith(OutputCaptureExtension.class)
|
||||
class WelcomePageHandlerMappingTests {
|
||||
|
||||
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
|
||||
.withUserConfiguration(HandlerMappingConfiguration.class)
|
||||
.withConfiguration(AutoConfigurations.of(PropertyPlaceholderAutoConfiguration.class));
|
||||
|
||||
@Test
|
||||
void isOrderedAtLowPriority() {
|
||||
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class).run((context) -> {
|
||||
WelcomePageHandlerMapping handler = context.getBean(WelcomePageHandlerMapping.class);
|
||||
assertThat(handler.getOrder()).isEqualTo(2);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void handlesRequestForStaticPageThatAcceptsTextHtml() {
|
||||
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class)
|
||||
.run(testWith((mvc) -> assertThat(mvc.get().uri("/").accept(MediaType.TEXT_HTML)).hasStatusOk()
|
||||
.hasForwardedUrl("index.html")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void handlesRequestForStaticPageThatAcceptsAll() {
|
||||
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class)
|
||||
.run(testWith((mvc) -> assertThat(mvc.get().uri("/").accept(MediaType.ALL)).hasStatusOk()
|
||||
.hasForwardedUrl("index.html")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void doesNotHandleRequestThatDoesNotAcceptTextHtml() {
|
||||
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class)
|
||||
.run(testWith((mvc) -> assertThat(mvc.get().uri("/").accept(MediaType.APPLICATION_JSON))
|
||||
.hasStatus(HttpStatus.NOT_FOUND)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void handlesRequestWithNoAcceptHeader() {
|
||||
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class)
|
||||
.run(testWith((mvc) -> assertThat(mvc.get().uri("/")).hasStatusOk().hasForwardedUrl("index.html")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void handlesRequestWithEmptyAcceptHeader() {
|
||||
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class)
|
||||
.run(testWith((mvc) -> assertThat(mvc.get().uri("/").header(HttpHeaders.ACCEPT, "")).hasStatusOk()
|
||||
.hasForwardedUrl("index.html")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rootHandlerIsNotRegisteredWhenStaticPathPatternIsNotSlashStarStar() {
|
||||
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class)
|
||||
.withPropertyValues("static-path-pattern=/foo/**")
|
||||
.run((context) -> assertThat(context.getBean(WelcomePageHandlerMapping.class).getRootHandler()).isNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
void producesNotFoundResponseWhenThereIsNoWelcomePage() {
|
||||
this.contextRunner.run(testWith(
|
||||
(mvc) -> assertThat(mvc.get().uri("/").accept(MediaType.TEXT_HTML)).hasStatus(HttpStatus.NOT_FOUND)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void handlesRequestForTemplateThatAcceptsTextHtml() {
|
||||
this.contextRunner.withUserConfiguration(TemplateConfiguration.class)
|
||||
.run(testWith((mvc) -> assertThat(mvc.get().uri("/").accept(MediaType.TEXT_HTML)).hasStatusOk()
|
||||
.hasBodyTextEqualTo("index template")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void handlesRequestForTemplateThatAcceptsAll() {
|
||||
this.contextRunner.withUserConfiguration(TemplateConfiguration.class)
|
||||
.run(testWith((mvc) -> assertThat(mvc.get().uri("/").accept(MediaType.ALL)).hasStatusOk()
|
||||
.hasBodyTextEqualTo("index template")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void prefersAStaticResourceToATemplate() {
|
||||
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class, TemplateConfiguration.class)
|
||||
.run(testWith((mvc) -> assertThat(mvc.get().uri("/").accept(MediaType.ALL)).hasStatusOk()
|
||||
.hasForwardedUrl("index.html")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void logsInvalidAcceptHeader(CapturedOutput output) {
|
||||
this.contextRunner.withUserConfiguration(TemplateConfiguration.class)
|
||||
.run(testWith((mvc) -> assertThat(mvc.get().uri("/").accept("*/*q=0.8")).hasStatusOk()
|
||||
.hasBodyTextEqualTo("index template")));
|
||||
assertThat(output).contains("Received invalid Accept header. Assuming all media types are accepted");
|
||||
}
|
||||
|
||||
private ContextConsumer<AssertableWebApplicationContext> testWith(ThrowingConsumer<MockMvcTester> mvc) {
|
||||
return (context) -> mvc.accept(MockMvcTester.from(context));
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class HandlerMappingConfiguration {
|
||||
|
||||
@Bean
|
||||
WelcomePageHandlerMapping handlerMapping(ApplicationContext applicationContext,
|
||||
ObjectProvider<TemplateAvailabilityProviders> templateAvailabilityProviders,
|
||||
ObjectProvider<Resource> staticIndexPage,
|
||||
@Value("${static-path-pattern:/**}") String staticPathPattern) {
|
||||
return new WelcomePageHandlerMapping(
|
||||
templateAvailabilityProviders
|
||||
.getIfAvailable(() -> new TemplateAvailabilityProviders(applicationContext)),
|
||||
applicationContext, staticIndexPage.getIfAvailable(), staticPathPattern);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class StaticResourceConfiguration {
|
||||
|
||||
@Bean
|
||||
Resource staticIndexPage() {
|
||||
return new ByteArrayResource("welcome-page-static".getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TemplateConfiguration {
|
||||
|
||||
@Bean
|
||||
TemplateAvailabilityProviders templateAvailabilityProviders() {
|
||||
return new TestTemplateAvailabilityProviders(
|
||||
(view, environment, classLoader, resourceLoader) -> view.equals("index"));
|
||||
}
|
||||
|
||||
@Bean
|
||||
ViewResolver viewResolver() {
|
||||
return (name, locale) -> {
|
||||
if (name.startsWith("forward:")) {
|
||||
return new InternalResourceView(name.substring("forward:".length()));
|
||||
}
|
||||
return new AbstractView() {
|
||||
|
||||
@Override
|
||||
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
|
||||
HttpServletResponse response) throws Exception {
|
||||
response.getWriter().print(name + " template");
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class TestTemplateAvailabilityProviders extends TemplateAvailabilityProviders {
|
||||
|
||||
TestTemplateAvailabilityProviders(TemplateAvailabilityProvider provider) {
|
||||
super(Collections.singletonList(provider));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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.webmvc.autoconfigure;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.boot.http.autoconfigure.HttpMessageConvertersAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.web.client.TestRestTemplate;
|
||||
import org.springframework.boot.test.web.server.LocalServerPort;
|
||||
import org.springframework.boot.thymeleaf.autoconfigure.ThymeleafAutoConfiguration;
|
||||
import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.RequestEntity;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Integration tests for the welcome page.
|
||||
*
|
||||
* @author Madhura Bhave
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = {
|
||||
"spring.web.resources.chain.strategy.content.enabled=true",
|
||||
"spring.thymeleaf.prefix=classpath:/org/springframework/boot/webmvc/autoconfigure/",
|
||||
"spring.web.resources.static-locations=classpath:/org/springframework/boot/webmvc/autoconfigure/static" })
|
||||
class WelcomePageIntegrationTests {
|
||||
|
||||
@LocalServerPort
|
||||
private int port;
|
||||
|
||||
private final TestRestTemplate template = new TestRestTemplate();
|
||||
|
||||
@Test
|
||||
void contentStrategyWithWelcomePage() throws Exception {
|
||||
RequestEntity<?> entity = RequestEntity.get(new URI("http://localhost:" + this.port + "/"))
|
||||
.header("Accept", MediaType.ALL.toString())
|
||||
.build();
|
||||
ResponseEntity<String> content = this.template.exchange(entity, String.class);
|
||||
assertThat(content.getBody()).contains("/custom-");
|
||||
assertThat(content.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
}
|
||||
|
||||
@Test
|
||||
void notAcceptableWelcomePage() throws Exception {
|
||||
RequestEntity<?> entity = RequestEntity.get(new URI("http://localhost:" + this.port + "/"))
|
||||
.header("Accept", "spring/boot")
|
||||
.build();
|
||||
ResponseEntity<String> content = this.template.exchange(entity, String.class);
|
||||
assertThat(content.getStatusCode()).isEqualTo(HttpStatus.NOT_ACCEPTABLE);
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@Import({ PropertyPlaceholderAutoConfiguration.class, WebMvcAutoConfiguration.class,
|
||||
HttpMessageConvertersAutoConfiguration.class, TomcatServletWebServerAutoConfiguration.class,
|
||||
DispatcherServletAutoConfiguration.class, ThymeleafAutoConfiguration.class })
|
||||
static class TestConfiguration {
|
||||
|
||||
static void main(String[] args) {
|
||||
new SpringApplicationBuilder(TestConfiguration.class).run(args);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* 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.webmvc.autoconfigure;
|
||||
|
||||
import org.assertj.core.api.ThrowingConsumer;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders;
|
||||
import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext;
|
||||
import org.springframework.boot.test.context.runner.ContextConsumer;
|
||||
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.test.web.servlet.assertj.MockMvcTester;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link WelcomePageNotAcceptableHandlerMapping}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class WelcomePageNotAcceptableHandlerMappingTests {
|
||||
|
||||
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
|
||||
.withUserConfiguration(HandlerMappingConfiguration.class)
|
||||
.withConfiguration(AutoConfigurations.of(PropertyPlaceholderAutoConfiguration.class));
|
||||
|
||||
@Test
|
||||
void isOrderedAtLowPriorityButAboveResourceHandlerRegistry() {
|
||||
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class).run((context) -> {
|
||||
WelcomePageNotAcceptableHandlerMapping handler = context
|
||||
.getBean(WelcomePageNotAcceptableHandlerMapping.class);
|
||||
ResourceHandlerRegistry registry = new ResourceHandlerRegistry(context, null);
|
||||
Integer resourceOrder = (Integer) ReflectionTestUtils.getField(registry, "order");
|
||||
assertThat(handler.getOrder()).isEqualTo(Ordered.LOWEST_PRECEDENCE - 10);
|
||||
assertThat(handler.getOrder()).isLessThan(resourceOrder);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void handlesRequestForStaticPageThatAcceptsTextHtml() {
|
||||
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class)
|
||||
.run(testWith((mvc) -> assertThat(mvc.get().uri("/").accept(MediaType.TEXT_HTML))
|
||||
.hasStatus(HttpStatus.NOT_ACCEPTABLE)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void handlesRequestForStaticPageThatDoesNotAcceptTextHtml() {
|
||||
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class)
|
||||
.run(testWith((mvc) -> assertThat(mvc.get().uri("/").accept(MediaType.APPLICATION_JSON))
|
||||
.hasStatus(HttpStatus.NOT_ACCEPTABLE)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void handlesRequestWithNoAcceptHeader() {
|
||||
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class)
|
||||
.run(testWith((mvc) -> assertThat(mvc.get().uri("/")).hasStatus(HttpStatus.NOT_ACCEPTABLE)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void handlesRequestWithEmptyAcceptHeader() {
|
||||
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class)
|
||||
.run(testWith((mvc) -> assertThat(mvc.get().uri("/").header(HttpHeaders.ACCEPT, ""))
|
||||
.hasStatus(HttpStatus.NOT_ACCEPTABLE)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rootHandlerIsNotRegisteredWhenStaticPathPatternIsNotSlashStarStar() {
|
||||
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class)
|
||||
.withPropertyValues("static-path-pattern=/foo/**")
|
||||
.run((context) -> assertThat(context.getBean(WelcomePageNotAcceptableHandlerMapping.class).getRootHandler())
|
||||
.isNull());
|
||||
}
|
||||
|
||||
@Test
|
||||
void producesNotFoundResponseWhenThereIsNoWelcomePage() {
|
||||
this.contextRunner.run(testWith(
|
||||
(mvc) -> assertThat(mvc.get().uri("/").accept(MediaType.TEXT_HTML)).hasStatus(HttpStatus.NOT_FOUND)));
|
||||
}
|
||||
|
||||
private ContextConsumer<AssertableWebApplicationContext> testWith(ThrowingConsumer<MockMvcTester> mvc) {
|
||||
return (context) -> mvc.accept(MockMvcTester.from(context));
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class HandlerMappingConfiguration {
|
||||
|
||||
@Bean
|
||||
WelcomePageNotAcceptableHandlerMapping handlerMapping(ApplicationContext applicationContext,
|
||||
ObjectProvider<TemplateAvailabilityProviders> templateAvailabilityProviders,
|
||||
ObjectProvider<Resource> staticIndexPage,
|
||||
@Value("${static-path-pattern:/**}") String staticPathPattern) {
|
||||
return new WelcomePageNotAcceptableHandlerMapping(
|
||||
templateAvailabilityProviders
|
||||
.getIfAvailable(() -> new TemplateAvailabilityProviders(applicationContext)),
|
||||
applicationContext, staticIndexPage.getIfAvailable(), staticPathPattern);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class StaticResourceConfiguration {
|
||||
|
||||
@Bean
|
||||
Resource staticIndexPage() {
|
||||
return new FileSystemResource("src/test/resources/welcome-page/index.html");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* 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.webmvc.autoconfigure.error;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Pointcut;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.boot.http.autoconfigure.HttpMessageConvertersAutoConfiguration;
|
||||
import org.springframework.boot.test.util.ApplicationContextTestUtils;
|
||||
import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration;
|
||||
import org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration;
|
||||
import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.EnableAspectJAutoProxy;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.assertj.MockMvcTester;
|
||||
import org.springframework.web.context.ConfigurableWebApplicationContext;
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link BasicErrorController} using {@link MockMvcTester} but not
|
||||
* {@link org.springframework.test.context.junit.jupiter.SpringExtension}.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Sebastien Deleuze
|
||||
*/
|
||||
class BasicErrorControllerDirectMockMvcTests {
|
||||
|
||||
private ConfigurableWebApplicationContext wac;
|
||||
|
||||
private MockMvcTester mvc;
|
||||
|
||||
@AfterEach
|
||||
void close() {
|
||||
ApplicationContextTestUtils.closeAll(this.wac);
|
||||
}
|
||||
|
||||
void setup(ConfigurableWebApplicationContext context) {
|
||||
this.wac = context;
|
||||
this.mvc = MockMvcTester.from(this.wac);
|
||||
}
|
||||
|
||||
@Test
|
||||
void errorPageAvailableWithParentContext() {
|
||||
setup((ConfigurableWebApplicationContext) new SpringApplicationBuilder(ParentConfiguration.class)
|
||||
.child(ChildConfiguration.class)
|
||||
.run("--server.port=0"));
|
||||
assertThat(this.mvc.get().uri("/error").accept(MediaType.TEXT_HTML)).hasStatus5xxServerError()
|
||||
.bodyText()
|
||||
.contains("status=999");
|
||||
}
|
||||
|
||||
@Test
|
||||
void errorPageAvailableWithMvcIncluded() {
|
||||
setup((ConfigurableWebApplicationContext) new SpringApplication(WebMvcIncludedConfiguration.class)
|
||||
.run("--server.port=0"));
|
||||
assertThat(this.mvc.get().uri("/error").accept(MediaType.TEXT_HTML)).hasStatus5xxServerError()
|
||||
.bodyText()
|
||||
.contains("status=999");
|
||||
}
|
||||
|
||||
@Test
|
||||
void errorPageNotAvailableWithWhitelabelDisabled() {
|
||||
setup((ConfigurableWebApplicationContext) new SpringApplication(WebMvcIncludedConfiguration.class)
|
||||
.run("--server.port=0", "--server.error.whitelabel.enabled=false"));
|
||||
assertThat(this.mvc.get().uri("/error").accept(MediaType.TEXT_HTML)).hasFailed()
|
||||
.failure()
|
||||
.isInstanceOf(ServletException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void errorControllerWithAop() {
|
||||
setup((ConfigurableWebApplicationContext) new SpringApplication(WithAopConfiguration.class)
|
||||
.run("--server.port=0"));
|
||||
assertThat(this.mvc.get().uri("/error").accept(MediaType.TEXT_HTML)).hasStatus5xxServerError()
|
||||
.bodyText()
|
||||
.contains("status=999");
|
||||
}
|
||||
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Import({ TomcatServletWebServerAutoConfiguration.class, DispatcherServletAutoConfiguration.class,
|
||||
WebMvcAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class,
|
||||
ErrorMvcAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class })
|
||||
protected @interface MinimalWebConfiguration {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@MinimalWebConfiguration
|
||||
static class ParentConfiguration {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@MinimalWebConfiguration
|
||||
@EnableWebMvc
|
||||
static class WebMvcIncludedConfiguration {
|
||||
|
||||
// For manual testing
|
||||
static void main(String[] args) {
|
||||
SpringApplication.run(WebMvcIncludedConfiguration.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@MinimalWebConfiguration
|
||||
static class VanillaConfiguration {
|
||||
|
||||
// For manual testing
|
||||
static void main(String[] args) {
|
||||
SpringApplication.run(VanillaConfiguration.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@MinimalWebConfiguration
|
||||
static class ChildConfiguration {
|
||||
|
||||
// For manual testing
|
||||
static void main(String[] args) {
|
||||
new SpringApplicationBuilder(ParentConfiguration.class).child(ChildConfiguration.class).run(args);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableAspectJAutoProxy(proxyTargetClass = false)
|
||||
@MinimalWebConfiguration
|
||||
@Aspect
|
||||
static class WithAopConfiguration {
|
||||
|
||||
@Pointcut("within(@org.springframework.stereotype.Controller *)")
|
||||
private void controllerPointCut() {
|
||||
}
|
||||
|
||||
@Around("controllerPointCut()")
|
||||
Object mvcAdvice(ProceedingJoinPoint pjp) throws Throwable {
|
||||
return pjp.proceed();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,545 @@
|
||||
/*
|
||||
* 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.webmvc.autoconfigure.error;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.lang.reflect.Parameter;
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.web.ServerProperties;
|
||||
import org.springframework.boot.freemarker.autoconfigure.FreeMarkerAutoConfiguration;
|
||||
import org.springframework.boot.http.autoconfigure.HttpMessageConvertersAutoConfiguration;
|
||||
import org.springframework.boot.test.web.client.TestRestTemplate;
|
||||
import org.springframework.boot.testsupport.classpath.resources.WithResource;
|
||||
import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration;
|
||||
import org.springframework.boot.web.error.ErrorAttributeOptions;
|
||||
import org.springframework.boot.web.error.ErrorAttributeOptions.Include;
|
||||
import org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration;
|
||||
import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration;
|
||||
import org.springframework.boot.webmvc.error.ErrorAttributes;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.RequestEntity;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestAttribute;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.View;
|
||||
import org.springframework.web.servlet.view.AbstractView;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link BasicErrorController} using a real HTTP server.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Dave Syer
|
||||
* @author Stephane Nicoll
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
class BasicErrorControllerIntegrationTests {
|
||||
|
||||
private ConfigurableApplicationContext context;
|
||||
|
||||
@AfterEach
|
||||
void closeContext() {
|
||||
if (this.context != null) {
|
||||
this.context.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
void testErrorForMachineClientDefault() {
|
||||
load();
|
||||
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("?trace=true"), Map.class);
|
||||
assertErrorAttributes(entity.getBody(), "500", "Internal Server Error", null, null, "/");
|
||||
assertThat(entity.getBody()).doesNotContainKey("exception");
|
||||
assertThat(entity.getBody()).doesNotContainKey("trace");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testErrorForMachineClientWithParamsTrue() {
|
||||
load("--server.error.include-exception=true", "--server.error.include-stacktrace=on-param",
|
||||
"--server.error.include-message=on-param");
|
||||
exceptionWithStackTraceAndMessage("?trace=true&message=true");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testErrorForMachineClientWithParamsFalse() {
|
||||
load("--server.error.include-exception=true", "--server.error.include-stacktrace=on-param",
|
||||
"--server.error.include-message=on-param");
|
||||
exceptionWithoutStackTraceAndMessage("?trace=false&message=false");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testErrorForMachineClientWithParamsAbsent() {
|
||||
load("--server.error.include-exception=true", "--server.error.include-stacktrace=on-param",
|
||||
"--server.error.include-message=on-param");
|
||||
exceptionWithoutStackTraceAndMessage("");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testErrorForMachineClientNeverParams() {
|
||||
load("--server.error.include-exception=true", "--server.error.include-stacktrace=never",
|
||||
"--server.error.include-message=never");
|
||||
exceptionWithoutStackTraceAndMessage("?trace=true&message=true");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testErrorForMachineClientAlwaysParams() {
|
||||
load("--server.error.include-exception=true", "--server.error.include-stacktrace=always",
|
||||
"--server.error.include-message=always");
|
||||
exceptionWithStackTraceAndMessage("?trace=false&message=false");
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("rawtypes")
|
||||
void testErrorForMachineClientAlwaysParamsWithoutMessage() {
|
||||
load("--server.error.include-exception=true", "--server.error.include-message=always");
|
||||
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("/noMessage"), Map.class);
|
||||
assertErrorAttributes(entity.getBody(), "500", "Internal Server Error", IllegalStateException.class,
|
||||
"No message available", "/noMessage");
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
private void exceptionWithStackTraceAndMessage(String path) {
|
||||
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl(path), Map.class);
|
||||
assertErrorAttributes(entity.getBody(), "500", "Internal Server Error", IllegalStateException.class,
|
||||
"Expected!", "/");
|
||||
assertThat(entity.getBody()).containsKey("trace");
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
private void exceptionWithoutStackTraceAndMessage(String path) {
|
||||
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl(path), Map.class);
|
||||
assertErrorAttributes(entity.getBody(), "500", "Internal Server Error", IllegalStateException.class, null, "/");
|
||||
assertThat(entity.getBody()).doesNotContainKey("trace");
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("rawtypes")
|
||||
void testErrorForAnnotatedExceptionWithoutMessage() {
|
||||
load("--server.error.include-exception=true");
|
||||
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("/annotated"), Map.class);
|
||||
assertErrorAttributes(entity.getBody(), "400", "Bad Request", TestConfiguration.Errors.ExpectedException.class,
|
||||
null, "/annotated");
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("rawtypes")
|
||||
void testErrorForAnnotatedExceptionWithMessage() {
|
||||
load("--server.error.include-exception=true", "--server.error.include-message=always");
|
||||
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("/annotated"), Map.class);
|
||||
assertErrorAttributes(entity.getBody(), "400", "Bad Request", TestConfiguration.Errors.ExpectedException.class,
|
||||
"Expected!", "/annotated");
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("rawtypes")
|
||||
void testErrorForAnnotatedNoReasonExceptionWithoutMessage() {
|
||||
load("--server.error.include-exception=true");
|
||||
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("/annotatedNoReason"), Map.class);
|
||||
assertErrorAttributes(entity.getBody(), "406", "Not Acceptable",
|
||||
TestConfiguration.Errors.NoReasonExpectedException.class, null, "/annotatedNoReason");
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("rawtypes")
|
||||
void testErrorForAnnotatedNoReasonExceptionWithMessage() {
|
||||
load("--server.error.include-exception=true", "--server.error.include-message=always");
|
||||
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("/annotatedNoReason"), Map.class);
|
||||
assertErrorAttributes(entity.getBody(), "406", "Not Acceptable",
|
||||
TestConfiguration.Errors.NoReasonExpectedException.class, "Expected message", "/annotatedNoReason");
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("rawtypes")
|
||||
void testErrorForAnnotatedNoMessageExceptionWithMessage() {
|
||||
load("--server.error.include-exception=true", "--server.error.include-message=always");
|
||||
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("/annotatedNoMessage"), Map.class);
|
||||
assertErrorAttributes(entity.getBody(), "406", "Not Acceptable",
|
||||
TestConfiguration.Errors.NoReasonExpectedException.class, "No message available",
|
||||
"/annotatedNoMessage");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBindingExceptionForMachineClientWithErrorsParamTrue() {
|
||||
load("--server.error.include-exception=true", "--server.error.include-binding-errors=on-param");
|
||||
bindingExceptionWithErrors("?errors=true");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBindingExceptionForMachineClientWithErrorsParamFalse() {
|
||||
load("--server.error.include-exception=true", "--server.error.include-binding-errors=on-param");
|
||||
bindingExceptionWithoutErrors("?errors=false");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBindingExceptionForMachineClientWithErrorsParamAbsent() {
|
||||
load("--server.error.include-exception=true", "--server.error.include-binding-errors=on-param");
|
||||
bindingExceptionWithoutErrors("");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBindingExceptionForMachineClientAlwaysErrors() {
|
||||
load("--server.error.include-exception=true", "--server.error.include-binding-errors=always");
|
||||
bindingExceptionWithErrors("?errors=false");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBindingExceptionForMachineClientNeverErrors() {
|
||||
load("--server.error.include-exception=true", "--server.error.include-binding-errors=never");
|
||||
bindingExceptionWithoutErrors("?errors=true");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBindingExceptionForMachineClientWithMessageParamTrue() {
|
||||
load("--server.error.include-exception=true", "--server.error.include-message=on-param");
|
||||
bindingExceptionWithMessage("?message=true");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBindingExceptionForMachineClientWithMessageParamFalse() {
|
||||
load("--server.error.include-exception=true", "--server.error.include-message=on-param");
|
||||
bindingExceptionWithoutMessage("?message=false");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBindingExceptionForMachineClientWithMessageParamAbsent() {
|
||||
load("--server.error.include-exception=true", "--server.error.include-message=on-param");
|
||||
bindingExceptionWithoutMessage("");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBindingExceptionForMachineClientAlwaysMessage() {
|
||||
load("--server.error.include-exception=true", "--server.error.include-message=always");
|
||||
bindingExceptionWithMessage("?message=false");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBindingExceptionForMachineClientNeverMessage() {
|
||||
load("--server.error.include-exception=true", "--server.error.include-message=never");
|
||||
bindingExceptionWithoutMessage("?message=true");
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
private void bindingExceptionWithErrors(String param) {
|
||||
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("/bind" + param), Map.class);
|
||||
assertErrorAttributes(entity.getBody(), "400", "Bad Request", MethodArgumentNotValidException.class, null,
|
||||
"/bind");
|
||||
assertThat(entity.getBody()).containsKey("errors");
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
private void bindingExceptionWithoutErrors(String param) {
|
||||
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("/bind" + param), Map.class);
|
||||
assertErrorAttributes(entity.getBody(), "400", "Bad Request", MethodArgumentNotValidException.class, null,
|
||||
"/bind");
|
||||
assertThat(entity.getBody()).doesNotContainKey("errors");
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
private void bindingExceptionWithMessage(String param) {
|
||||
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("/bind" + param), Map.class);
|
||||
assertErrorAttributes(entity.getBody(), "400", "Bad Request", MethodArgumentNotValidException.class,
|
||||
"Validation failed for object='test'. Error count: 1", "/bind");
|
||||
assertThat(entity.getBody()).doesNotContainKey("errors");
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
private void bindingExceptionWithoutMessage(String param) {
|
||||
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("/bind" + param), Map.class);
|
||||
assertErrorAttributes(entity.getBody(), "400", "Bad Request", MethodArgumentNotValidException.class, null,
|
||||
"/bind");
|
||||
assertThat(entity.getBody()).doesNotContainKey("errors");
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
void testRequestBodyValidationForMachineClient() {
|
||||
load("--server.error.include-exception=true");
|
||||
RequestEntity request = RequestEntity.post(URI.create(createUrl("/bodyValidation")))
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body("{}");
|
||||
ResponseEntity<Map> entity = new TestRestTemplate().exchange(request, Map.class);
|
||||
assertErrorAttributes(entity.getBody(), "400", "Bad Request", MethodArgumentNotValidException.class, null,
|
||||
"/bodyValidation");
|
||||
assertThat(entity.getBody()).doesNotContainKey("errors");
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
void testBindingExceptionForMachineClientDefault() {
|
||||
load();
|
||||
RequestEntity request = RequestEntity.get(URI.create(createUrl("/bind?trace=true,message=true")))
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.build();
|
||||
ResponseEntity<Map> entity = new TestRestTemplate().exchange(request, Map.class);
|
||||
assertThat(entity.getBody()).doesNotContainKey("exception");
|
||||
assertThat(entity.getBody()).doesNotContainKey("trace");
|
||||
assertThat(entity.getBody()).doesNotContainKey("errors");
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithResource(name = "templates/error/507.ftlh", content = "We are out of storage")
|
||||
void testConventionTemplateMapping() {
|
||||
load();
|
||||
RequestEntity<?> request = RequestEntity.get(URI.create(createUrl("/noStorage")))
|
||||
.accept(MediaType.TEXT_HTML)
|
||||
.build();
|
||||
ResponseEntity<String> entity = new TestRestTemplate().exchange(request, String.class);
|
||||
String resp = entity.getBody();
|
||||
assertThat(resp).contains("We are out of storage");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIncompatibleMediaType() {
|
||||
load();
|
||||
RequestEntity<?> request = RequestEntity.get(URI.create(createUrl("/incompatibleType")))
|
||||
.accept(MediaType.TEXT_PLAIN)
|
||||
.build();
|
||||
ResponseEntity<String> entity = new TestRestTemplate().exchange(request, String.class);
|
||||
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
|
||||
assertThat(entity.getHeaders().getContentType()).isNull();
|
||||
assertThat(entity.getBody()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
void customErrorControllerWithoutStatusConfiguration() {
|
||||
load(CustomErrorControllerWithoutStatusConfiguration.class);
|
||||
RequestEntity request = RequestEntity.post(URI.create(createUrl("/bodyValidation")))
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body("{}");
|
||||
ResponseEntity<Map> entity = new TestRestTemplate().exchange(request, Map.class);
|
||||
assertThat(entity.getBody()).doesNotContainKey("status");
|
||||
}
|
||||
|
||||
private void assertErrorAttributes(Map<?, ?> content, String status, String error, Class<?> exception,
|
||||
String message, String path) {
|
||||
assertThat(content.get("status")).as("Wrong status").hasToString(status);
|
||||
assertThat(content.get("error")).as("Wrong error").isEqualTo(error);
|
||||
if (exception != null) {
|
||||
assertThat(content.get("exception")).as("Wrong exception").isEqualTo(exception.getName());
|
||||
}
|
||||
else {
|
||||
assertThat(content.containsKey("exception")).as("Exception attribute should not be set").isFalse();
|
||||
}
|
||||
assertThat(content.get("message")).as("Wrong message").isEqualTo(message);
|
||||
assertThat(content.get("path")).as("Wrong path").isEqualTo(path);
|
||||
}
|
||||
|
||||
private String createUrl(String path) {
|
||||
int port = this.context.getEnvironment().getProperty("local.server.port", int.class);
|
||||
return "http://localhost:" + port + path;
|
||||
}
|
||||
|
||||
private void load(String... arguments) {
|
||||
load(TestConfiguration.class, arguments);
|
||||
}
|
||||
|
||||
private void load(Class<?> configuration, String... arguments) {
|
||||
List<String> args = new ArrayList<>();
|
||||
args.add("--server.port=0");
|
||||
if (arguments != null) {
|
||||
args.addAll(Arrays.asList(arguments));
|
||||
}
|
||||
this.context = SpringApplication.run(configuration, StringUtils.toStringArray(args));
|
||||
}
|
||||
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@ImportAutoConfiguration({ TomcatServletWebServerAutoConfiguration.class, DispatcherServletAutoConfiguration.class,
|
||||
WebMvcAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class,
|
||||
ErrorMvcAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class })
|
||||
private @interface MinimalWebConfiguration {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@MinimalWebConfiguration
|
||||
@ImportAutoConfiguration(FreeMarkerAutoConfiguration.class)
|
||||
public static class TestConfiguration {
|
||||
|
||||
// For manual testing
|
||||
static void main(String[] args) {
|
||||
SpringApplication.run(TestConfiguration.class, args);
|
||||
}
|
||||
|
||||
@Bean
|
||||
View error() {
|
||||
return new AbstractView() {
|
||||
|
||||
@Override
|
||||
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
|
||||
HttpServletResponse response) throws Exception {
|
||||
response.getWriter().write("ERROR_BEAN");
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
@RestController
|
||||
public static class Errors {
|
||||
|
||||
public String getFoo() {
|
||||
return "foo";
|
||||
}
|
||||
|
||||
@RequestMapping("/")
|
||||
String home() {
|
||||
throw new IllegalStateException("Expected!");
|
||||
}
|
||||
|
||||
@RequestMapping("/noMessage")
|
||||
String noMessage() {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
@RequestMapping("/annotated")
|
||||
String annotated() {
|
||||
throw new ExpectedException();
|
||||
}
|
||||
|
||||
@RequestMapping("/annotatedNoReason")
|
||||
String annotatedNoReason() {
|
||||
throw new NoReasonExpectedException("Expected message");
|
||||
}
|
||||
|
||||
@RequestMapping("/annotatedNoMessage")
|
||||
String annotatedNoMessage() {
|
||||
throw new NoReasonExpectedException("");
|
||||
}
|
||||
|
||||
@RequestMapping("/bind")
|
||||
String bind(@RequestAttribute(required = false) String foo) throws Exception {
|
||||
BindException error = new BindException(this, "test");
|
||||
error.rejectValue("foo", "bar.error");
|
||||
Parameter fooParameter = ReflectionUtils.findMethod(Errors.class, "bind", String.class)
|
||||
.getParameters()[0];
|
||||
throw new MethodArgumentNotValidException(MethodParameter.forParameter(fooParameter), error);
|
||||
}
|
||||
|
||||
@PostMapping(path = "/bodyValidation", produces = "application/json")
|
||||
String bodyValidation(@Valid @RequestBody DummyBody body) {
|
||||
return body.content;
|
||||
}
|
||||
|
||||
@RequestMapping(path = "/noStorage")
|
||||
String noStorage() {
|
||||
throw new InsufficientStorageException();
|
||||
}
|
||||
|
||||
@RequestMapping(path = "/incompatibleType", produces = "text/plain")
|
||||
String incompatibleType() {
|
||||
throw new ExpectedException();
|
||||
}
|
||||
|
||||
@ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "Expected!")
|
||||
@SuppressWarnings("serial")
|
||||
static class ExpectedException extends RuntimeException {
|
||||
|
||||
}
|
||||
|
||||
@ResponseStatus(HttpStatus.INSUFFICIENT_STORAGE)
|
||||
static class InsufficientStorageException extends RuntimeException {
|
||||
|
||||
}
|
||||
|
||||
@ResponseStatus(HttpStatus.NOT_ACCEPTABLE)
|
||||
@SuppressWarnings("serial")
|
||||
static class NoReasonExpectedException extends RuntimeException {
|
||||
|
||||
NoReasonExpectedException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class DummyBody {
|
||||
|
||||
@NotNull
|
||||
private String content;
|
||||
|
||||
String getContent() {
|
||||
return this.content;
|
||||
}
|
||||
|
||||
void setContent(String content) {
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class CustomErrorControllerWithoutStatusConfiguration extends TestConfiguration {
|
||||
|
||||
@Bean
|
||||
BasicErrorController basicErrorController(ServerProperties serverProperties, ErrorAttributes errorAttributes,
|
||||
ObjectProvider<ErrorViewResolver> errorViewResolvers) {
|
||||
return new BasicErrorController(errorAttributes, serverProperties.getError(),
|
||||
errorViewResolvers.orderedStream().toList()) {
|
||||
|
||||
@Override
|
||||
protected ErrorAttributeOptions getErrorAttributeOptions(HttpServletRequest request,
|
||||
MediaType mediaType) {
|
||||
return super.getErrorAttributeOptions(request, mediaType).excluding(Include.STATUS);
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
* 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.webmvc.autoconfigure.error;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.lang.reflect.Parameter;
|
||||
import java.util.Map;
|
||||
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import jakarta.servlet.ServletContext;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
|
||||
import org.springframework.boot.http.autoconfigure.HttpMessageConvertersAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration;
|
||||
import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.test.web.servlet.RequestBuilder;
|
||||
import org.springframework.test.web.servlet.assertj.MockMvcTester;
|
||||
import org.springframework.test.web.servlet.assertj.MvcTestResult;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.RequestAttribute;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
import org.springframework.web.servlet.View;
|
||||
import org.springframework.web.servlet.view.AbstractView;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link BasicErrorController} using {@link MockMvcTester} and
|
||||
* {@link SpringBootTest @SpringBootTest}.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
@SpringBootTest(properties = { "server.error.include-message=always", "debug=true" })
|
||||
@DirtiesContext
|
||||
class BasicErrorControllerMockMvcTests {
|
||||
|
||||
@Autowired
|
||||
private WebApplicationContext wac;
|
||||
|
||||
private MockMvcTester mvc;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
this.mvc = MockMvcTester.from(this.wac);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDirectAccessForMachineClient() {
|
||||
assertThat(this.mvc.get().uri("/error")).hasStatus5xxServerError().bodyText().contains("999");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testErrorWithNotFoundResponseStatus() {
|
||||
assertThat(this.mvc.get().uri("/bang")).hasStatus(HttpStatus.NOT_FOUND)
|
||||
.satisfies((result) -> assertThat(this.mvc.perform(new ErrorDispatcher(result, "/error"))).bodyText()
|
||||
.contains("Expected!"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testErrorWithNoContentResponseStatus() {
|
||||
assertThat(this.mvc.get().uri("/noContent").accept("some/thing")).hasStatus(HttpStatus.NO_CONTENT)
|
||||
.satisfies((result) -> assertThat(this.mvc.perform(new ErrorDispatcher(result, "/error")))
|
||||
.hasStatus(HttpStatus.NO_CONTENT)
|
||||
.body()
|
||||
.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBindingExceptionForMachineClient() {
|
||||
// In a real server the response is carried over into the error dispatcher, but
|
||||
// in the mock a new one is created, so we have to assert the status at this
|
||||
// intermediate point, and the rendered status code is always wrong (but would
|
||||
// be 400 in a real system)
|
||||
assertThat(this.mvc.get().uri("/bind")).hasStatus4xxClientError()
|
||||
.satisfies((result) -> assertThat(this.mvc.perform(new ErrorDispatcher(result, "/error"))).bodyText()
|
||||
.contains("Validation failed"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDirectAccessForBrowserClient() {
|
||||
assertThat(this.mvc.get().uri("/error").accept(MediaType.TEXT_HTML)).hasStatus5xxServerError()
|
||||
.bodyText()
|
||||
.contains("ERROR_BEAN");
|
||||
}
|
||||
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@ImportAutoConfiguration({ DispatcherServletAutoConfiguration.class, WebMvcAutoConfiguration.class,
|
||||
HttpMessageConvertersAutoConfiguration.class, ErrorMvcAutoConfiguration.class,
|
||||
PropertyPlaceholderAutoConfiguration.class })
|
||||
private @interface MinimalWebConfiguration {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@MinimalWebConfiguration
|
||||
static class TestConfiguration {
|
||||
|
||||
// For manual testing
|
||||
static void main(String[] args) {
|
||||
SpringApplication.run(TestConfiguration.class, args);
|
||||
}
|
||||
|
||||
@Bean
|
||||
View error() {
|
||||
return new AbstractView() {
|
||||
@Override
|
||||
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
|
||||
HttpServletResponse response) throws Exception {
|
||||
response.getWriter().write("ERROR_BEAN");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@RestController
|
||||
public static class Errors {
|
||||
|
||||
@RequestMapping("/")
|
||||
String home() {
|
||||
throw new IllegalStateException("Expected!");
|
||||
}
|
||||
|
||||
@RequestMapping("/bang")
|
||||
String bang() {
|
||||
throw new NotFoundException("Expected!");
|
||||
}
|
||||
|
||||
@RequestMapping("/bind")
|
||||
String bind(@RequestAttribute(required = false) String foo) throws Exception {
|
||||
BindException error = new BindException(this, "test");
|
||||
error.rejectValue("foo", "bar.error");
|
||||
Parameter fooParameter = ReflectionUtils.findMethod(Errors.class, "bind", String.class)
|
||||
.getParameters()[0];
|
||||
throw new MethodArgumentNotValidException(MethodParameter.forParameter(fooParameter), error);
|
||||
}
|
||||
|
||||
@RequestMapping("/noContent")
|
||||
void noContent() {
|
||||
throw new NoContentException("Expected!");
|
||||
}
|
||||
|
||||
public String getFoo() {
|
||||
return "foo";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||
static class NotFoundException extends RuntimeException {
|
||||
|
||||
NotFoundException(String string) {
|
||||
super(string);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
private static class NoContentException extends RuntimeException {
|
||||
|
||||
NoContentException(String string) {
|
||||
super(string);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class ErrorDispatcher implements RequestBuilder {
|
||||
|
||||
private final MvcResult result;
|
||||
|
||||
private final String path;
|
||||
|
||||
ErrorDispatcher(MvcTestResult mvcTestResult, String path) {
|
||||
this.result = mvcTestResult.getMvcResult();
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MockHttpServletRequest buildRequest(ServletContext servletContext) {
|
||||
MockHttpServletRequest request = this.result.getRequest();
|
||||
request.setDispatcherType(DispatcherType.ERROR);
|
||||
request.setRequestURI(this.path);
|
||||
request.setAttribute("jakarta.servlet.error.status_code", this.result.getResponse().getStatus());
|
||||
return request;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 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.webmvc.autoconfigure.error;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
|
||||
import org.springframework.boot.http.autoconfigure.HttpMessageConvertersAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration;
|
||||
import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.web.servlet.assertj.MockMvcTester;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Integration tests for the default error view.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
@SpringBootTest(properties = { "server.error.include-message=always" })
|
||||
@DirtiesContext
|
||||
class DefaultErrorViewIntegrationTests {
|
||||
|
||||
@Autowired
|
||||
private WebApplicationContext wac;
|
||||
|
||||
private MockMvcTester mvc;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
this.mvc = MockMvcTester.from(this.wac);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testErrorForBrowserClient() {
|
||||
assertThat(this.mvc.get().uri("/error").accept(MediaType.TEXT_HTML)).hasStatus5xxServerError()
|
||||
.bodyText()
|
||||
.contains("<html>", "999");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testErrorWithHtmlEscape() {
|
||||
assertThat(this.mvc.get()
|
||||
.uri("/error")
|
||||
.requestAttr("jakarta.servlet.error.exception",
|
||||
new RuntimeException("<script>alert('Hello World')</script>"))
|
||||
.accept(MediaType.TEXT_HTML)).hasStatus5xxServerError()
|
||||
.bodyText()
|
||||
.contains("<script>", "Hello World", "999");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testErrorWithSpelEscape() {
|
||||
String spel = "${T(" + getClass().getName() + ").injectCall()}";
|
||||
assertThat(this.mvc.get()
|
||||
.uri("/error")
|
||||
.requestAttr("jakarta.servlet.error.exception", new RuntimeException(spel))
|
||||
.accept(MediaType.TEXT_HTML)).hasStatus5xxServerError().bodyText().doesNotContain("injection");
|
||||
}
|
||||
|
||||
static String injectCall() {
|
||||
return "injection";
|
||||
}
|
||||
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Import({ DispatcherServletAutoConfiguration.class, WebMvcAutoConfiguration.class,
|
||||
HttpMessageConvertersAutoConfiguration.class, ErrorMvcAutoConfiguration.class,
|
||||
PropertyPlaceholderAutoConfiguration.class })
|
||||
protected @interface MinimalWebConfiguration {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@MinimalWebConfiguration
|
||||
static class TestConfiguration {
|
||||
|
||||
// For manual testing
|
||||
static void main(String[] args) {
|
||||
SpringApplication.run(TestConfiguration.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
/*
|
||||
* 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.webmvc.autoconfigure.error;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider;
|
||||
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders;
|
||||
import org.springframework.boot.autoconfigure.web.WebProperties.Resources;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link DefaultErrorViewResolver}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DefaultErrorViewResolverTests {
|
||||
|
||||
private DefaultErrorViewResolver resolver;
|
||||
|
||||
@Mock
|
||||
private TemplateAvailabilityProvider templateAvailabilityProvider;
|
||||
|
||||
private Resources resourcesProperties;
|
||||
|
||||
private final Map<String, Object> model = new HashMap<>();
|
||||
|
||||
private final HttpServletRequest request = new MockHttpServletRequest();
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
|
||||
applicationContext.refresh();
|
||||
this.resourcesProperties = new Resources();
|
||||
TemplateAvailabilityProviders templateAvailabilityProviders = new TestTemplateAvailabilityProviders(
|
||||
this.templateAvailabilityProvider);
|
||||
this.resolver = new DefaultErrorViewResolver(applicationContext, this.resourcesProperties,
|
||||
templateAvailabilityProviders);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWhenApplicationContextIsNullShouldThrowException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new DefaultErrorViewResolver(null, new Resources()))
|
||||
.withMessageContaining("'applicationContext' must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWhenResourcePropertiesIsNullShouldThrowException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> new DefaultErrorViewResolver(mock(ApplicationContext.class), (Resources) null))
|
||||
.withMessageContaining("'resources' must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveWhenNoMatchShouldReturnNull() {
|
||||
ModelAndView resolved = this.resolver.resolveErrorView(this.request, HttpStatus.NOT_FOUND, this.model);
|
||||
assertThat(resolved).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveWhenExactTemplateMatchShouldReturnTemplate() {
|
||||
given(this.templateAvailabilityProvider.isTemplateAvailable(eq("error/404"), any(Environment.class),
|
||||
any(ClassLoader.class), any(ResourceLoader.class)))
|
||||
.willReturn(true);
|
||||
ModelAndView resolved = this.resolver.resolveErrorView(this.request, HttpStatus.NOT_FOUND, this.model);
|
||||
assertThat(resolved).isNotNull();
|
||||
assertThat(resolved.getViewName()).isEqualTo("error/404");
|
||||
then(this.templateAvailabilityProvider).should()
|
||||
.isTemplateAvailable(eq("error/404"), any(Environment.class), any(ClassLoader.class),
|
||||
any(ResourceLoader.class));
|
||||
then(this.templateAvailabilityProvider).shouldHaveNoMoreInteractions();
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveWhenSeries5xxTemplateMatchShouldReturnTemplate() {
|
||||
given(this.templateAvailabilityProvider.isTemplateAvailable(eq("error/503"), any(Environment.class),
|
||||
any(ClassLoader.class), any(ResourceLoader.class)))
|
||||
.willReturn(false);
|
||||
given(this.templateAvailabilityProvider.isTemplateAvailable(eq("error/5xx"), any(Environment.class),
|
||||
any(ClassLoader.class), any(ResourceLoader.class)))
|
||||
.willReturn(true);
|
||||
ModelAndView resolved = this.resolver.resolveErrorView(this.request, HttpStatus.SERVICE_UNAVAILABLE,
|
||||
this.model);
|
||||
assertThat(resolved.getViewName()).isEqualTo("error/5xx");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveWhenSeries4xxTemplateMatchShouldReturnTemplate() {
|
||||
given(this.templateAvailabilityProvider.isTemplateAvailable(eq("error/404"), any(Environment.class),
|
||||
any(ClassLoader.class), any(ResourceLoader.class)))
|
||||
.willReturn(false);
|
||||
given(this.templateAvailabilityProvider.isTemplateAvailable(eq("error/4xx"), any(Environment.class),
|
||||
any(ClassLoader.class), any(ResourceLoader.class)))
|
||||
.willReturn(true);
|
||||
ModelAndView resolved = this.resolver.resolveErrorView(this.request, HttpStatus.NOT_FOUND, this.model);
|
||||
assertThat(resolved.getViewName()).isEqualTo("error/4xx");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveWhenExactResourceMatchShouldReturnResource() throws Exception {
|
||||
setResourceLocation("/exact");
|
||||
ModelAndView resolved = this.resolver.resolveErrorView(this.request, HttpStatus.NOT_FOUND, this.model);
|
||||
MockHttpServletResponse response = render(resolved);
|
||||
assertThat(response.getContentAsString().trim()).isEqualTo("exact/404");
|
||||
assertThat(response.getContentType()).isEqualTo(MediaType.TEXT_HTML_VALUE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveWhenSeries4xxResourceMatchShouldReturnResource() throws Exception {
|
||||
setResourceLocation("/4xx");
|
||||
ModelAndView resolved = this.resolver.resolveErrorView(this.request, HttpStatus.NOT_FOUND, this.model);
|
||||
MockHttpServletResponse response = render(resolved);
|
||||
assertThat(response.getContentAsString().trim()).isEqualTo("4xx/4xx");
|
||||
assertThat(response.getContentType()).isEqualTo(MediaType.TEXT_HTML_VALUE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveWhenSeries5xxResourceMatchShouldReturnResource() throws Exception {
|
||||
setResourceLocation("/5xx");
|
||||
ModelAndView resolved = this.resolver.resolveErrorView(this.request, HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
this.model);
|
||||
MockHttpServletResponse response = render(resolved);
|
||||
assertThat(response.getContentAsString().trim()).isEqualTo("5xx/5xx");
|
||||
assertThat(response.getContentType()).isEqualTo(MediaType.TEXT_HTML_VALUE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveWhenTemplateAndResourceMatchShouldFavorTemplate() {
|
||||
setResourceLocation("/exact");
|
||||
given(this.templateAvailabilityProvider.isTemplateAvailable(eq("error/404"), any(Environment.class),
|
||||
any(ClassLoader.class), any(ResourceLoader.class)))
|
||||
.willReturn(true);
|
||||
ModelAndView resolved = this.resolver.resolveErrorView(this.request, HttpStatus.NOT_FOUND, this.model);
|
||||
assertThat(resolved.getViewName()).isEqualTo("error/404");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveWhenExactResourceMatchAndSeriesTemplateMatchShouldFavorResource() throws Exception {
|
||||
setResourceLocation("/exact");
|
||||
given(this.templateAvailabilityProvider.isTemplateAvailable(eq("error/404"), any(Environment.class),
|
||||
any(ClassLoader.class), any(ResourceLoader.class)))
|
||||
.willReturn(false);
|
||||
ModelAndView resolved = this.resolver.resolveErrorView(this.request, HttpStatus.NOT_FOUND, this.model);
|
||||
then(this.templateAvailabilityProvider).shouldHaveNoMoreInteractions();
|
||||
MockHttpServletResponse response = render(resolved);
|
||||
assertThat(response.getContentAsString().trim()).isEqualTo("exact/404");
|
||||
assertThat(response.getContentType()).isEqualTo(MediaType.TEXT_HTML_VALUE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void orderShouldBeLowest() {
|
||||
assertThat(this.resolver.getOrder()).isEqualTo(Ordered.LOWEST_PRECEDENCE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void setOrderShouldChangeOrder() {
|
||||
this.resolver.setOrder(123);
|
||||
assertThat(this.resolver.getOrder()).isEqualTo(123);
|
||||
}
|
||||
|
||||
private void setResourceLocation(String path) {
|
||||
String packageName = getClass().getPackage().getName();
|
||||
this.resourcesProperties
|
||||
.setStaticLocations(new String[] { "classpath:" + packageName.replace('.', '/') + path + "/" });
|
||||
}
|
||||
|
||||
private MockHttpServletResponse render(ModelAndView modelAndView) throws Exception {
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
modelAndView.getView().render(this.model, this.request, response);
|
||||
return response;
|
||||
}
|
||||
|
||||
static class TestTemplateAvailabilityProviders extends TemplateAvailabilityProviders {
|
||||
|
||||
TestTemplateAvailabilityProviders(TemplateAvailabilityProvider provider) {
|
||||
super(Collections.singletonList(provider));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* 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.webmvc.autoconfigure.error;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
|
||||
import org.springframework.boot.test.system.CapturedOutput;
|
||||
import org.springframework.boot.test.system.OutputCaptureExtension;
|
||||
import org.springframework.boot.web.error.ErrorAttributeOptions;
|
||||
import org.springframework.boot.web.error.ErrorAttributeOptions.Include;
|
||||
import org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration;
|
||||
import org.springframework.boot.webmvc.error.ErrorAttributes;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.servlet.View;
|
||||
import org.springframework.web.servlet.handler.DispatcherServletWebRequest;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link ErrorMvcAutoConfiguration}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Scott Frederick
|
||||
*/
|
||||
@ExtendWith(OutputCaptureExtension.class)
|
||||
class ErrorMvcAutoConfigurationTests {
|
||||
|
||||
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner().withConfiguration(
|
||||
AutoConfigurations.of(DispatcherServletAutoConfiguration.class, ErrorMvcAutoConfiguration.class));
|
||||
|
||||
@Test
|
||||
void renderContainsViewWithExceptionDetails() {
|
||||
this.contextRunner.run((context) -> {
|
||||
View errorView = context.getBean("error", View.class);
|
||||
ErrorAttributes errorAttributes = context.getBean(ErrorAttributes.class);
|
||||
DispatcherServletWebRequest webRequest = createWebRequest(new IllegalStateException("Exception message"),
|
||||
false);
|
||||
errorView.render(errorAttributes.getErrorAttributes(webRequest, withAllOptions()), webRequest.getRequest(),
|
||||
webRequest.getResponse());
|
||||
assertThat(webRequest.getResponse().getContentType()).isEqualTo("text/html;charset=UTF-8");
|
||||
String responseString = ((MockHttpServletResponse) webRequest.getResponse()).getContentAsString();
|
||||
assertThat(responseString).contains(
|
||||
"<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>")
|
||||
.contains("<div>Exception message</div>")
|
||||
.contains("<div style='white-space:pre-wrap;'>java.lang.IllegalStateException");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void renderCanUseJavaTimeTypeAsTimestamp() { // gh-23256
|
||||
this.contextRunner.run((context) -> {
|
||||
View errorView = context.getBean("error", View.class);
|
||||
ErrorAttributes errorAttributes = context.getBean(ErrorAttributes.class);
|
||||
DispatcherServletWebRequest webRequest = createWebRequest(new IllegalStateException("Exception message"),
|
||||
false);
|
||||
Map<String, Object> attributes = errorAttributes.getErrorAttributes(webRequest, withAllOptions());
|
||||
attributes.put("timestamp", Clock.systemUTC().instant());
|
||||
errorView.render(attributes, webRequest.getRequest(), webRequest.getResponse());
|
||||
assertThat(webRequest.getResponse().getContentType()).isEqualTo("text/html;charset=UTF-8");
|
||||
String responseString = ((MockHttpServletResponse) webRequest.getResponse()).getContentAsString();
|
||||
assertThat(responseString).contains("This application has no explicit mapping for /error");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void renderWhenAlreadyCommittedLogsMessage(CapturedOutput output) {
|
||||
this.contextRunner.run((context) -> {
|
||||
View errorView = context.getBean("error", View.class);
|
||||
ErrorAttributes errorAttributes = context.getBean(ErrorAttributes.class);
|
||||
DispatcherServletWebRequest webRequest = createWebRequest(new IllegalStateException("Exception message"),
|
||||
true);
|
||||
errorView.render(errorAttributes.getErrorAttributes(webRequest, withAllOptions()), webRequest.getRequest(),
|
||||
webRequest.getResponse());
|
||||
assertThat(output).contains("Cannot render error page for request [/path] "
|
||||
+ "and exception [Exception message] as the response has "
|
||||
+ "already been committed. As a result, the response may have the wrong status code.");
|
||||
});
|
||||
}
|
||||
|
||||
private DispatcherServletWebRequest createWebRequest(Exception ex, boolean committed) {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/path");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
DispatcherServletWebRequest webRequest = new DispatcherServletWebRequest(request, response);
|
||||
webRequest.setAttribute("jakarta.servlet.error.exception", ex, RequestAttributes.SCOPE_REQUEST);
|
||||
webRequest.setAttribute("jakarta.servlet.error.request_uri", "/path", RequestAttributes.SCOPE_REQUEST);
|
||||
response.setCommitted(committed);
|
||||
response.setOutputStreamAccessAllowed(!committed);
|
||||
response.setWriterAccessAllowed(!committed);
|
||||
return webRequest;
|
||||
}
|
||||
|
||||
private ErrorAttributeOptions withAllOptions() {
|
||||
return ErrorAttributeOptions.of(Include.values());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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.webmvc.autoconfigure.error;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.boot.http.autoconfigure.HttpMessageConvertersAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
||||
import org.springframework.boot.test.web.client.TestRestTemplate;
|
||||
import org.springframework.boot.test.web.server.LocalServerPort;
|
||||
import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration;
|
||||
import org.springframework.boot.web.server.ErrorPage;
|
||||
import org.springframework.boot.web.server.ErrorPageRegistrar;
|
||||
import org.springframework.boot.web.server.ErrorPageRegistry;
|
||||
import org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration;
|
||||
import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Integration tests for remapped error pages.
|
||||
*
|
||||
* @author Dave Syer
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = "spring.mvc.servlet.path:/spring/")
|
||||
@DirtiesContext
|
||||
class RemappedErrorViewIntegrationTests {
|
||||
|
||||
@LocalServerPort
|
||||
private int port;
|
||||
|
||||
private final TestRestTemplate template = new TestRestTemplate();
|
||||
|
||||
@Test
|
||||
void directAccessToErrorPage() {
|
||||
String content = this.template.getForObject("http://localhost:" + this.port + "/spring/error", String.class);
|
||||
assertThat(content).contains("error");
|
||||
assertThat(content).contains("999");
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardToErrorPage() {
|
||||
String content = this.template.getForObject("http://localhost:" + this.port + "/spring/", String.class);
|
||||
assertThat(content).contains("error");
|
||||
assertThat(content).contains("500");
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@Import({ PropertyPlaceholderAutoConfiguration.class, WebMvcAutoConfiguration.class,
|
||||
HttpMessageConvertersAutoConfiguration.class, TomcatServletWebServerAutoConfiguration.class,
|
||||
DispatcherServletAutoConfiguration.class, ErrorMvcAutoConfiguration.class })
|
||||
@Controller
|
||||
static class TestConfiguration implements ErrorPageRegistrar {
|
||||
|
||||
@RequestMapping("/")
|
||||
String home() {
|
||||
throw new RuntimeException("Planned!");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
|
||||
errorPageRegistry.addErrorPages(new ErrorPage("/spring/error"));
|
||||
}
|
||||
|
||||
// For manual testing
|
||||
static void main(String[] args) {
|
||||
new SpringApplicationBuilder(TestConfiguration.class).properties("spring.mvc.servlet.path:spring/*")
|
||||
.run(args);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,333 @@
|
||||
/*
|
||||
* 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.webmvc.error;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.web.error.ErrorAttributeOptions;
|
||||
import org.springframework.boot.web.error.ErrorAttributeOptions.Include;
|
||||
import org.springframework.context.MessageSourceResolvable;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.validation.MapBindingResult;
|
||||
import org.springframework.validation.ObjectError;
|
||||
import org.springframework.validation.method.MethodValidationResult;
|
||||
import org.springframework.validation.method.ParameterValidationResult;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.context.request.ServletWebRequest;
|
||||
import org.springframework.web.context.request.WebRequest;
|
||||
import org.springframework.web.method.annotation.HandlerMethodValidationException;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link DefaultErrorAttributes}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Vedran Pavic
|
||||
* @author Scott Frederick
|
||||
* @author Moritz Halbritter
|
||||
* @author Yanming Zhou
|
||||
*/
|
||||
class DefaultErrorAttributesTests {
|
||||
|
||||
private final DefaultErrorAttributes errorAttributes = new DefaultErrorAttributes();
|
||||
|
||||
private final MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
|
||||
private final WebRequest webRequest = new ServletWebRequest(this.request);
|
||||
|
||||
@Test
|
||||
void includeTimeStamp() {
|
||||
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
|
||||
ErrorAttributeOptions.defaults());
|
||||
assertThat(attributes.get("timestamp")).isInstanceOf(Date.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void specificStatusCode() {
|
||||
this.request.setAttribute("jakarta.servlet.error.status_code", 404);
|
||||
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
|
||||
ErrorAttributeOptions.defaults());
|
||||
assertThat(attributes).containsEntry("error", HttpStatus.NOT_FOUND.getReasonPhrase());
|
||||
assertThat(attributes).containsEntry("status", 404);
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingStatusCode() {
|
||||
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
|
||||
ErrorAttributeOptions.defaults());
|
||||
assertThat(attributes).containsEntry("error", "None");
|
||||
assertThat(attributes).containsEntry("status", 999);
|
||||
}
|
||||
|
||||
@Test
|
||||
void mvcError() {
|
||||
RuntimeException ex = new RuntimeException("Test");
|
||||
ModelAndView modelAndView = this.errorAttributes.resolveException(this.request, null, null, ex);
|
||||
this.request.setAttribute("jakarta.servlet.error.exception", new RuntimeException("Ignored"));
|
||||
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
|
||||
ErrorAttributeOptions.of(Include.MESSAGE));
|
||||
assertThat(this.errorAttributes.getError(this.webRequest)).isSameAs(ex);
|
||||
assertThat(modelAndView).isNull();
|
||||
assertThat(attributes).doesNotContainKey("exception");
|
||||
assertThat(attributes).containsEntry("message", "Test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void servletErrorWithMessage() {
|
||||
RuntimeException ex = new RuntimeException("Test");
|
||||
this.request.setAttribute("jakarta.servlet.error.exception", ex);
|
||||
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
|
||||
ErrorAttributeOptions.of(Include.MESSAGE));
|
||||
assertThat(this.errorAttributes.getError(this.webRequest)).isSameAs(ex);
|
||||
assertThat(attributes).doesNotContainKey("exception");
|
||||
assertThat(attributes).containsEntry("message", "Test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void servletErrorWithoutMessage() {
|
||||
RuntimeException ex = new RuntimeException("Test");
|
||||
this.request.setAttribute("jakarta.servlet.error.exception", ex);
|
||||
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
|
||||
ErrorAttributeOptions.defaults());
|
||||
assertThat(this.errorAttributes.getError(this.webRequest)).isSameAs(ex);
|
||||
assertThat(attributes).doesNotContainKey("exception");
|
||||
assertThat(attributes).doesNotContainKey("message");
|
||||
}
|
||||
|
||||
@Test
|
||||
void servletMessageWithMessage() {
|
||||
this.request.setAttribute("jakarta.servlet.error.message", "Test");
|
||||
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
|
||||
ErrorAttributeOptions.of(Include.MESSAGE));
|
||||
assertThat(attributes).doesNotContainKey("exception");
|
||||
assertThat(attributes).containsEntry("message", "Test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void servletMessageWithoutMessage() {
|
||||
this.request.setAttribute("jakarta.servlet.error.message", "Test");
|
||||
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
|
||||
ErrorAttributeOptions.defaults());
|
||||
assertThat(attributes).doesNotContainKey("exception");
|
||||
assertThat(attributes).doesNotContainKey("message");
|
||||
}
|
||||
|
||||
@Test
|
||||
void nullExceptionMessage() {
|
||||
this.request.setAttribute("jakarta.servlet.error.exception", new RuntimeException());
|
||||
this.request.setAttribute("jakarta.servlet.error.message", "Test");
|
||||
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
|
||||
ErrorAttributeOptions.of(Include.MESSAGE));
|
||||
assertThat(attributes).doesNotContainKey("exception");
|
||||
assertThat(attributes).containsEntry("message", "Test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void nullExceptionMessageAndServletMessage() {
|
||||
this.request.setAttribute("jakarta.servlet.error.exception", new RuntimeException());
|
||||
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
|
||||
ErrorAttributeOptions.of(Include.MESSAGE));
|
||||
assertThat(attributes).doesNotContainKey("exception");
|
||||
assertThat(attributes).containsEntry("message", "No message available");
|
||||
}
|
||||
|
||||
@Test
|
||||
void unwrapServletException() {
|
||||
RuntimeException ex = new RuntimeException("Test");
|
||||
ServletException wrapped = new ServletException(new ServletException(ex));
|
||||
this.request.setAttribute("jakarta.servlet.error.exception", wrapped);
|
||||
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
|
||||
ErrorAttributeOptions.of(Include.MESSAGE));
|
||||
assertThat(this.errorAttributes.getError(this.webRequest)).isSameAs(wrapped);
|
||||
assertThat(attributes).doesNotContainKey("exception");
|
||||
assertThat(attributes).containsEntry("message", "Test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getError() {
|
||||
Error error = new OutOfMemoryError("Test error");
|
||||
this.request.setAttribute("jakarta.servlet.error.exception", error);
|
||||
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
|
||||
ErrorAttributeOptions.of(Include.MESSAGE));
|
||||
assertThat(this.errorAttributes.getError(this.webRequest)).isSameAs(error);
|
||||
assertThat(attributes).doesNotContainKey("exception");
|
||||
assertThat(attributes).containsEntry("message", "Test error");
|
||||
}
|
||||
|
||||
@Test
|
||||
void withBindingErrors() {
|
||||
BindingResult bindingResult = new MapBindingResult(Collections.singletonMap("a", "b"), "objectName");
|
||||
bindingResult.addError(new ObjectError("c", "d"));
|
||||
Exception ex = new BindException(bindingResult);
|
||||
testBindingResult(bindingResult, ex, ErrorAttributeOptions.of(Include.MESSAGE, Include.BINDING_ERRORS));
|
||||
}
|
||||
|
||||
@Test
|
||||
void withoutBindingErrors() {
|
||||
BindingResult bindingResult = new MapBindingResult(Collections.singletonMap("a", "b"), "objectName");
|
||||
bindingResult.addError(new ObjectError("c", "d"));
|
||||
Exception ex = new BindException(bindingResult);
|
||||
testBindingResult(bindingResult, ex, ErrorAttributeOptions.defaults());
|
||||
}
|
||||
|
||||
@Test
|
||||
void withMethodArgumentNotValidExceptionBindingErrors() {
|
||||
Method method = ReflectionUtils.findMethod(String.class, "substring", int.class);
|
||||
MethodParameter parameter = new MethodParameter(method, 0);
|
||||
BindingResult bindingResult = new MapBindingResult(Collections.singletonMap("a", "b"), "objectName");
|
||||
bindingResult.addError(new ObjectError("c", "d"));
|
||||
Exception ex = new MethodArgumentNotValidException(parameter, bindingResult);
|
||||
testBindingResult(bindingResult, ex, ErrorAttributeOptions.of(Include.MESSAGE, Include.BINDING_ERRORS));
|
||||
}
|
||||
|
||||
@Test
|
||||
void withHandlerMethodValidationExceptionBindingErrors() {
|
||||
Object target = "test";
|
||||
Method method = ReflectionUtils.findMethod(String.class, "substring", int.class);
|
||||
MethodParameter parameter = new MethodParameter(method, 0);
|
||||
MethodValidationResult methodValidationResult = MethodValidationResult.create(target, method,
|
||||
List.of(new ParameterValidationResult(parameter, -1,
|
||||
List.of(new ObjectError("beginIndex", "beginIndex is negative")), null, null, null,
|
||||
(error, sourceType) -> {
|
||||
throw new IllegalArgumentException("No source object of the given type");
|
||||
})));
|
||||
HandlerMethodValidationException ex = new HandlerMethodValidationException(methodValidationResult);
|
||||
testErrors(methodValidationResult.getAllErrors(),
|
||||
"Validation failed for method='public java.lang.String java.lang.String.substring(int)'. Error count: 1",
|
||||
ex, ErrorAttributeOptions.of(Include.MESSAGE, Include.BINDING_ERRORS));
|
||||
}
|
||||
|
||||
private void testBindingResult(BindingResult bindingResult, Exception ex, ErrorAttributeOptions options) {
|
||||
testErrors(bindingResult.getAllErrors(), "Validation failed for object='objectName'. Error count: 1", ex,
|
||||
options);
|
||||
}
|
||||
|
||||
private void testErrors(List<? extends MessageSourceResolvable> errors, String expectedMessage, Exception ex,
|
||||
ErrorAttributeOptions options) {
|
||||
this.request.setAttribute("jakarta.servlet.error.exception", ex);
|
||||
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest, options);
|
||||
if (options.isIncluded(Include.MESSAGE)) {
|
||||
assertThat(attributes).containsEntry("message", expectedMessage);
|
||||
}
|
||||
else {
|
||||
assertThat(attributes).doesNotContainKey("message");
|
||||
}
|
||||
if (options.isIncluded(Include.BINDING_ERRORS)) {
|
||||
assertThat(attributes).containsEntry("errors", org.springframework.boot.web.error.Error.wrap(errors));
|
||||
}
|
||||
else {
|
||||
assertThat(attributes).doesNotContainKey("errors");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void withExceptionAttribute() {
|
||||
DefaultErrorAttributes errorAttributes = new DefaultErrorAttributes();
|
||||
RuntimeException ex = new RuntimeException("Test");
|
||||
this.request.setAttribute("jakarta.servlet.error.exception", ex);
|
||||
Map<String, Object> attributes = errorAttributes.getErrorAttributes(this.webRequest,
|
||||
ErrorAttributeOptions.of(Include.EXCEPTION, Include.MESSAGE));
|
||||
assertThat(attributes).containsEntry("exception", RuntimeException.class.getName());
|
||||
assertThat(attributes).containsEntry("message", "Test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void withStackTraceAttribute() {
|
||||
RuntimeException ex = new RuntimeException("Test");
|
||||
this.request.setAttribute("jakarta.servlet.error.exception", ex);
|
||||
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
|
||||
ErrorAttributeOptions.of(Include.STACK_TRACE));
|
||||
assertThat(attributes.get("trace").toString()).startsWith("java.lang");
|
||||
}
|
||||
|
||||
@Test
|
||||
void withoutStackTraceAttribute() {
|
||||
RuntimeException ex = new RuntimeException("Test");
|
||||
this.request.setAttribute("jakarta.servlet.error.exception", ex);
|
||||
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
|
||||
ErrorAttributeOptions.defaults());
|
||||
assertThat(attributes).doesNotContainKey("trace");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldIncludePathByDefault() {
|
||||
this.request.setAttribute("jakarta.servlet.error.request_uri", "path");
|
||||
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
|
||||
ErrorAttributeOptions.defaults());
|
||||
assertThat(attributes).containsEntry("path", "path");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldIncludePath() {
|
||||
this.request.setAttribute("jakarta.servlet.error.request_uri", "path");
|
||||
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
|
||||
ErrorAttributeOptions.of(Include.PATH));
|
||||
assertThat(attributes).containsEntry("path", "path");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExcludePath() {
|
||||
this.request.setAttribute("jakarta.servlet.error.request_uri", "path");
|
||||
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
|
||||
ErrorAttributeOptions.of());
|
||||
assertThat(attributes).doesNotContainEntry("path", "path");
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenGetMessageIsOverriddenThenMessageAttributeContainsValueReturnedFromIt() {
|
||||
Map<String, Object> attributes = new DefaultErrorAttributes() {
|
||||
|
||||
@Override
|
||||
protected String getMessage(WebRequest webRequest, Throwable error) {
|
||||
return "custom message";
|
||||
}
|
||||
|
||||
}.getErrorAttributes(this.webRequest, ErrorAttributeOptions.of(Include.MESSAGE));
|
||||
assertThat(attributes).containsEntry("message", "custom message");
|
||||
}
|
||||
|
||||
@Test
|
||||
void excludeStatus() {
|
||||
this.request.setAttribute("jakarta.servlet.error.status_code", 404);
|
||||
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
|
||||
ErrorAttributeOptions.defaults().excluding(Include.STATUS));
|
||||
assertThat(attributes).doesNotContainKey("status");
|
||||
}
|
||||
|
||||
@Test
|
||||
void excludeError() {
|
||||
this.request.setAttribute("jakarta.servlet.error.status_code", 404);
|
||||
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
|
||||
ErrorAttributeOptions.defaults().excluding(Include.ERROR));
|
||||
assertThat(attributes).doesNotContainKey("error");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
4xx/402
|
||||
@@ -0,0 +1 @@
|
||||
4xx/4xx
|
||||
@@ -0,0 +1 @@
|
||||
5xx/4xx
|
||||
@@ -0,0 +1 @@
|
||||
5xx/5xx
|
||||
@@ -0,0 +1 @@
|
||||
exact/404
|
||||
@@ -0,0 +1 @@
|
||||
exact/4xx
|
||||
@@ -0,0 +1,10 @@
|
||||
<!doctype html>
|
||||
<html xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<title>Test Thymeleaf</title>
|
||||
<link th:href="@{/custom.css}" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user