diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/FormParameters.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/FormParameters.java index bc6510b24..96be1a759 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/FormParameters.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/FormParameters.java @@ -6,8 +6,6 @@ import java.io.UnsupportedEncodingException; import java.net.URLDecoder; -import java.util.LinkedList; -import java.util.List; import java.util.Scanner; import org.springframework.util.LinkedMultiValueMap; @@ -16,6 +14,7 @@ * A request's form parameters, derived from its form URL encoded body content. * * @author Andy Wilkinson + * @author Minju Kim * @since 3.0.0 */ public final class FormParameters extends LinkedMultiValueMap { @@ -54,20 +53,12 @@ private static FormParameters parse(String bodyContent) { } private static void processParameter(String parameter, FormParameters parameters) { - String[] components = parameter.split("="); - if (components.length > 0 && components.length < 3) { - if (components.length == 2) { - String name = components[0]; - String value = components[1]; - parameters.add(decode(name), decode(value)); - } - else { - List values = parameters.computeIfAbsent(components[0], (p) -> new LinkedList<>()); - values.add(""); - } + int separator = parameter.indexOf('='); + if (separator != -1) { + parameters.add(decode(parameter.substring(0, separator)), decode(parameter.substring(separator + 1))); } else { - throw new IllegalArgumentException("The parameter '" + parameter + "' is malformed"); + parameters.add(decode(parameter), ""); } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/QueryParameters.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/QueryParameters.java index aabe56ab6..4a02b1c67 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/QueryParameters.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/QueryParameters.java @@ -6,8 +6,6 @@ import java.net.URLDecoder; import java.nio.charset.StandardCharsets; -import java.util.LinkedList; -import java.util.List; import java.util.Scanner; import org.springframework.util.LinkedMultiValueMap; @@ -16,6 +14,7 @@ * A request's query parameters, derived from its URI's query string. * * @author Andy Wilkinson + * @author Minju Kim * @since 3.0.0 */ public final class QueryParameters extends LinkedMultiValueMap { @@ -54,20 +53,12 @@ private static QueryParameters parse(String query) { } private static void processParameter(String parameter, QueryParameters parameters) { - String[] components = parameter.split("="); - if (components.length > 0 && components.length < 3) { - if (components.length == 2) { - String name = components[0]; - String value = components[1]; - parameters.add(decode(name), decode(value)); - } - else { - List values = parameters.computeIfAbsent(components[0], (p) -> new LinkedList<>()); - values.add(""); - } + int separator = parameter.indexOf('='); + if (separator != -1) { + parameters.add(decode(parameter.substring(0, separator)), decode(parameter.substring(separator + 1))); } else { - throw new IllegalArgumentException("The parameter '" + parameter + "' is malformed"); + parameters.add(decode(parameter), ""); } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/FormParametersTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/FormParametersTests.java new file mode 100644 index 000000000..ece6feaea --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/FormParametersTests.java @@ -0,0 +1,96 @@ +/* + * Copyright 2014-present the original author or authors. + */ + +package org.springframework.restdocs.operation; + +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link FormParameters}. + * + * @author Minju Kim + */ +class FormParametersTests { + + @Test + void noContent() { + FormParameters parameters = formParameters(""); + assertThat(parameters).isEmpty(); + } + + @Test + void singleParameter() { + FormParameters parameters = formParameters("a=alpha"); + assertThat(parameters).containsOnlyKeys("a"); + assertThat(parameters.get("a")).containsExactly("alpha"); + } + + @Test + void multipleParameters() { + FormParameters parameters = formParameters("a=alpha&b=bravo&a=apple"); + assertThat(parameters).containsOnlyKeys("a", "b"); + assertThat(parameters.get("a")).containsExactly("alpha", "apple"); + assertThat(parameters.get("b")).containsExactly("bravo"); + } + + @Test + void parameterWithNoValue() { + FormParameters parameters = formParameters("a"); + assertThat(parameters).containsOnlyKeys("a"); + assertThat(parameters.get("a")).containsExactly(""); + } + + @Test + void parameterWithEmptyValue() { + FormParameters parameters = formParameters("a="); + assertThat(parameters).containsOnlyKeys("a"); + assertThat(parameters.get("a")).containsExactly(""); + } + + @Test + void encodedParameter() { + FormParameters parameters = formParameters("a+b=c%26d"); + assertThat(parameters).containsOnlyKeys("a b"); + assertThat(parameters.get("a b")).containsExactly("c&d"); + } + + @Test + void encodedParameterWithNoValue() { + FormParameters parameters = formParameters("a+b"); + assertThat(parameters).containsOnlyKeys("a b"); + assertThat(parameters.get("a b")).containsExactly(""); + } + + @Test + void parameterWithValueContainingEquals() { + FormParameters parameters = formParameters("filter=name==bob"); + assertThat(parameters).containsOnlyKeys("filter"); + assertThat(parameters.get("filter")).containsExactly("name==bob"); + } + + @Test + void parameterWithValueEndingWithEquals() { + FormParameters parameters = formParameters("cursor=YWJj=="); + assertThat(parameters).containsOnlyKeys("cursor"); + assertThat(parameters.get("cursor")).containsExactly("YWJj=="); + } + + private FormParameters formParameters(String content) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + return FormParameters.from(new OperationRequestFactory().create(URI.create("http://localhost"), HttpMethod.POST, + content.getBytes(StandardCharsets.UTF_8), headers, Collections.emptyList())); + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/QueryParametersTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/QueryParametersTests.java new file mode 100644 index 000000000..ceb2088b2 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/QueryParametersTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2014-present the original author or authors. + */ + +package org.springframework.restdocs.operation; + +import java.net.URI; +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link QueryParameters}. + * + * @author Minju Kim + */ +class QueryParametersTests { + + @Test + void noQueryString() { + QueryParameters parameters = queryParameters("http://localhost"); + assertThat(parameters).isEmpty(); + } + + @Test + void singleParameter() { + QueryParameters parameters = queryParameters("http://localhost?a=alpha"); + assertThat(parameters).containsOnlyKeys("a"); + assertThat(parameters.get("a")).containsExactly("alpha"); + } + + @Test + void multipleParameters() { + QueryParameters parameters = queryParameters("http://localhost?a=alpha&b=bravo&a=apple"); + assertThat(parameters).containsOnlyKeys("a", "b"); + assertThat(parameters.get("a")).containsExactly("alpha", "apple"); + assertThat(parameters.get("b")).containsExactly("bravo"); + } + + @Test + void parameterWithNoValue() { + QueryParameters parameters = queryParameters("http://localhost?a"); + assertThat(parameters).containsOnlyKeys("a"); + assertThat(parameters.get("a")).containsExactly(""); + } + + @Test + void parameterWithEmptyValue() { + QueryParameters parameters = queryParameters("http://localhost?a="); + assertThat(parameters).containsOnlyKeys("a"); + assertThat(parameters.get("a")).containsExactly(""); + } + + @Test + void encodedParameter() { + QueryParameters parameters = queryParameters("http://localhost?a%20b=c%26d"); + assertThat(parameters).containsOnlyKeys("a b"); + assertThat(parameters.get("a b")).containsExactly("c&d"); + } + + @Test + void encodedParameterWithNoValue() { + QueryParameters parameters = queryParameters("http://localhost?a%20b"); + assertThat(parameters).containsOnlyKeys("a b"); + assertThat(parameters.get("a b")).containsExactly(""); + } + + @Test + void parameterWithValueContainingEquals() { + QueryParameters parameters = queryParameters("http://localhost?filter=name==bob"); + assertThat(parameters).containsOnlyKeys("filter"); + assertThat(parameters.get("filter")).containsExactly("name==bob"); + } + + @Test + void parameterWithValueEndingWithEquals() { + QueryParameters parameters = queryParameters("http://localhost?cursor=YWJj=="); + assertThat(parameters).containsOnlyKeys("cursor"); + assertThat(parameters.get("cursor")).containsExactly("YWJj=="); + } + + private QueryParameters queryParameters(String uri) { + return QueryParameters.from(new OperationRequestFactory().create(URI.create(uri), HttpMethod.GET, new byte[0], + new HttpHeaders(), Collections.emptyList())); + } + +} diff --git a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java index b893c102b..7d935e7c5 100644 --- a/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java +++ b/spring-restdocs-mockmvc/src/main/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverter.java @@ -12,7 +12,6 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -46,6 +45,7 @@ * * @author Andy Wilkinson * @author Clyde Stubbs + * @author Minju Kim */ class MockMvcRequestConverter implements RequestConverter { @@ -254,20 +254,12 @@ private static MultiValueMap parse(@Nullable String query) { } private static void processParameter(String parameter, MultiValueMap parameters) { - String[] components = parameter.split("="); - if (components.length > 0 && components.length < 3) { - if (components.length == 2) { - String name = components[0]; - String value = components[1]; - parameters.add(decode(name), decode(value)); - } - else { - List values = parameters.computeIfAbsent(components[0], (p) -> new LinkedList<>()); - values.add(""); - } + int separator = parameter.indexOf('='); + if (separator != -1) { + parameters.add(decode(parameter.substring(0, separator)), decode(parameter.substring(separator + 1))); } else { - throw new IllegalArgumentException("The parameter '" + parameter + "' is malformed"); + parameters.add(decode(parameter), ""); } } diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java index 3ad4116d5..0632219af 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRequestConverterTests.java @@ -159,6 +159,24 @@ void postRequestWithNonAsciiParametersAndQueryStringCreatesFormUrlEncodedContent assertThat(request.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_FORM_URLENCODED); } + @Test + void postRequestWithQueryStringParameterWithValueContainingEquals() { + OperationRequest request = createOperationRequest( + MockMvcRequestBuilders.post(URI.create("/foo?filter=name==bob"))); + assertThat(request.getUri()).isEqualTo(URI.create("http://localhost/foo?filter=name==bob")); + assertThat(request.getMethod()).isEqualTo(HttpMethod.POST); + assertThat(request.getContentAsString()).isEmpty(); + } + + @Test + void postRequestWithQueryStringParameterWithValueEndingWithEqualsDoesNotDuplicateItInContent() { + OperationRequest request = createOperationRequest( + MockMvcRequestBuilders.post(URI.create("/foo?cursor=YWJj=="))); + assertThat(request.getUri()).isEqualTo(URI.create("http://localhost/foo?cursor=YWJj==")); + assertThat(request.getMethod()).isEqualTo(HttpMethod.POST); + assertThat(request.getContentAsString()).isEmpty(); + } + @Test void mockMultipartFileUpload() { OperationRequest request = createOperationRequest(MockMvcRequestBuilders.multipart("/foo")