From 0d2c24ccc16ff6f2e69356dfa1639965c9633b57 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 28 Sep 2015 16:18:27 +0100 Subject: [PATCH] Improve curl request snippet's handling of query parameters Previously, if a request had no content and it was a POST or a PUT request, the curl request snippet would use the request's parameters as application/x-www-form-urlencoded data sent using the -d option. This resulted in the wrong snippet being produced if those parameters had actually come from the request's query string. This commit enhances CurlRequestSnippet so that, when it's using the request's parameter's to create its content, it only includes parameters that are not specified in the query string. With these changes in place, this MockMvc request: post("/?foo=bar").param("foo", "bar").param("a", "alpha") Will produce a curl snippet with the following command: $ curl 'http://localhost:8080/?foo=bar' -i -X POST -d 'a=alpha' foo=bar only appears in the command's query string, despite also being a general request parameter, and a=alpha only appears in the request's data. Closes gh-139 --- .../restdocs/curl/CurlRequestSnippet.java | 39 +++++++- .../restdocs/curl/QueryStringParser.java | 83 +++++++++++++++++ .../curl/CurlRequestSnippetTests.java | 41 ++++++++- .../restdocs/curl/QueryStringParserTests.java | 92 +++++++++++++++++++ ...kMvcRestDocumentationIntegrationTests.java | 19 ++++ 5 files changed, 270 insertions(+), 4 deletions(-) create mode 100644 spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/QueryStringParser.java create mode 100644 spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/QueryStringParserTests.java diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java index 73829f24..56f1fd4c 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/CurlRequestSnippet.java @@ -31,6 +31,7 @@ import org.springframework.http.HttpMethod; import org.springframework.restdocs.operation.Operation; import org.springframework.restdocs.operation.OperationRequest; import org.springframework.restdocs.operation.OperationRequestPart; +import org.springframework.restdocs.operation.Parameters; import org.springframework.restdocs.snippet.Snippet; import org.springframework.restdocs.snippet.TemplatedSnippet; import org.springframework.util.Base64Utils; @@ -165,9 +166,41 @@ public class CurlRequestSnippet extends TemplatedSnippet { } } else if (isPutOrPost(request)) { - String queryString = request.getParameters().toQueryString(); - if (StringUtils.hasText(queryString)) { - writer.print(String.format(" -d '%s'", queryString)); + writeContentUsingParameters(request, writer); + } + } + + private void writeContentUsingParameters(OperationRequest request, PrintWriter writer) { + Parameters uniqueParameters = getUniqueParameters(request); + String queryString = uniqueParameters.toQueryString(); + if (StringUtils.hasText(queryString)) { + writer.print(String.format(" -d '%s'", queryString)); + } + } + + private Parameters getUniqueParameters(OperationRequest request) { + Parameters queryStringParameters = new QueryStringParser() + .parse(request.getUri()); + Parameters uniqueParameters = new Parameters(); + + for (Entry> parameter : request.getParameters().entrySet()) { + addIfUnique(parameter, queryStringParameters, uniqueParameters); + } + return uniqueParameters; + } + + private void addIfUnique(Entry> parameter, + Parameters queryStringParameters, Parameters uniqueParameters) { + if (!queryStringParameters.containsKey(parameter.getKey())) { + uniqueParameters.put(parameter.getKey(), parameter.getValue()); + } + else { + List candidates = parameter.getValue(); + List existing = queryStringParameters.get(parameter.getKey()); + for (String candidate : candidates) { + if (!existing.contains(candidate)) { + uniqueParameters.add(parameter.getKey(), candidate); + } } } } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/QueryStringParser.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/QueryStringParser.java new file mode 100644 index 00000000..d09d6e44 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/curl/QueryStringParser.java @@ -0,0 +1,83 @@ +/* + * Copyright 2014-2015 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 + * + * http://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.restdocs.curl; + +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLDecoder; +import java.util.Scanner; + +import org.springframework.restdocs.operation.Parameters; + +/** + * A parser for the query string of a URI. + * + * @author Andy Wilkinson + */ +public class QueryStringParser { + + /** + * Parses the query string of the given {@code uri} and returns the resulting + * {@link Parameters}. + * + * @param uri the uri to parse + * @return the parameters parsed from the query string + */ + public Parameters parse(URI uri) { + String query = uri.getRawQuery(); + if (query != null) { + return parse(query); + } + return new Parameters(); + } + + private Parameters parse(String query) { + Parameters parameters = new Parameters(); + try (Scanner scanner = new Scanner(query)) { + scanner.useDelimiter("&"); + while (scanner.hasNext()) { + processParameter(scanner.next(), parameters); + } + } + return parameters; + } + + private void processParameter(String parameter, Parameters parameters) { + String[] components = parameter.split("="); + if (components.length == 2) { + String name = components[0]; + String value = components[1]; + parameters.add(decode(name), decode(value)); + } + else { + throw new IllegalArgumentException("The parameter '" + parameter + + "' is malformed"); + } + } + + private String decode(String encoded) { + try { + return URLDecoder.decode(encoded, "UTF-8"); + } + catch (UnsupportedEncodingException ex) { + throw new IllegalStateException("Unable to URL encode " + encoded + + " using UTF-8", ex); + } + + } + +} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java index f84cf0e1..21c1973d 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/CurlRequestSnippetTests.java @@ -84,7 +84,7 @@ public class CurlRequestSnippetTests { } @Test - public void requestWithQueryString() throws IOException { + public void getRequestWithQueryString() throws IOException { this.snippet.expectCurlRequest("request-with-query-string") .withContents( codeBlock("bash").content( @@ -94,6 +94,16 @@ public class CurlRequestSnippetTests { "http://localhost/foo?param=value").build()); } + @Test + public void postRequestWithQueryString() throws IOException { + this.snippet.expectCurlRequest("post-request-with-query-string").withContents( + codeBlock("bash").content( + "$ curl 'http://localhost/foo?param=value' -i -X POST")); + new CurlRequestSnippet().document(new OperationBuilder( + "post-request-with-query-string", this.snippet.getOutputDirectory()) + .request("http://localhost/foo?param=value").method("POST").build()); + } + @Test public void postRequestWithOneParameter() throws IOException { this.snippet.expectCurlRequest("post-request-with-one-parameter").withContents( @@ -132,6 +142,35 @@ public class CurlRequestSnippetTests { .method("POST").param("k1", "a&b").build()); } + @Test + public void postRequestWithQueryStringAndParameter() throws IOException { + this.snippet + .expectCurlRequest("post-request-with-query-string-and-parameter") + .withContents( + codeBlock("bash") + .content( + "$ curl 'http://localhost/foo?a=alpha' -i -X POST -d 'b=bravo'")); + new CurlRequestSnippet().document(new OperationBuilder( + "post-request-with-query-string-and-parameter", this.snippet + .getOutputDirectory()).request("http://localhost/foo?a=alpha") + .method("POST").param("b", "bravo").build()); + } + + @Test + public void postRequestWithOverlappingQueryStringAndParameters() throws IOException { + this.snippet + .expectCurlRequest( + "post-request-with-overlapping-query-string-and-parameters") + .withContents( + codeBlock("bash") + .content( + "$ curl 'http://localhost/foo?a=alpha' -i -X POST -d 'b=bravo'")); + new CurlRequestSnippet().document(new OperationBuilder( + "post-request-with-overlapping-query-string-and-parameters", this.snippet + .getOutputDirectory()).request("http://localhost/foo?a=alpha") + .method("POST").param("a", "alpha").param("b", "bravo").build()); + } + @Test public void putRequestWithOneParameter() throws IOException { this.snippet.expectCurlRequest("put-request-with-one-parameter").withContents( diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/QueryStringParserTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/QueryStringParserTests.java new file mode 100644 index 00000000..bfdc7010 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/curl/QueryStringParserTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2012-2015 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 + * + * http://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.restdocs.curl; + +import java.net.URI; +import java.util.Arrays; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.restdocs.operation.Parameters; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.Matchers.hasEntry; +import static org.junit.Assert.assertThat; + +/** + * Tests for {@link QueryStringParser}. + * + * @author Andy Wilkinson + */ +public class QueryStringParserTests { + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + private final QueryStringParser queryStringParser = new QueryStringParser(); + + @Test + public void noParameters() { + Parameters parameters = this.queryStringParser.parse(URI + .create("http://localhost")); + assertThat(parameters.size(), is(equalTo(0))); + } + + @Test + public void singleParameter() { + Parameters parameters = this.queryStringParser.parse(URI + .create("http://localhost?a=alpha")); + assertThat(parameters.size(), is(equalTo(1))); + assertThat(parameters, hasEntry("a", Arrays.asList("alpha"))); + } + + @Test + public void multipleParameters() { + Parameters parameters = this.queryStringParser.parse(URI + .create("http://localhost?a=alpha&b=bravo&c=charlie")); + assertThat(parameters.size(), is(equalTo(3))); + assertThat(parameters, hasEntry("a", Arrays.asList("alpha"))); + assertThat(parameters, hasEntry("b", Arrays.asList("bravo"))); + assertThat(parameters, hasEntry("c", Arrays.asList("charlie"))); + } + + @Test + public void multipleParametersWithSameKey() { + Parameters parameters = this.queryStringParser.parse(URI + .create("http://localhost?a=apple&a=avocado")); + assertThat(parameters.size(), is(equalTo(1))); + assertThat(parameters, hasEntry("a", Arrays.asList("apple", "avocado"))); + } + + @Test + public void encoded() { + Parameters parameters = this.queryStringParser.parse(URI + .create("http://localhost?a=al%26%3Dpha")); + assertThat(parameters.size(), is(equalTo(1))); + assertThat(parameters, hasEntry("a", Arrays.asList("al&=pha"))); + } + + @Test + public void malformedParameter() { + this.thrown.expect(IllegalArgumentException.class); + this.thrown + .expectMessage(equalTo("The parameter 'a=apple=avocado' is malformed")); + this.queryStringParser.parse(URI.create("http://localhost?a=apple=avocado")); + } +} diff --git a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java index a7422f94..cbf72544 100644 --- a/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java +++ b/spring-restdocs-mockmvc/src/test/java/org/springframework/restdocs/mockmvc/MockMvcRestDocumentationIntegrationTests.java @@ -136,6 +136,25 @@ public class MockMvcRestDocumentationIntegrationTests { + "-H 'Accept: application/json' -d 'content'")))); } + @Test + public void curlSnippetWithQueryStringOnPost() throws Exception { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context) + .apply(documentationConfiguration(this.restDocumentation)).build(); + + mockMvc.perform( + post("/?foo=bar").param("foo", "bar").param("a", "alpha") + .accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()) + .andDo(document("curl-snippet-with-query-string")); + assertThat( + new File( + "build/generated-snippets/curl-snippet-with-query-string/curl-request.adoc"), + is(snippet().withContents( + codeBlock("bash").content( + "$ curl " + + "'http://localhost:8080/?foo=bar' -i -X POST " + + "-H 'Accept: application/json' -d 'a=alpha'")))); + } + @Test public void linksSnippet() throws Exception { MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context)