From 309521947984dd8572c1edac30c3a8fd4a75d50f Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Mon, 19 May 2025 08:51:38 +0100 Subject: [PATCH] Support API versioning in MockMvc See gh-34919 --- .../servlet/client/MockMvcWebTestClient.java | 11 ++- .../servlet/client/StandaloneMockMvcSpec.java | 9 ++- ...AbstractMockHttpServletRequestBuilder.java | 78 ++++++++++++++++--- .../servlet/setup/AbstractMockMvcBuilder.java | 19 +++++ .../setup/ConfigurableMockMvcBuilder.java | 11 ++- .../setup/StandaloneMockMvcBuilder.java | 17 +++- ...actMockHttpServletRequestBuilderTests.java | 33 +++++++- .../samples/standalone/ApiVersionTests.java | 78 +++++++++++++++++++ .../setup/StandaloneMockMvcBuilderTests.java | 13 ++++ 9 files changed, 254 insertions(+), 15 deletions(-) create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/ApiVersionTests.java diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/client/MockMvcWebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/servlet/client/MockMvcWebTestClient.java index bab99a0a41..7967512b3f 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/client/MockMvcWebTestClient.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/client/MockMvcWebTestClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-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. @@ -38,6 +38,7 @@ import org.springframework.test.web.servlet.setup.MockMvcConfigurer; import org.springframework.test.web.servlet.setup.RouterFunctionMockMvcBuilder; import org.springframework.test.web.servlet.setup.StandaloneMockMvcBuilder; import org.springframework.validation.Validator; +import org.springframework.web.accept.ApiVersionStrategy; import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.method.support.HandlerMethodArgumentResolver; @@ -284,6 +285,14 @@ public interface MockMvcWebTestClient { */ ControllerSpec conversionService(FormattingConversionService conversionService); + /** + * Set the {@link ApiVersionStrategy} to use when mapping requests. + *

This is delegated to + * {@link StandaloneMockMvcBuilder#setApiVersionStrategy(ApiVersionStrategy)}. + * @since 7.0 + */ + ControllerSpec apiVersionStrategy(ApiVersionStrategy versionStrategy); + /** * Add global interceptors. *

This is delegated to diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/client/StandaloneMockMvcSpec.java b/spring-test/src/main/java/org/springframework/test/web/servlet/client/StandaloneMockMvcSpec.java index b4a2e9c0e1..d0f6712f11 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/client/StandaloneMockMvcSpec.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/client/StandaloneMockMvcSpec.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-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. @@ -26,6 +26,7 @@ import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.test.web.servlet.setup.StandaloneMockMvcBuilder; import org.springframework.validation.Validator; +import org.springframework.web.accept.ApiVersionStrategy; import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.HandlerMethodReturnValueHandler; @@ -79,6 +80,12 @@ class StandaloneMockMvcSpec extends AbstractMockMvcServerSpec locales = new ArrayList<>(); + private @Nullable Object version; + + private @Nullable ApiVersionInserter versionInserter; + private final Map requestAttributes = new LinkedHashMap<>(); private final Map sessionAttributes = new LinkedHashMap<>(); @@ -469,6 +476,34 @@ public abstract class AbstractMockHttpServletRequestBuilder entry : parentBuilder.requestAttributes.entrySet()) { String attributeName = entry.getKey(); if (!this.requestAttributes.containsKey(attributeName)) { @@ -700,7 +743,15 @@ public abstract class AbstractMockHttpServletRequestBuilder values.forEach(value -> this.headers.add(name, value))); + } + this.headers.forEach((name, values) -> { for (Object value : values) { request.addHeader(name, value); @@ -753,7 +811,7 @@ public abstract class AbstractMockHttpServletRequestBuilder { for (String value : values) { 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 7667f652d4..2189ad5dd6 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 @@ -36,10 +36,12 @@ import org.springframework.test.web.servlet.MockMvcBuilderSupport; import org.springframework.test.web.servlet.RequestBuilder; import org.springframework.test.web.servlet.ResultHandler; import org.springframework.test.web.servlet.ResultMatcher; +import org.springframework.test.web.servlet.request.AbstractMockHttpServletRequestBuilder; import org.springframework.test.web.servlet.request.ConfigurableSmartRequestBuilder; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.request.RequestPostProcessor; import org.springframework.util.Assert; +import org.springframework.web.client.ApiVersionInserter; import org.springframework.web.context.WebApplicationContext; /** @@ -62,6 +64,8 @@ public abstract class AbstractMockMvcBuilder private final List filters = new ArrayList<>(); + private @Nullable ApiVersionInserter apiVersionInserter; + private @Nullable RequestBuilder defaultRequestBuilder; private @Nullable Charset defaultResponseCharacterEncoding; @@ -106,6 +110,12 @@ public abstract class AbstractMockMvcBuilder return self(); } + @Override + public T apiVersionInserter(ApiVersionInserter versionInserter) { + this.apiVersionInserter = versionInserter; + return self(); + } + @Override public final T defaultRequest(RequestBuilder requestBuilder) { this.defaultRequestBuilder = requestBuilder; @@ -194,6 +204,15 @@ public abstract class AbstractMockMvcBuilder } } + if (this.apiVersionInserter != null) { + if (this.defaultRequestBuilder == null) { + this.defaultRequestBuilder = MockMvcRequestBuilders.get("/"); + } + if (this.defaultRequestBuilder instanceof AbstractMockHttpServletRequestBuilder srb) { + srb.apiVersionInserter(this.apiVersionInserter); + } + } + return super.createMockMvc(filterArray, mockServletConfig, wac, this.defaultRequestBuilder, this.defaultResponseCharacterEncoding, this.globalResultMatchers, this.globalResultHandlers, this.dispatcherServletCustomizers); 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 109280cd53..7afd093c88 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-2023 the original author or authors. + * Copyright 2002-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. @@ -30,6 +30,7 @@ import org.springframework.test.web.servlet.MockMvcBuilder; import org.springframework.test.web.servlet.RequestBuilder; import org.springframework.test.web.servlet.ResultHandler; import org.springframework.test.web.servlet.ResultMatcher; +import org.springframework.web.client.ApiVersionInserter; /** * Defines common methods for building a {@code MockMvc}. @@ -76,6 +77,14 @@ public interface ConfigurableMockMvcBuilder initParams, EnumSet dispatcherTypes, String... urlPatterns); + /** + * Set the {@link ApiVersionInserter} to use to apply to versions specified via + * {@link org.springframework.test.web.servlet.request.AbstractMockHttpServletRequestBuilder#apiVersion(Object)}. + * @param versionInserter the inserter to use + * @since 7.0 + */ + T apiVersionInserter(ApiVersionInserter versionInserter); + /** * 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/StandaloneMockMvcBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StandaloneMockMvcBuilder.java index 4f0ec932a9..2759335aed 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StandaloneMockMvcBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StandaloneMockMvcBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-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. @@ -42,6 +42,7 @@ import org.springframework.util.PropertyPlaceholderHelper; import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; import org.springframework.util.StringValueResolver; import org.springframework.validation.Validator; +import org.springframework.web.accept.ApiVersionStrategy; import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.WebApplicationObjectSupport; @@ -108,6 +109,8 @@ public class StandaloneMockMvcBuilder extends AbstractMockMvcBuilder handlerExceptionResolvers; private @Nullable Long asyncRequestTimeout; @@ -189,6 +192,15 @@ public class StandaloneMockMvcBuilder extends AbstractMockMvcBuilder builder) { return builder.buildRequest(this.servletContext); diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/ApiVersionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/ApiVersionTests.java new file mode 100644 index 0000000000..2472ed3154 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/ApiVersionTests.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-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.test.web.servlet.samples.standalone; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.http.MediaType; +import org.springframework.stereotype.Controller; +import org.springframework.test.web.Person; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.accept.DefaultApiVersionStrategy; +import org.springframework.web.accept.SemanticApiVersionParser; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.client.ApiVersionInserter; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup; + +/** + * Tests demonstrating the use of API version. + * @author Rossen Stoyanchev + */ +public class ApiVersionTests { + + @Test + public void queryParameter() throws Exception { + + String header = "API-Version"; + + DefaultApiVersionStrategy versionStrategy = new DefaultApiVersionStrategy( + List.of(request -> request.getHeader(header)), + new SemanticApiVersionParser(), + true, null); + + MockMvc mockMvc = standaloneSetup(new PersonController()) + .setApiVersionStrategy(versionStrategy) + .apiVersionInserter(ApiVersionInserter.useHeader(header)) + .build(); + + mockMvc.perform(get("/search?name=George").accept(MediaType.APPLICATION_JSON).apiVersion(1.1)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/json")) + .andExpect(jsonPath("$.name").value("George")); + } + + + @Controller + private static class PersonController { + + @RequestMapping(path="/search", version = "1.1") + @ResponseBody + public Person get(@RequestParam String name) { + return new Person(name); + } + } + +} 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 0d120a5832..9ef7f287a1 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 @@ -36,6 +36,7 @@ import org.springframework.http.support.JacksonHandlerInstantiator; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.stereotype.Controller; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.accept.ApiVersionStrategy; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.WebApplicationContextUtils; @@ -74,6 +75,18 @@ class StandaloneMockMvcBuilderTests { assertThat(((HandlerMethod) chain.getHandler()).getMethod().getName()).isEqualTo("handleWithPlaceholders"); } + @Test + void apiVersionStrategySet() { + ApiVersionStrategy versionStrategy = mock(ApiVersionStrategy.class); + + TestStandaloneMockMvcBuilder builder = new TestStandaloneMockMvcBuilder(); + builder.setApiVersionStrategy(versionStrategy); + builder.build(); + + assertThat(builder.wac.getBean(RequestMappingHandlerMapping.class).getApiVersionStrategy()) + .isSameAs(versionStrategy); + } + @Test // SPR-12553 void applicationContextAttribute() { TestStandaloneMockMvcBuilder builder = new TestStandaloneMockMvcBuilder(new PlaceholderController());