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
This commit is contained in:
Andy Wilkinson
2015-09-28 16:18:27 +01:00
parent 535bea24f9
commit 0d2c24ccc1
5 changed files with 270 additions and 4 deletions

View File

@@ -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<String, List<String>> parameter : request.getParameters().entrySet()) {
addIfUnique(parameter, queryStringParameters, uniqueParameters);
}
return uniqueParameters;
}
private void addIfUnique(Entry<String, List<String>> parameter,
Parameters queryStringParameters, Parameters uniqueParameters) {
if (!queryStringParameters.containsKey(parameter.getKey())) {
uniqueParameters.put(parameter.getKey(), parameter.getValue());
}
else {
List<String> candidates = parameter.getValue();
List<String> existing = queryStringParameters.get(parameter.getKey());
for (String candidate : candidates) {
if (!existing.contains(candidate)) {
uniqueParameters.add(parameter.getKey(), candidate);
}
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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(

View File

@@ -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"));
}
}

View File

@@ -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)