diff --git a/docs/src/main/asciidoc/_configprops.adoc b/docs/src/main/asciidoc/_configprops.adoc index b256efec..ab087e78 100644 --- a/docs/src/main/asciidoc/_configprops.adoc +++ b/docs/src/main/asciidoc/_configprops.adoc @@ -2,6 +2,7 @@ |Name | Default | Description |feign.client.config | | +|feign.client.decode-slash | `true` | Feign clients do not encode slash `/` characters by default. To change this behavior, set the `decodeSlash` to `false`. |feign.client.default-config | `default` | |feign.client.default-to-properties | `true` | |feign.compression.request.enabled | `false` | Enables the request sent by Feign to be compressed. diff --git a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc index f8ef02f3..7bf0cf56 100644 --- a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc +++ b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc @@ -272,6 +272,8 @@ public FeignClientConfigurer feignClientConfigurer() { } ---- +TIP: By default, Feign clients do not encode slash `/` characters. You can change this behaviour, by setting the value of `feign.client.decodeSlash` to `false`. + [[timeout-handling]] === Timeout Handling diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientProperties.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientProperties.java index 0e8df314..da77ffe9 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientProperties.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientProperties.java @@ -36,6 +36,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties; /** * @author Eko Kurniawan Khannedy * @author Ilia Ilinykh + * @author Ram Anaswara */ @ConfigurationProperties("feign.client") public class FeignClientProperties { @@ -46,8 +47,14 @@ public class FeignClientProperties { private Map config = new HashMap<>(); + /** + * Feign clients do not encode slash `/` characters by default. To change this + * behavior, set the `decodeSlash` to `false`. + */ + private boolean decodeSlash = true; + public boolean isDefaultToProperties() { - return this.defaultToProperties; + return defaultToProperties; } public void setDefaultToProperties(boolean defaultToProperties) { @@ -55,7 +62,7 @@ public class FeignClientProperties { } public String getDefaultConfig() { - return this.defaultConfig; + return defaultConfig; } public void setDefaultConfig(String defaultConfig) { @@ -63,13 +70,21 @@ public class FeignClientProperties { } public Map getConfig() { - return this.config; + return config; } public void setConfig(Map config) { this.config = config; } + public boolean isDecodeSlash() { + return decodeSlash; + } + + public void setDecodeSlash(boolean decodeSlash) { + this.decodeSlash = decodeSlash; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -79,13 +94,13 @@ public class FeignClientProperties { return false; } FeignClientProperties that = (FeignClientProperties) o; - return this.defaultToProperties == that.defaultToProperties - && Objects.equals(this.defaultConfig, that.defaultConfig) && Objects.equals(this.config, that.config); + return defaultToProperties == that.defaultToProperties && Objects.equals(defaultConfig, that.defaultConfig) + && Objects.equals(config, that.config) && Objects.equals(decodeSlash, that.decodeSlash); } @Override public int hashCode() { - return Objects.hash(this.defaultToProperties, this.defaultConfig, this.config); + return Objects.hash(defaultToProperties, defaultConfig, config, decodeSlash); } /** @@ -120,7 +135,7 @@ public class FeignClientProperties { private ExceptionPropagationPolicy exceptionPropagationPolicy; public Logger.Level getLoggerLevel() { - return this.loggerLevel; + return loggerLevel; } public void setLoggerLevel(Logger.Level loggerLevel) { @@ -128,7 +143,7 @@ public class FeignClientProperties { } public Integer getConnectTimeout() { - return this.connectTimeout; + return connectTimeout; } public void setConnectTimeout(Integer connectTimeout) { @@ -136,7 +151,7 @@ public class FeignClientProperties { } public Integer getReadTimeout() { - return this.readTimeout; + return readTimeout; } public void setReadTimeout(Integer readTimeout) { @@ -144,7 +159,7 @@ public class FeignClientProperties { } public Class getRetryer() { - return this.retryer; + return retryer; } public void setRetryer(Class retryer) { @@ -152,7 +167,7 @@ public class FeignClientProperties { } public Class getErrorDecoder() { - return this.errorDecoder; + return errorDecoder; } public void setErrorDecoder(Class errorDecoder) { @@ -160,7 +175,7 @@ public class FeignClientProperties { } public List> getRequestInterceptors() { - return this.requestInterceptors; + return requestInterceptors; } public void setRequestInterceptors(List> requestInterceptors) { @@ -184,7 +199,7 @@ public class FeignClientProperties { } public Boolean getDecode404() { - return this.decode404; + return decode404; } public void setDecode404(Boolean decode404) { @@ -192,7 +207,7 @@ public class FeignClientProperties { } public Class getDecoder() { - return this.decoder; + return decoder; } public void setDecoder(Class decoder) { @@ -200,7 +215,7 @@ public class FeignClientProperties { } public Class getEncoder() { - return this.encoder; + return encoder; } public void setEncoder(Class encoder) { @@ -208,7 +223,7 @@ public class FeignClientProperties { } public Class getContract() { - return this.contract; + return contract; } public void setContract(Class contract) { @@ -232,23 +247,22 @@ public class FeignClientProperties { return false; } FeignClientConfiguration that = (FeignClientConfiguration) o; - return this.loggerLevel == that.loggerLevel && Objects.equals(this.connectTimeout, that.connectTimeout) - && Objects.equals(this.readTimeout, that.readTimeout) && Objects.equals(this.retryer, that.retryer) - && Objects.equals(this.errorDecoder, that.errorDecoder) - && Objects.equals(this.requestInterceptors, that.requestInterceptors) - && Objects.equals(this.decode404, that.decode404) && Objects.equals(this.encoder, that.encoder) - && Objects.equals(this.decoder, that.decoder) && Objects.equals(this.contract, that.contract) - && Objects.equals(this.exceptionPropagationPolicy, that.exceptionPropagationPolicy) - && Objects.equals(this.defaultRequestHeaders, that.defaultRequestHeaders) - && Objects.equals(this.defaultQueryParameters, that.defaultQueryParameters); + return loggerLevel == that.loggerLevel && Objects.equals(connectTimeout, that.connectTimeout) + && Objects.equals(readTimeout, that.readTimeout) && Objects.equals(retryer, that.retryer) + && Objects.equals(errorDecoder, that.errorDecoder) + && Objects.equals(requestInterceptors, that.requestInterceptors) + && Objects.equals(decode404, that.decode404) && Objects.equals(encoder, that.encoder) + && Objects.equals(decoder, that.decoder) && Objects.equals(contract, that.contract) + && Objects.equals(exceptionPropagationPolicy, that.exceptionPropagationPolicy) + && Objects.equals(defaultRequestHeaders, that.defaultRequestHeaders) + && Objects.equals(defaultQueryParameters, that.defaultQueryParameters); } @Override public int hashCode() { - return Objects.hash(this.loggerLevel, this.connectTimeout, this.readTimeout, this.retryer, - this.errorDecoder, this.requestInterceptors, this.decode404, this.encoder, this.decoder, - this.contract, this.exceptionPropagationPolicy, this.defaultQueryParameters, - this.defaultRequestHeaders); + return Objects.hash(loggerLevel, connectTimeout, readTimeout, retryer, errorDecoder, requestInterceptors, + decode404, encoder, decoder, contract, exceptionPropagationPolicy, defaultQueryParameters, + defaultRequestHeaders); } } diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java index a7f72ea2..f6566a45 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java @@ -79,6 +79,9 @@ public class FeignClientsConfiguration { @Autowired(required = false) private SpringDataWebProperties springDataWebProperties; + @Autowired(required = false) + private FeignClientProperties feignClientProperties; + @Bean @ConditionalOnMissingBean public Decoder feignDecoder() { @@ -109,7 +112,8 @@ public class FeignClientsConfiguration { @Bean @ConditionalOnMissingBean public Contract feignContract(ConversionService feignConversionService) { - return new SpringMvcContract(this.parameterProcessors, feignConversionService); + boolean decodeSlash = feignClientProperties == null || feignClientProperties.isDecodeSlash(); + return new SpringMvcContract(this.parameterProcessors, feignConversionService, decodeSlash); } @Bean diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringMvcContract.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringMvcContract.java index b88e0122..1c6b7249 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringMvcContract.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringMvcContract.java @@ -78,6 +78,7 @@ import static org.springframework.core.annotation.AnnotatedElementUtils.findMerg * @author Aaron Whiteside * @author Artyom Romanenko * @author Darren Foong + * @author Ram Anaswara */ public class SpringMvcContract extends Contract.BaseContract implements ResourceLoaderAware { @@ -101,6 +102,8 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource private ResourceLoader resourceLoader = new DefaultResourceLoader(); + private boolean decodeSlash; + public SpringMvcContract() { this(Collections.emptyList()); } @@ -111,6 +114,11 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource public SpringMvcContract(List annotatedParameterProcessors, ConversionService conversionService) { + this(annotatedParameterProcessors, conversionService, true); + } + + public SpringMvcContract(List annotatedParameterProcessors, + ConversionService conversionService, boolean decodeSlash) { Assert.notNull(annotatedParameterProcessors, "Parameter processors can not be null."); Assert.notNull(conversionService, "ConversionService can not be null."); @@ -120,6 +128,7 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource annotatedArgumentProcessors = toAnnotatedArgumentProcessorMap(processors); this.conversionService = conversionService; convertingExpanderFactory = new ConvertingExpanderFactory(conversionService); + this.decodeSlash = decodeSlash; } private static TypeDescriptor createTypeDescriptor(Method method, int paramIndex) { @@ -171,6 +180,9 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource pathValue = "/" + pathValue; } data.template().uri(pathValue); + if (data.template().decodeSlash() != decodeSlash) { + data.template().decodeSlash(decodeSlash); + } } } } @@ -232,6 +244,9 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource pathValue = "/" + pathValue; } data.template().uri(pathValue, true); + if (data.template().decodeSlash() != decodeSlash) { + data.template().decodeSlash(decodeSlash); + } } } diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/AbstractSpringMvcContractIntegrationTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/AbstractSpringMvcContractIntegrationTests.java new file mode 100644 index 00000000..03e3e2a5 --- /dev/null +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/AbstractSpringMvcContractIntegrationTests.java @@ -0,0 +1,112 @@ +/* + * Copyright 2013-2020 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.cloud.openfeign.support; + +import java.nio.charset.Charset; + +import feign.Response; +import feign.codec.Decoder; +import feign.codec.Encoder; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.cloud.openfeign.test.NoSecurityConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpHeaders; +import org.springframework.util.SocketUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * Abstract class for the integration tests for {@link SpringMvcContract}. + * + * @author Ram Anaswara + */ +public class AbstractSpringMvcContractIntegrationTests { + + @BeforeAll + public static void beforeClass() { + System.setProperty("server.port", String.valueOf(SocketUtils.findAvailableTcpPort())); + } + + @AfterAll + public static void afterClass() { + System.clearProperty("server.port"); + } + + protected String getUrlQueryParam(Response response) { + return response.request().requestTemplate().queries().get("url").stream().findFirst() + .orElseThrow(IllegalStateException::new); + } + + @FeignClient(name = "test", url = "http://localhost:${server.port}/", + configuration = NoCodecsFeignConfiguration.class) + interface TestClient { + + @PostMapping("/test") + Object sendMessage(@RequestBody String message, @RequestHeader(HttpHeaders.CONTENT_TYPE) String acceptHeader); + + @GetMapping("/get") + Object getMessage(@RequestParam String url); + + } + + @Configuration(proxyBeanMethods = false) + @EnableFeignClients(clients = TestClient.class) + @EnableAutoConfiguration + @RestController + @Import(NoSecurityConfiguration.class) + protected static class Config { + + @PostMapping("/test") + Object sendMessage(@RequestBody String message, @RequestHeader(HttpHeaders.CONTENT_TYPE) String acceptHeader) { + return message; + } + + @GetMapping("/get") + Object getMessage(@RequestParam String url) { + return url; + } + + } + + // Avoid feign.codec.EncodeException - this feature works for users that override + // Encoder + protected static class NoCodecsFeignConfiguration { + + @Bean + public Decoder decoder() { + return (response, type) -> response; + } + + @Bean + public Encoder encoder() { + return (object, bodyType, request) -> request.body(object.toString().getBytes(), Charset.defaultCharset()); + } + + } + +} diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SpringMvcContractIntegrationTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SpringMvcContractIntegrationTests.java index 9d322c48..ff28f41e 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SpringMvcContractIntegrationTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SpringMvcContractIntegrationTests.java @@ -16,96 +16,39 @@ package org.springframework.cloud.openfeign.support; -import java.nio.charset.Charset; - -import feign.codec.Decoder; -import feign.codec.Encoder; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; +import feign.Response; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.cloud.openfeign.EnableFeignClients; -import org.springframework.cloud.openfeign.FeignClient; -import org.springframework.cloud.openfeign.test.NoSecurityConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.http.HttpHeaders; -import org.springframework.util.SocketUtils; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestHeader; -import org.springframework.web.bind.annotation.RestController; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; /** - * Integration tests for {@link SpringMvcContract} + * Integration tests for {@link SpringMvcContract}. * * @author Olga Maciaszek-Sharma + * @author Ram Anaswara */ -@SpringBootTest(classes = SpringMvcContractIntegrationTests.Config.class, +@SpringBootTest(classes = AbstractSpringMvcContractIntegrationTests.Config.class, webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) -public class SpringMvcContractIntegrationTests { +public class SpringMvcContractIntegrationTests extends AbstractSpringMvcContractIntegrationTests { @Autowired private TestClient client; - @BeforeAll - public static void beforeClass() { - System.setProperty("server.port", String.valueOf(SocketUtils.findAvailableTcpPort())); - } - - @AfterAll - public static void afterClass() { - System.clearProperty("server.port"); - } - @Test public void shouldNotThrowInvalidMediaTypeExceptionWhenContentTypeTemplateUsed() { assertThatCode(() -> client.sendMessage("test", "text/markdown")).doesNotThrowAnyException(); } - @FeignClient(name = "test", url = "http://localhost:${server.port}/", - configuration = NoCodecsFeignConfiguration.class) - interface TestClient { - - @PostMapping("/test") - Object sendMessage(@RequestBody String message, @RequestHeader(HttpHeaders.CONTENT_TYPE) String acceptHeader); - - } - - @Configuration(proxyBeanMethods = false) - @EnableFeignClients(clients = TestClient.class) - @EnableAutoConfiguration - @RestController - @Import(NoSecurityConfiguration.class) - protected static class Config { - - @PostMapping("/test") - Object sendMessage(@RequestBody String message, @RequestHeader(HttpHeaders.CONTENT_TYPE) String acceptHeader) { - return message; - } - - } - - // avoid feign.codec.EncodeException - this feature works for users that override - // Encoder - protected static class NoCodecsFeignConfiguration { - - @Bean - public Decoder decoder() { - return (response, type) -> response; - } - - @Bean - public Encoder encoder() { - return (object, bodyType, request) -> request.body(object.toString().getBytes(), Charset.defaultCharset()); - } + @Test + public void feignClientShouldPreserveSlash() { + Response response = (Response) client.getMessage("https://www.google.com"); + String urlQueryParam = getUrlQueryParam(response); + assertThat(urlQueryParam).isEqualTo("https%3A//www.google.com"); } } diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SpringMvcContractSlashEncodingIntegrationTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SpringMvcContractSlashEncodingIntegrationTests.java new file mode 100644 index 00000000..67b1cff4 --- /dev/null +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SpringMvcContractSlashEncodingIntegrationTests.java @@ -0,0 +1,47 @@ +/* + * Copyright 2013-2020 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.cloud.openfeign.support; + +import feign.Response; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link SpringMvcContract}. + * + * @author Ram Anaswara + */ +@SpringBootTest(classes = SpringMvcContractSlashEncodingIntegrationTests.Config.class, + webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, properties = { "feign.client.decodeSlash=false" }) +public class SpringMvcContractSlashEncodingIntegrationTests extends AbstractSpringMvcContractIntegrationTests { + + @Autowired + private TestClient client; + + @Test + public void feignClientShouldNotDecodeEncodedSlash() { + Response response = (Response) client.getMessage("https://www.google.com"); + + String urlQueryParam = getUrlQueryParam(response); + assertThat(urlQueryParam).isEqualTo("https%3A%2F%2Fwww.google.com"); + } + +} diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SpringMvcContractTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SpringMvcContractTests.java index 7dcf7078..738333f6 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SpringMvcContractTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SpringMvcContractTests.java @@ -114,11 +114,7 @@ public class SpringMvcContractTests { @Before public void setup() { - FormattingConversionServiceFactoryBean conversionServiceFactoryBean = new FormattingConversionServiceFactoryBean(); - conversionServiceFactoryBean.afterPropertiesSet(); - ConversionService conversionService = conversionServiceFactoryBean.getObject(); - - contract = new SpringMvcContract(Collections.emptyList(), conversionService); + contract = new SpringMvcContract(Collections.emptyList(), getConversionService()); } @Test @@ -130,6 +126,19 @@ public class SpringMvcContractTests { assertThat(data.template().method()).isEqualTo("GET"); assertThat(data.template().headers().get("Accept").iterator().next()) .isEqualTo(MediaType.APPLICATION_JSON_VALUE); + assertThat(data.template().decodeSlash()).isTrue(); + } + + @Test + public void testProcessAnnotationOnMethod_Simple_SlashEncoded() throws Exception { + contract = new SpringMvcContract(Collections.emptyList(), getConversionService(), false); + + Method method = TestTemplate_Simple.class.getDeclaredMethod("getTest", String.class); + MethodMetadata data = contract.parseAndValidateMetadata(method.getDeclaringClass(), method); + + assertThat(data.template().url()).isEqualTo("/test/{id}"); + + assertThat(data.template().decodeSlash()).isFalse(); } @Test @@ -180,6 +189,19 @@ public class SpringMvcContractTests { assertThat(data.template().method()).isEqualTo("GET"); assertThat(data.indexToName().get(0).iterator().next()).isEqualTo("classId"); + assertThat(data.template().decodeSlash()).isTrue(); + } + + @Test + public void testProcessAnnotations_Class_AnnotationsGetAllTests_EncodeSlash() throws Exception { + contract = new SpringMvcContract(Collections.emptyList(), getConversionService(), false); + + Method method = TestTemplate_Class_Annotations.class.getDeclaredMethod("getAllTests", String.class); + MethodMetadata data = contract.parseAndValidateMetadata(method.getDeclaringClass(), method); + + assertThat(data.template().url()).isEqualTo("/prepend/{classId}"); + + assertThat(data.template().decodeSlash()).isFalse(); } @Test @@ -193,8 +215,25 @@ public class SpringMvcContractTests { assertThat(data.template().url()).isEqualTo(extendedData.template().url()); assertThat(data.template().method()).isEqualTo(extendedData.template().method()); - assertThat(data.indexToName().get(0).iterator().next()).isEqualTo(data.indexToName().get(0).iterator().next()); + assertThat(data.indexToName().get(0).iterator().next()).isEqualTo(data.indexToName().get(0).iterator().next()); + assertThat(data.template().decodeSlash()).isTrue(); + } + + @Test + public void testProcessAnnotations_ExtendedInterface_EncodeSlash() throws Exception { + contract = new SpringMvcContract(Collections.emptyList(), getConversionService(), false); + + Method extendedMethod = TestTemplate_Extended.class.getMethod("getAllTests", String.class); + MethodMetadata extendedData = contract.parseAndValidateMetadata(extendedMethod.getDeclaringClass(), + extendedMethod); + + Method method = TestTemplate_Class_Annotations.class.getDeclaredMethod("getAllTests", String.class); + MethodMetadata data = contract.parseAndValidateMetadata(method.getDeclaringClass(), method); + + assertThat(data.template().url()).isEqualTo(extendedData.template().url()); + assertThat(data.template().method()).isEqualTo(extendedData.template().method()); + assertThat(data.template().decodeSlash()).isFalse(); } @Test @@ -343,6 +382,19 @@ public class SpringMvcContractTests { assertThat(data.template().method()).isEqualTo("GET"); assertThat(data.template().headers().get("Accept").iterator().next()) .isEqualTo(MediaType.APPLICATION_JSON_VALUE); + assertThat(data.template().decodeSlash()).isTrue(); + } + + @Test + public void testProcessAnnotations_Advanced3_DecodeSlashFlagNotModified() throws Exception { + contract = new SpringMvcContract(Collections.emptyList(), getConversionService(), false); + + Method method = TestTemplate_Simple.class.getDeclaredMethod("getTest"); + MethodMetadata data = contract.parseAndValidateMetadata(method.getDeclaringClass(), method); + + assertThat(data.template().url()).isEqualTo("/"); + + assertThat(data.template().decodeSlash()).isTrue(); } @Test @@ -524,6 +576,12 @@ public class SpringMvcContractTests { assertThat(data.formParams()).contains("file", "id"); } + private ConversionService getConversionService() { + FormattingConversionServiceFactoryBean conversionServiceFactoryBean = new FormattingConversionServiceFactoryBean(); + conversionServiceFactoryBean.afterPropertiesSet(); + return conversionServiceFactoryBean.getObject(); + } + public interface TestTemplate_Simple { @RequestMapping(value = "/test/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)