From a63a514193983fa4ec8b6cc67c469576b3a7e0de Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 10 Jan 2022 13:15:05 +0000 Subject: [PATCH] Avoid duplicates in multipart reqs with overlapping parts and params Fixes gh-746 --- .../restdocs/cli/CliOperationRequest.java | 12 +++++++++- .../restdocs/cli/CurlRequestSnippet.java | 4 ++-- .../restdocs/cli/HttpieRequestSnippet.java | 4 ++-- .../restdocs/http/HttpRequestSnippet.java | 24 ++++++++++++------- .../restdocs/cli/CurlRequestSnippetTests.java | 13 +++++++++- .../cli/HttpieRequestSnippetTests.java | 14 ++++++++++- .../http/HttpRequestSnippetTests.java | 15 +++++++++++- 7 files changed, 69 insertions(+), 17 deletions(-) diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java index 254bf623..74f5979b 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CliOperationRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2019 the original author or authors. + * Copyright 2014-2022 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. @@ -24,6 +24,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.stream.Collectors; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -64,6 +65,15 @@ final class CliOperationRequest implements OperationRequest { return null; } + Parameters getNonPartParameters() { + Parameters parameters = getParameters(); + Parameters nonPartParameters = new Parameters(); + nonPartParameters.putAll(parameters); + Set partNames = getParts().stream().map(OperationRequestPart::getName).collect(Collectors.toSet()); + nonPartParameters.keySet().removeAll(partNames); + return nonPartParameters; + } + @Override public byte[] getContent() { return this.delegate.getContent(); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java index c0a7db3c..82b0e36f 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/CurlRequestSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2020 the original author or authors. + * Copyright 2014-2022 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. @@ -177,7 +177,7 @@ public class CurlRequestSnippet extends TemplatedSnippet { lines.add(String.format("-d '%s'", content)); } else if (!request.getParts().isEmpty()) { - for (Entry> entry : request.getParameters().entrySet()) { + for (Entry> entry : request.getNonPartParameters().entrySet()) { for (String value : entry.getValue()) { lines.add(String.format("-F '%s=%s'", entry.getKey(), value)); } diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java index 2617c80c..c2efd681 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/cli/HttpieRequestSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2020 the original author or authors. + * Copyright 2014-2022 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. @@ -192,7 +192,7 @@ public class HttpieRequestSnippet extends TemplatedSnippet { return; } if (!request.getParts().isEmpty()) { - writeContentUsingParameters(request.getParameters(), lines); + writeContentUsingParameters(request.getNonPartParameters(), lines); } else if (request.isPutOrPost()) { writeContentUsingParameters(request.getParameters().getUniqueParameters(request.getUri()), lines); diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java index 1fb83582..8f57ae5a 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/http/HttpRequestSnippet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2020 the original author or authors. + * Copyright 2014-2022 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. @@ -23,6 +23,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Set; +import java.util.stream.Collectors; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -151,16 +153,20 @@ public class HttpRequestSnippet extends TemplatedSnippet { private void writeParts(OperationRequest request, PrintWriter writer) { writer.println(); + Set partNames = request.getParts().stream().map(OperationRequestPart::getName) + .collect(Collectors.toSet()); for (Entry> parameter : request.getParameters().entrySet()) { - if (parameter.getValue().isEmpty()) { - writePartBoundary(writer); - writePart(parameter.getKey(), "", null, null, writer); - } - else { - for (String value : parameter.getValue()) { + if (!partNames.contains(parameter.getKey())) { + if (parameter.getValue().isEmpty()) { writePartBoundary(writer); - writePart(parameter.getKey(), value, null, null, writer); - writer.println(); + writePart(parameter.getKey(), "", null, null, writer); + } + else { + for (String value : parameter.getValue()) { + writePartBoundary(writer); + writePart(parameter.getKey(), value, null, null, writer); + writer.println(); + } } } } diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java index 779c0077..856be874 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/CurlRequestSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2020 the original author or authors. + * Copyright 2014-2022 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. @@ -299,6 +299,17 @@ public class CurlRequestSnippetTests extends AbstractSnippetTests { assertThat(this.generatedSnippets.curlRequest()).is(codeBlock("bash").withContent(expectedContent)); } + @Test + public void multipartPostWithOverlappingPartsAndParameters() throws IOException { + new CurlRequestSnippet(this.commandFormatter).document(this.operationBuilder.request("http://localhost/upload") + .method("POST").header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) + .part("image", new byte[0]).submittedFileName("documents/images/example.png").and() + .part("a", "apple".getBytes()).and().param("a", "apple").build()); + String expectedContent = "$ curl 'http://localhost/upload' -i -X POST -H " + + "'Content-Type: multipart/form-data' -F 'image=@documents/images/example.png' -F 'a=apple'"; + assertThat(this.generatedSnippets.curlRequest()).is(codeBlock("bash").withContent(expectedContent)); + } + @Test public void basicAuthCredentialsAreSuppliedUsingUserOption() throws IOException { new CurlRequestSnippet(this.commandFormatter).document(this.operationBuilder.request("http://localhost/foo") diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java index 4ef85cb7..09ae870e 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/cli/HttpieRequestSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2020 the original author or authors. + * Copyright 2014-2022 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. @@ -303,6 +303,18 @@ public class HttpieRequestSnippetTests extends AbstractSnippetTests { assertThat(this.generatedSnippets.httpieRequest()).is(codeBlock("bash").withContent(expectedContent)); } + @Test + public void multipartPostWithOverlappingPartsAndParameters() throws IOException { + new HttpieRequestSnippet(this.commandFormatter) + .document(this.operationBuilder.request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE) + .part("image", new byte[0]).submittedFileName("documents/images/example.png").and() + .part("a", "apple".getBytes()).and().param("a", "apple").build()); + String expectedContent = "$ http --form POST 'http://localhost/upload'" + + " 'image'@'documents/images/example.png' 'a'@<(echo 'apple')"; + assertThat(this.generatedSnippets.httpieRequest()).is(codeBlock("bash").withContent(expectedContent)); + } + @Test public void basicAuthCredentialsAreSuppliedUsingAuthOption() throws IOException { new HttpieRequestSnippet(this.commandFormatter).document(this.operationBuilder.request("http://localhost/foo") diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java index ae9ef1a4..cffc0eb9 100644 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/http/HttpRequestSnippetTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2020 the original author or authors. + * Copyright 2014-2022 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. @@ -263,6 +263,19 @@ public class HttpRequestSnippetTests extends AbstractSnippetTests { .header(HttpHeaders.HOST, "localhost").content(expectedContent)); } + @Test + public void multipartPostWithOverlappingPartsAndParameters() throws IOException { + new HttpRequestSnippet().document(this.operationBuilder.request("http://localhost/upload").method("POST") + .header(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE).param("a", "apple") + .part("a", "apple".getBytes()).and().part("image", "<< data >>".getBytes()).build()); + String paramPart = createPart(String.format("Content-Disposition: form-data; " + "name=a%n%napple"), false); + String filePart = createPart(String.format("Content-Disposition: form-data; " + "name=image%n%n<< data >>")); + String expectedContent = paramPart + filePart; + assertThat(this.generatedSnippets.httpRequest()).is(httpRequest(RequestMethod.POST, "/upload") + .header("Content-Type", "multipart/form-data; boundary=" + BOUNDARY) + .header(HttpHeaders.HOST, "localhost").content(expectedContent)); + } + @Test public void multipartPostWithParameterWithNoValue() throws IOException { new HttpRequestSnippet().document(this.operationBuilder.request("http://localhost/upload").method("POST")