diff --git a/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java b/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java index e975586ceb1..58dbbfb4b57 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java @@ -36,6 +36,7 @@ import org.jspecify.annotations.Nullable; import org.w3c.dom.Node; import org.springframework.core.io.Resource; +import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.client.ClientHttpRequest; @@ -205,6 +206,7 @@ public class ContentRequestMatchers { *
  • {@code String} - form field *
  • {@link Resource} - content from a file *
  • {@code byte[]} - other raw content + *
  • {@link HttpEntity} - entity with a Resource or byte[], and a Content-Type header * *

    Note: This method uses the fork of Commons FileUpload library * packaged with Apache Tomcat in the {@code org.apache.tomcat.util.http.fileupload} @@ -239,7 +241,7 @@ public class ContentRequestMatchers { return multipartData(map, DEFAULT_MULTIPART_ENCODING, false); } - @SuppressWarnings("ConstantConditions") + @SuppressWarnings({"ConstantConditions", "unchecked"}) private RequestMatcher multipartData( MultiValueMap expectedMap, Charset defaultCharset, boolean containsExactly) { @@ -256,27 +258,45 @@ public class ContentRequestMatchers { values.size() == actualMap.get(name).size() : values.size() <= actualMap.get(name).size()); for (int i = 0; i < values.size(); i++) { + MediaType expectedContentType = null; Object expected = values.get(i); Object actual = actualMap.get(name).get(i); + + if (expected instanceof HttpEntity entity) { + expectedContentType = entity.getHeaders().getContentType(); + expected = entity.getBody(); + } if (expected instanceof Resource resource) { expected = StreamUtils.copyToByteArray(resource.getInputStream()); } + if (expected instanceof byte[]) { - assertTrue("Multipart is not a file", actual instanceof byte[]); - assertEquals("Multipart content", expected, actual); + assertEquals("Multipart content", expected, asHttpEntity(actual).getBody()); } else if (expected instanceof String) { assertTrue("Multipart is not a String", actual instanceof String); assertEquals("Multipart content", expected, actual); } else { - throw new IllegalArgumentException("Unexpected multipart value: " + expected.getClass()); + throw new IllegalArgumentException("Unexpected multipart value: " + + (expected != null ? expected.getClass() : null)); + } + + if (expectedContentType != null) { + assertEquals("Multipart content-type", + expectedContentType, asHttpEntity(actual).getHeaders().getContentType()); } } } }; } + @SuppressWarnings("unchecked") + private static HttpEntity asHttpEntity(Object actual) { + assertTrue("Multipart is not an HttpEntity", actual instanceof HttpEntity); + return (HttpEntity) actual; + } + /** * Parse the request body and the given String as XML and assert that the * two are "similar" - i.e. they contain the same elements and attributes @@ -444,7 +464,7 @@ public class ContentRequestMatchers { MultiValueMap result = new LinkedMultiValueMap<>(); for (FileItem fileItem : fileItems) { result.add(fileItem.getFieldName(), - (fileItem.isFormField() ? fileItem.getString() : fileItem.get())); + (fileItem.isFormField() ? fileItem.getString() : toHttpEntity(fileItem))); } return result; } @@ -452,6 +472,12 @@ public class ContentRequestMatchers { throw new IllegalStateException("Failed to parse multipart request", ex); } } + + private static HttpEntity toHttpEntity(FileItem fileItem) { + HttpHeaders headers = new HttpHeaders(); + headers.set(HttpHeaders.CONTENT_TYPE, fileItem.getContentType()); + return new HttpEntity<>(fileItem.get(), headers); + } } } diff --git a/spring-test/src/test/java/org/springframework/test/web/client/match/MultipartRequestMatchersTests.java b/spring-test/src/test/java/org/springframework/test/web/client/match/MultipartRequestMatchersTests.java index c105ef21eab..0375a5086d9 100644 --- a/spring-test/src/test/java/org/springframework/test/web/client/match/MultipartRequestMatchersTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/client/match/MultipartRequestMatchersTests.java @@ -23,6 +23,7 @@ import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpOutputMessage; import org.springframework.http.MediaType; @@ -160,6 +161,20 @@ public class MultipartRequestMatchersTests { assertThatExceptionOfType(AssertionError.class).isThrownBy(this::writeAndAssert); } + @Test // gh-36154 + public void testHttpEntityMatch() throws Exception { + String contentType = "text/plain"; + MultipartFile file = new MockMultipartFile("file", "foo.txt", contentType, "Foo Lorem ipsum".getBytes()); + + HttpHeaders headers = new HttpHeaders(); + headers.set(HttpHeaders.CONTENT_TYPE, contentType); + this.input.add("foo.txt", new HttpEntity<>(file.getResource(), headers)); + + this.expected.addAll(this.input); + + writeAndAssert(); + } + private void writeAndAssert() throws IOException { writeForm();