diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/MockMvcBuilderSupport.java b/spring-test/src/main/java/org/springframework/test/web/servlet/MockMvcBuilderSupport.java index 7ea2eac36338..40403ddf60f7 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/MockMvcBuilderSupport.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/MockMvcBuilderSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -55,7 +55,10 @@ protected final MockMvc createMockMvc(Filter[] filters, MockServletConfig servle List globalResultMatchers, List globalResultHandlers, @Nullable List dispatcherServletCustomizers) { - MockMvc mockMvc = createMockMvc(filters, servletConfig, webAppContext, defaultRequestBuilder, globalResultMatchers, globalResultHandlers, dispatcherServletCustomizers); + MockMvc mockMvc = createMockMvc( + filters, servletConfig, webAppContext, defaultRequestBuilder, + globalResultMatchers, globalResultHandlers, dispatcherServletCustomizers); + mockMvc.setDefaultResponseCharacterEncoding(defaultResponseCharacterEncoding); return mockMvc; } @@ -75,7 +78,7 @@ protected final MockMvc createMockMvc(Filter[] filters, MockServletConfig servle dispatcherServlet.init(servletConfig); } catch (ServletException ex) { - // should never happen.. + // should never happen... throw new MockMvcBuildException("Failed to initialize TestDispatcherServlet", ex); } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/AbstractMockMvcBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/AbstractMockMvcBuilder.java index 269ed8836aff..856d4d619907 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/AbstractMockMvcBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/AbstractMockMvcBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -18,10 +18,14 @@ import java.nio.charset.Charset; import java.util.ArrayList; +import java.util.EnumSet; import java.util.List; +import java.util.Map; +import jakarta.servlet.DispatcherType; import jakarta.servlet.Filter; import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; import org.springframework.lang.Nullable; import org.springframework.mock.web.MockServletConfig; @@ -76,9 +80,9 @@ public abstract class AbstractMockMvcBuilder @Override public final T addFilters(Filter... filters) { Assert.notNull(filters, "filters cannot be null"); - for (Filter f : filters) { - Assert.notNull(f, "filters cannot contain null values"); - this.filters.add(f); + for (Filter filter : filters) { + Assert.notNull(filter, "filters cannot contain null values"); + this.filters.add(filter); } return self(); } @@ -88,12 +92,22 @@ public final T addFilter(Filter filter, String... urlPatterns) { Assert.notNull(filter, "filter cannot be null"); Assert.notNull(urlPatterns, "urlPatterns cannot be null"); if (urlPatterns.length > 0) { - filter = new PatternMappingFilterProxy(filter, urlPatterns); + filter = new MockMvcFilterDecorator(filter, urlPatterns); } this.filters.add(filter); return self(); } + @Override + public T addFilter( + Filter filter, Map initParams, + EnumSet dispatcherTypes, String... urlPatterns) { + + filter = new MockMvcFilterDecorator(filter, initParams, dispatcherTypes, urlPatterns); + this.filters.add(filter); + return self(); + } + @Override public final T defaultRequest(RequestBuilder requestBuilder) { this.defaultRequestBuilder = requestBuilder; @@ -171,6 +185,16 @@ public final MockMvc build() { } Filter[] filterArray = this.filters.toArray(new Filter[0]); + for (Filter filter : filterArray) { + if (filter instanceof MockMvcFilterDecorator filterDecorator) { + try { + filterDecorator.initIfRequired(servletContext); + } + catch (ServletException ex) { + throw new RuntimeException("Failed to initialize Filter " + filter, ex); + } + } + } return super.createMockMvc(filterArray, mockServletConfig, wac, this.defaultRequestBuilder, this.defaultResponseCharacterEncoding, this.globalResultMatchers, this.globalResultHandlers, diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/ConfigurableMockMvcBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/ConfigurableMockMvcBuilder.java index fcd96945c261..11ad705dc751 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/ConfigurableMockMvcBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/ConfigurableMockMvcBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -17,8 +17,12 @@ package org.springframework.test.web.servlet.setup; import java.nio.charset.Charset; +import java.util.EnumSet; +import java.util.Map; +import jakarta.servlet.DispatcherType; import jakarta.servlet.Filter; +import jakarta.servlet.FilterConfig; import org.springframework.test.web.servlet.DispatcherServletCustomizer; import org.springframework.test.web.servlet.MockMvcBuilder; @@ -37,40 +41,37 @@ public interface ConfigurableMockMvcBuilder> extends MockMvcBuilder { /** - * Add filters mapped to any request (i.e. "/*"). For example: - *
-	 * mockMvcBuilder.addFilters(springSecurityFilterChain);
-	 * 
- *

It is the equivalent of the following web.xml configuration: - *

-	 * <filter-mapping>
-	 *     <filter-name>springSecurityFilterChain</filter-name>
-	 *     <url-pattern>/*</url-pattern>
-	 * </filter-mapping>
-	 * 
- *

Filters will be invoked in the order in which they are provided. + * Add filters mapped to all requests. Filters are invoked in the same order. + *

Note: if you need the filter to be initialized with {@link Filter#init(FilterConfig)}, + * please use {@link #addFilter(Filter, Map, EnumSet, String...)} instead. * @param filters the filters to add */ T addFilters(Filter... filters); /** - * Add a filter mapped to a specific set of patterns. For example: - *

-	 * mockMvcBuilder.addFilter(myResourceFilter, "/resources/*");
-	 * 
- *

It is the equivalent of: - *

-	 * <filter-mapping>
-	 *     <filter-name>myResourceFilter</filter-name>
-	 *     <url-pattern>/resources/*</url-pattern>
-	 * </filter-mapping>
-	 * 
- *

Filters will be invoked in the order in which they are provided. + * Add a filter mapped to specific patterns. + *

Note: if you need the filter to be initialized with {@link Filter#init(FilterConfig)}, + * please use {@link #addFilter(Filter, Map, EnumSet, String...)} instead. * @param filter the filter to add - * @param urlPatterns the URL patterns to map to; if empty, "/*" is used by default + * @param urlPatterns the URL patterns to map to; if empty, matches all requests */ T addFilter(Filter filter, String... urlPatterns); + /** + * Add a filter that will be initialized via {@link Filter#init(FilterConfig)} + * with the given init parameters, and will also apply only to requests that + * match the given dispatcher types and URL patterns. + * @param filter the filter to add + * @param initParams the init parameters to initialize the filter with + * @param dispatcherTypes dispatcher types the filter applies to + * @param urlPatterns the URL patterns to map to; if empty, matches all requests + * @since 6.1 + * @see org.springframework.mock.web.MockFilterConfig + */ + T addFilter( + Filter filter, Map initParams, + EnumSet dispatcherTypes, String... urlPatterns); + /** * Define default request properties that should be merged into all * performed requests. In effect this provides a mechanism for defining diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/PatternMappingFilterProxy.java b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/MockMvcFilterDecorator.java similarity index 64% rename from spring-test/src/main/java/org/springframework/test/web/servlet/setup/PatternMappingFilterProxy.java rename to spring-test/src/main/java/org/springframework/test/web/servlet/setup/MockMvcFilterDecorator.java index 2bcb3135b843..b7120a24d306 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/PatternMappingFilterProxy.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/MockMvcFilterDecorator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 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. @@ -18,16 +18,22 @@ import java.io.IOException; import java.util.ArrayList; +import java.util.EnumSet; import java.util.List; +import java.util.Map; +import jakarta.servlet.DispatcherType; import jakarta.servlet.Filter; import jakarta.servlet.FilterChain; import jakarta.servlet.FilterConfig; +import jakarta.servlet.ServletContext; import jakarta.servlet.ServletException; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletRequest; +import org.springframework.lang.Nullable; +import org.springframework.mock.web.MockFilterConfig; import org.springframework.util.Assert; import org.springframework.web.util.UrlPathHelper; @@ -39,7 +45,7 @@ * @author Rob Winch * @since 3.2 */ -final class PatternMappingFilterProxy implements Filter { +final class MockMvcFilterDecorator implements Filter { private static final String EXTENSION_MAPPING_PATTERN = "*."; @@ -47,6 +53,14 @@ final class PatternMappingFilterProxy implements Filter { private final Filter delegate; + @Nullable + private final Map initParams; + + @Nullable + private final EnumSet dispatcherTypes; + + private final boolean hasPatterns; + /** Patterns that require an exact match, e.g. "/test" */ private final List exactMatches = new ArrayList<>(); @@ -58,11 +72,27 @@ final class PatternMappingFilterProxy implements Filter { /** - * Creates a new instance. + * Create instance with URL patterns only. + *

Note: when this constructor is used, the Filter is not initialized. + */ + public MockMvcFilterDecorator(Filter delegate, String[] urlPatterns) { + this(delegate, null, null, urlPatterns); + } + + /** + * Create instance with init parameters to initialize the filter with, + * as well as dispatcher types and URL patterns to match. */ - public PatternMappingFilterProxy(Filter delegate, String... urlPatterns) { - Assert.notNull(delegate, "A delegate Filter is required"); + public MockMvcFilterDecorator( + Filter delegate, @Nullable Map initParams, + @Nullable EnumSet dispatcherTypes, String... urlPatterns) { + + Assert.notNull(delegate, "filter cannot be null"); + Assert.notNull(urlPatterns, "urlPatterns cannot be null"); this.delegate = delegate; + this.initParams = initParams; + this.dispatcherTypes = dispatcherTypes; + this.hasPatterns = (urlPatterns.length != 0); for (String urlPattern : urlPatterns) { addUrlPattern(urlPattern); } @@ -96,7 +126,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha HttpServletRequest httpRequest = (HttpServletRequest) request; String requestPath = UrlPathHelper.defaultInstance.getPathWithinApplication(httpRequest); - if (matches(requestPath)) { + if (matchDispatcherType(httpRequest.getDispatcherType()) && matchRequestPath(requestPath)) { this.delegate.doFilter(request, response, filterChain); } else { @@ -104,7 +134,15 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha } } - private boolean matches(String requestPath) { + private boolean matchDispatcherType(DispatcherType dispatcherType) { + return (this.dispatcherTypes == null || + this.dispatcherTypes.stream().anyMatch(type -> type == dispatcherType)); + } + + private boolean matchRequestPath(String requestPath) { + if (!this.hasPatterns) { + return true; + } for (String pattern : this.exactMatches) { if (pattern.equals(requestPath)) { return true; @@ -136,4 +174,12 @@ public void destroy() { this.delegate.destroy(); } + public void initIfRequired(@Nullable ServletContext servletContext) throws ServletException { + if (this.initParams != null) { + MockFilterConfig filterConfig = new MockFilterConfig(servletContext); + this.initParams.forEach(filterConfig::addInitParameter); + this.delegate.init(filterConfig); + } + } + } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/setup/ConditionalDelegatingFilterProxyTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/setup/MockMvcFilterDecoratorTests.java similarity index 86% rename from spring-test/src/test/java/org/springframework/test/web/servlet/setup/ConditionalDelegatingFilterProxyTests.java rename to spring-test/src/test/java/org/springframework/test/web/servlet/setup/MockMvcFilterDecoratorTests.java index ffaa26a02a48..eb9cea257e56 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/setup/ConditionalDelegatingFilterProxyTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/setup/MockMvcFilterDecoratorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2023 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. @@ -16,6 +16,9 @@ package org.springframework.test.web.servlet.setup; +import java.util.EnumSet; + +import jakarta.servlet.DispatcherType; import jakarta.servlet.Filter; import jakarta.servlet.FilterChain; import jakarta.servlet.FilterConfig; @@ -32,13 +35,12 @@ import static org.assertj.core.api.Assertions.assertThat; - - - /** + * Unit tests for {@link MockMvcFilterDecorator}. * @author Rob Winch + * @author Rossen Stoyanchev */ -public class ConditionalDelegatingFilterProxyTests { +public class MockMvcFilterDecoratorTests { private MockHttpServletRequest request; @@ -48,7 +50,7 @@ public class ConditionalDelegatingFilterProxyTests { private MockFilter delegate; - private PatternMappingFilterProxy filter; + private MockMvcFilterDecorator filter; @BeforeEach @@ -64,14 +66,14 @@ public void setup() { @Test public void init() throws Exception { FilterConfig config = new MockFilterConfig(); - filter = new PatternMappingFilterProxy(delegate, "/"); + filter = new MockMvcFilterDecorator(delegate, null, null, "/"); filter.init(config); assertThat(delegate.filterConfig).isEqualTo(config); } @Test - public void destroy() throws Exception { - filter = new PatternMappingFilterProxy(delegate, "/"); + public void destroy() { + filter = new MockMvcFilterDecorator(delegate, null, null, "/"); filter.destroy(); assertThat(delegate.destroy).isTrue(); } @@ -146,6 +148,11 @@ public void noMatchPathMappingMissingSlash() throws Exception { assertFilterNotInvoked("/test2", "/test/*"); } + @Test + public void noMatchDispatcherType() throws Exception { + assertFilterNotInvoked(DispatcherType.FORWARD, DispatcherType.REQUEST, "/test", "/test"); + } + @Test public void matchExtensionMulti() throws Exception { assertFilterInvoked("/test/this/here.html", "*.html"); @@ -231,8 +238,16 @@ public void specExtensionMapping() throws Exception { } private void assertFilterNotInvoked(String requestUri, String pattern) throws Exception { + assertFilterNotInvoked(DispatcherType.REQUEST, DispatcherType.REQUEST, requestUri, pattern); + } + + private void assertFilterNotInvoked( + DispatcherType requestDispatcherType, DispatcherType filterDispatcherType, + String requestUri, String pattern) throws Exception { + + request.setDispatcherType(requestDispatcherType); request.setRequestURI(request.getContextPath() + requestUri); - filter = new PatternMappingFilterProxy(delegate, pattern); + filter = new MockMvcFilterDecorator(delegate, null, EnumSet.of(filterDispatcherType), pattern); filter.doFilter(request, response, filterChain); assertThat(delegate.request).isNull(); @@ -246,7 +261,7 @@ private void assertFilterNotInvoked(String requestUri, String pattern) throws Ex private void assertFilterInvoked(String requestUri, String pattern) throws Exception { request.setRequestURI(request.getContextPath() + requestUri); - filter = new PatternMappingFilterProxy(delegate, pattern); + filter = new MockMvcFilterDecorator(delegate, null, null, pattern); filter.doFilter(request, response, filterChain); assertThat(delegate.request).isEqualTo(request); diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/setup/StandaloneMockMvcBuilderTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/setup/StandaloneMockMvcBuilderTests.java index 463ad1807308..762b5dae6fce 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/setup/StandaloneMockMvcBuilderTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/setup/StandaloneMockMvcBuilderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2023 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. @@ -17,15 +17,20 @@ package org.springframework.test.web.servlet.setup; import java.io.IOException; +import java.util.EnumSet; +import java.util.Map; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.ser.impl.UnknownSerializer; +import jakarta.servlet.DispatcherType; import jakarta.servlet.Filter; import jakarta.servlet.FilterChain; +import jakarta.servlet.FilterConfig; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.springframework.http.converter.json.SpringHandlerInstantiator; import org.springframework.mock.web.MockHttpServletRequest; @@ -40,6 +45,9 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; /** * Tests for {@link StandaloneMockMvcBuilder} @@ -120,6 +128,19 @@ void addFilterPatternContainsNull() { builder.addFilter(new ContinueFilter(), (String) null)); } + @Test + void addFilterWithInitParams() throws ServletException { + Filter filter = mock(Filter.class); + ArgumentCaptor captor = ArgumentCaptor.forClass(FilterConfig.class); + + MockMvcBuilders.standaloneSetup(new PersonController()) + .addFilter(filter, Map.of("p", "v"), EnumSet.of(DispatcherType.REQUEST), "/") + .build(); + + verify(filter, times(1)).init(captor.capture()); + assertThat(captor.getValue().getInitParameter("p")).isEqualTo("v"); + } + @Test // SPR-13375 @SuppressWarnings("rawtypes") void springHandlerInstantiator() {