diff --git a/spring-test/spring-test.gradle b/spring-test/spring-test.gradle
index ef413bc466c..99b051667b0 100644
--- a/spring-test/spring-test.gradle
+++ b/spring-test/spring-test.gradle
@@ -74,6 +74,7 @@ dependencies {
testCompile("org.hsqldb:hsqldb")
testCompile("org.apache.httpcomponents:httpclient")
testCompile("io.projectreactor.netty:reactor-netty")
+ testCompile("commons-fileupload:commons-fileupload")
testCompile("de.bechte.junit:junit-hierarchicalcontextrunner")
testRuntime("org.junit.vintage:junit-vintage-engine") {
exclude group: "junit", module: "junit"
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 a2486726849..f26449fc110 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
@@ -158,6 +158,13 @@ public class ContentRequestMatchers {
};
}
+ /**
+ * Access to request body matchers. Matches content type {@link MediaType#MULTIPART_FORM_DATA}
+ */
+ public MultipartFormDataRequestMatchers multipart() {
+ return new MultipartFormDataRequestMatchers();
+ }
+
/**
* 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
diff --git a/spring-test/src/main/java/org/springframework/test/web/client/match/MultipartFormDataRequestMatchers.java b/spring-test/src/main/java/org/springframework/test/web/client/match/MultipartFormDataRequestMatchers.java
new file mode 100644
index 00000000000..74beeac7775
--- /dev/null
+++ b/spring-test/src/main/java/org/springframework/test/web/client/match/MultipartFormDataRequestMatchers.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright 2002-2019 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
+ *
+ * https://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.test.web.client.match;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.apache.commons.io.IOUtils;
+import org.hamcrest.Matcher;
+import org.hamcrest.Matchers;
+import org.jetbrains.annotations.NotNull;
+
+import org.springframework.core.io.Resource;
+import org.springframework.http.MediaType;
+import org.springframework.http.client.ClientHttpRequest;
+import org.springframework.mock.http.client.MockClientHttpRequest;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.test.web.client.RequestMatcher;
+import org.springframework.util.Assert;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.multipart.MultipartFile;
+import org.springframework.web.multipart.MultipartHttpServletRequest;
+import org.springframework.web.multipart.commons.CommonsMultipartResolver;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.springframework.test.util.AssertionErrors.assertEquals;
+import static org.springframework.test.util.AssertionErrors.fail;
+
+/**
+ * Factory for assertions on the multipart form data parameters. Handles only {@link MediaType#MULTIPART_FORM_DATA}
+ *
+ *
An instance of this class is typically accessed via {@link ContentRequestMatchers#multipart()}
+ *
+ * @author Valentin Spac
+ * @since 5.3
+ */
+public class MultipartFormDataRequestMatchers {
+
+ public RequestMatcher param(String parameter, String... expectedValues) {
+ List> matcherList = Arrays.stream(expectedValues)
+ .map(Matchers::equalTo)
+ .collect(Collectors.toList());
+
+ return this.param(parameter, matcherList);
+ }
+
+ @SafeVarargs
+ @SuppressWarnings("varargs")
+ public final RequestMatcher param(String parameter, Matcher super String>... matchers) {
+ return this.param(parameter, Arrays.stream(matchers).collect(Collectors.toList()));
+ }
+
+ public RequestMatcher param(String parameter, Matcher> matchers) {
+ return request -> {
+ Map requestParams = MultipartRequestParser.parameterMap(request);
+ assertValueCount(parameter, requestParams, 1);
+
+ String[] values = requestParams.get(parameter);
+ assertThat("Request parameter [" + parameter + "]", Arrays.asList(values), matchers);
+ };
+ }
+
+ private RequestMatcher param(String parameter, List> matchers) {
+ return request -> {
+ Map requestParams = MultipartRequestParser.parameterMap(request);
+ assertValueCount(parameter, requestParams, matchers.size());
+
+ String[] values = requestParams.get(parameter);
+
+ Assert.state(values != null, "No values for request parameter " + parameter);
+ for (int i = 0; i < matchers.size(); i++) {
+ assertThat("Request parameter [" + parameter + "]", values[i], matchers.get(i));
+ }
+ };
+ }
+
+ public RequestMatcher params(MultiValueMap expectedParameters) {
+ return request -> {
+ Map requestParams = MultipartRequestParser.parameterMap(request);
+
+ expectedParameters.forEach((param, values) -> {
+ String[] actualValues = requestParams.get(param);
+ Assert.state(actualValues != null, "No values for request parameter " + param);
+
+ assertValueCount(param, requestParams, values.size());
+
+ assertEquals("Parameter " + param, values, Arrays.asList(actualValues));
+ });
+ };
+ }
+
+ public RequestMatcher file(String parameter, byte[]... resources) {
+ return request -> {
+ MultiValueMap files = MultipartRequestParser.multiFileMap(request);
+
+ assertValueCount(parameter, files, resources.length);
+
+ assertByteArrayMatch(parameter, Arrays.asList(resources), files.get(parameter));
+ };
+ }
+
+ @SafeVarargs
+ @SuppressWarnings("varargs")
+ public final RequestMatcher file(String parameter, Matcher super Resource>... matchers) {
+ return request -> {
+ MultiValueMap files = MultipartRequestParser.multiFileMap(request);
+ assertValueCount(parameter, files, matchers.length);
+ List parts = files.get(parameter);
+
+ for (int i = 0; i < matchers.length; i++) {
+ assertThat("File [" + parameter + "]", parts.get(i).getResource(), matchers[i]);
+ }
+ };
+ }
+
+ public RequestMatcher file(String parameter, Resource... resources) {
+ return request -> {
+ MultiValueMap files = MultipartRequestParser.multiFileMap(request);
+ assertValueCount(parameter, files, resources.length);
+
+ assertResourceMatch(parameter, Arrays.asList(resources), files.get(parameter));
+ };
+ }
+
+ public RequestMatcher files(MultiValueMap expectedFiles) {
+ return request -> {
+ MultiValueMap actualFiles = MultipartRequestParser.multiFileMap(request);
+
+ expectedFiles.forEach((param, parts) -> {
+ assertValueCount(param, actualFiles, parts.size());
+ assertResourceMatch(param, parts, actualFiles.get(param));
+ });
+ };
+ }
+
+
+ private void assertByteArrayMatch(String parameterName, List expectedFiles,
+ List actualFiles) {
+ for (int index = 0; index < actualFiles.size(); index++) {
+ MultipartFile multiPartFile = actualFiles.get(index);
+ byte[] expectedContent = expectedFiles.get(index);
+
+ try {
+ assertEquals("Content mismatch for file " + parameterName, expectedContent,
+ multiPartFile.getBytes());
+ }
+ catch (IOException ex) {
+ throw new AssertionError("Could not get bytes from actual multipart files", ex);
+ }
+ }
+ }
+
+ private void assertResourceMatch(String parameterName, List expectedFiles,
+ List actualFiles) {
+ for (int index = 0; index < actualFiles.size(); index++) {
+ MultipartFile multiPartFile = actualFiles.get(index);
+ Resource expectedResource = expectedFiles.get(index);
+ try {
+ byte[] fileContent = IOUtils.toByteArray(expectedResource.getInputStream());
+
+ assertEquals("Content mismatch for file " + parameterName, fileContent,
+ multiPartFile.getBytes());
+ assertEquals("Filename ", expectedResource.getFilename(), multiPartFile.getOriginalFilename());
+ }
+ catch (IOException ex) {
+ throw new AssertionError("Could not get bytes from actual multipart files", ex);
+ }
+ }
+ }
+
+ private static void assertValueCount(String parameter, Map map, int count) {
+ String[] values = map.get(parameter);
+ if (values == null) {
+ fail("Expected <" + parameter + "> to exist but was null");
+ }
+ assertValueCount(parameter, count, Arrays.asList(values));
+ }
+
+ private static void assertValueCount(String parameter, MultiValueMap map, int count) {
+ List> values = map.get(parameter);
+ assertValueCount(parameter, count, values);
+ }
+
+ private static void assertValueCount(String parameter, int count, List> values) {
+ String message = "Expected multipart file <" + parameter + ">";
+ if (count > values.size()) {
+ fail(message + " to have at least <" + count + "> values but found " + values.size());
+ }
+ }
+
+
+ private static class MultipartRequestParser {
+ private static MultipartHttpServletRequest extract(ClientHttpRequest request) {
+ MockClientHttpRequest mockRequest = (MockClientHttpRequest) request;
+ final MockHttpServletRequest mockHttpServletRequest = toMockHttpServletRequest(mockRequest);
+
+ CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
+ return multipartResolver.resolveMultipart(mockHttpServletRequest);
+ }
+
+ @NotNull
+ private static MockHttpServletRequest toMockHttpServletRequest(MockClientHttpRequest mockRequest) {
+ final MockHttpServletRequest mockHttpServletRequest = new MockHttpServletRequest();
+ mockHttpServletRequest.setContent(mockRequest.getBodyAsBytes());
+
+ // copy headers
+ mockRequest.getHeaders()
+ .forEach((headerName, headerValue) ->
+ headerValue.forEach(value -> mockHttpServletRequest.addHeader(headerName, value)));
+ return mockHttpServletRequest;
+ }
+
+ private static Map parameterMap(ClientHttpRequest request) {
+ return extract(request).getParameterMap();
+ }
+
+ private static MultiValueMap multiFileMap(ClientHttpRequest request) {
+ return extract(request).getMultiFileMap();
+ }
+ }
+}
diff --git a/spring-test/src/test/java/org/springframework/test/web/client/match/MultipartFormDatRequestMatchersTests.java b/spring-test/src/test/java/org/springframework/test/web/client/match/MultipartFormDatRequestMatchersTests.java
new file mode 100644
index 00000000000..cdeaaa29cec
--- /dev/null
+++ b/spring-test/src/test/java/org/springframework/test/web/client/match/MultipartFormDatRequestMatchersTests.java
@@ -0,0 +1,320 @@
+/*
+ * Copyright 2002-2019 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
+ *
+ * https://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.test.web.client.match;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Arrays;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.core.io.Resource;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpOutputMessage;
+import org.springframework.http.MediaType;
+import org.springframework.http.converter.FormHttpMessageConverter;
+import org.springframework.mock.http.client.MockClientHttpRequest;
+import org.springframework.mock.web.MockMultipartFile;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.equalTo;
+
+/**
+ * Unit tests for {@link MultipartFormDataRequestMatchers}.
+ *
+ * @author Valentin Spac
+ */
+public class MultipartFormDatRequestMatchersTests {
+
+ private MockClientHttpRequest request = new MockClientHttpRequest();
+ private MultipartFormDataRequestMatchers multipartRequestMatchers = MockRestRequestMatchers.content().multipart();
+
+ @BeforeEach
+ public void setUp() {
+ this.request.getHeaders().setContentType(MediaType.MULTIPART_FORM_DATA);
+ }
+
+ @Test
+ public void testContains() throws Exception {
+ MultiValueMap payload = new LinkedMultiValueMap<>();
+ payload.add("foo", "bar");
+ payload.add("foo", "baz");
+ payload.add("lorem", "ipsum");
+
+ writeForm(payload);
+
+ multipartRequestMatchers.param("foo", containsInAnyOrder("bar", "baz")).match(request);
+ }
+
+ @Test
+ public void testNoContains() throws Exception {
+ MultiValueMap payload = new LinkedMultiValueMap<>();
+ payload.add("foo", "bar");
+ payload.add("foo", "baz");
+ payload.add("lorem", "ipsum");
+
+ writeForm(payload);
+
+ assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
+ multipartRequestMatchers.param("foo", containsInAnyOrder("wrongValue")).match(request));
+ }
+
+ @Test
+ public void testEqualMatcher() throws Exception {
+ MultiValueMap payload = new LinkedMultiValueMap<>();
+ payload.add("foo", "bar");
+ payload.add("baz", "foobar");
+
+ writeForm(payload);
+ multipartRequestMatchers.param("foo", equalTo("bar")).match(request);
+ }
+
+ @Test
+ public void testNoEqualMatcher() throws Exception {
+ MultiValueMap payload = new LinkedMultiValueMap<>();
+ payload.add("foo", "bar");
+ payload.add("baz", "foobar");
+
+ writeForm(payload);
+
+ assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
+ multipartRequestMatchers.param("foo", equalTo("wrongValue")).match(request));
+ }
+
+ @Test
+ public void testParamMatch() throws Exception {
+ MultiValueMap payload = new LinkedMultiValueMap<>();
+ payload.add("foo", "bar");
+ payload.add("baz", "foobar");
+
+ writeForm(payload);
+ multipartRequestMatchers.param("foo", "bar").match(request);
+ }
+
+ @Test
+ public void testParamNoMatch() throws Exception {
+ MultiValueMap payload = new LinkedMultiValueMap<>();
+ payload.add("foo", "bar");
+ payload.add("baz", "foobar");
+
+ writeForm(payload);
+ assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
+ multipartRequestMatchers.param("foo", "wrongValue").match(request));
+ }
+
+ @Test
+ public void testParamsMultimapMatch() throws Exception {
+ MultiValueMap map = new LinkedMultiValueMap<>();
+ map.add("foo", "value 1");
+ map.add("bar", "value A");
+ map.add("baz", "value B");
+
+ writeForm(map);
+
+ multipartRequestMatchers.params(map).match(this.request);
+ }
+
+ @Test
+ public void testParamsMultimapNoMatch() throws Exception {
+ MultiValueMap map = new LinkedMultiValueMap<>();
+ map.add("foo", "foo value");
+ map.add("bar", "bar value");
+ map.add("baz", "baz value");
+ map.add("baz", "second baz value");
+
+ writeForm(map);
+
+ map.set("baz", "wrong baz value");
+ assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
+ multipartRequestMatchers.params(map).match(this.request));
+ }
+
+
+ @Test
+ public void testResourceMatch() throws Exception {
+ MockMultipartFile foo = new MockMultipartFile("fooFile", "foo.txt", "text/plain", "Foo Lorem ipsum".getBytes());
+ MockMultipartFile bar = new MockMultipartFile("fooFile", "bar.txt", "text/plain", "Bar Lorem ipsum".getBytes());
+ MockMultipartFile foobar = new MockMultipartFile("foobarFile", "foobar.txt", "text/plain", "Foobar Lorem ipsum".getBytes());
+
+ MultiValueMap map = new LinkedMultiValueMap<>();
+ map.add("fooParam", "foo value");
+ map.add("barParam", "bar value");
+ map.add(foo.getName(), foo.getResource());
+ map.add(bar.getName(), bar.getResource());
+ map.add(foobar.getName(), foobar.getResource());
+
+ writeForm(map);
+
+ multipartRequestMatchers.file(foo.getName(), foo.getResource(), bar.getResource()).match(this.request);
+ }
+
+ @Test
+ public void testResourceNoMatch() throws Exception {
+ MockMultipartFile foo = new MockMultipartFile("fooFile", "foo.txt", "text/plain", "Foo Lorem ipsum".getBytes());
+ MockMultipartFile bar = new MockMultipartFile("barFile", "bar.txt", "text/plain", "Bar Lorem ipsum".getBytes());
+
+ MultiValueMap map = new LinkedMultiValueMap<>();
+ map.add("fooParam", "foo value");
+ map.add("barParam", "bar value");
+ map.add(foo.getName(), foo.getResource());
+ map.add(bar.getName(), bar.getResource());
+
+ writeForm(map);
+
+
+ assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
+ multipartRequestMatchers.file(foo.getName(), foo.getResource(), bar.getResource()).match(this.request));
+ }
+
+ @Test
+ public void testByteArrayMatch() throws Exception {
+ MockMultipartFile foo = new MockMultipartFile("fooFile", "foo.txt", "text/plain", "Foo Lorem ipsum".getBytes());
+ MockMultipartFile bar = new MockMultipartFile("fooFile", "bar.txt", "text/plain", "Bar Lorem ipsum".getBytes());
+ MockMultipartFile foobar = new MockMultipartFile("foobarFile", "foobar.txt", "text/plain", "Foobar Lorem ipsum".getBytes());
+
+ MultiValueMap map = new LinkedMultiValueMap<>();
+ map.add("fooParam", "foo value");
+ map.add("barParam", "bar value");
+ map.add(foo.getName(), foo.getResource());
+ map.add(bar.getName(), bar.getResource());
+ map.add(foobar.getName(), foobar.getResource());
+
+ writeForm(map);
+
+ multipartRequestMatchers.file(foo.getName(), foo.getBytes(), bar.getBytes()).match(this.request);
+ }
+
+ @Test
+ public void testByteArrayNoMatch() throws Exception {
+ MockMultipartFile foo = new MockMultipartFile("fooFile", "foo.txt", "text/plain", "Foo Lorem ipsum".getBytes());
+ MockMultipartFile bar = new MockMultipartFile("barFile", "bar.txt", "text/plain", "Bar Lorem ipsum".getBytes());
+
+ MultiValueMap map = new LinkedMultiValueMap<>();
+ map.add("fooParam", "foo value");
+ map.add("barParam", "bar value");
+ map.add(foo.getName(), foo.getResource());
+ map.add(bar.getName(), bar.getResource());
+
+ writeForm(map);
+
+
+ assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
+ multipartRequestMatchers.file(foo.getName(), bar.getBytes()).match(this.request));
+ }
+
+ @Test
+ public void testResourceMatcher() throws Exception {
+ MockMultipartFile foo = new MockMultipartFile("fooFile", "foo.txt", "text/plain", "Foo Lorem ipsum".getBytes());
+ MockMultipartFile bar = new MockMultipartFile("barFile", "bar.txt", "text/plain", "Bar Lorem ipsum".getBytes());
+
+ MultiValueMap map = new LinkedMultiValueMap<>();
+ map.add("fooParam", "foo value");
+ map.add("barParam", "bar value");
+ map.add(foo.getName(), foo.getResource());
+ map.add(bar.getName(), bar.getResource());
+
+ writeForm(map);
+ multipartRequestMatchers.file(foo.getName(), resourceMatcher(foo.getResource())).match(this.request);
+ }
+
+ @Test
+ public void testResourceMatcherNoMatch() throws Exception {
+ MockMultipartFile foo = new MockMultipartFile("fooFile", "foo.txt", "text/plain", "Foo Lorem ipsum".getBytes());
+ MockMultipartFile bar = new MockMultipartFile("barFile", "bar.txt", "text/plain", "Bar Lorem ipsum".getBytes());
+
+ MultiValueMap map = new LinkedMultiValueMap<>();
+ map.add("fooParam", "foo value");
+ map.add("barParam", "bar value");
+ map.add(foo.getName(), foo.getResource());
+ map.add(bar.getName(), bar.getResource());
+
+ writeForm(map);
+ assertThatExceptionOfType(AssertionError.class).isThrownBy(() ->
+ multipartRequestMatchers.file(foo.getName(), resourceMatcher(bar.getResource())).match(this.request));
+ }
+
+ @NotNull
+ private Matcher resourceMatcher(Resource expectedResource) {
+ return new TypeSafeMatcher() {
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendValue(expectedResource.getDescription());
+ }
+
+ @Override
+ protected boolean matchesSafely(Resource resource) {
+ try {
+ byte[] actual = IOUtils.toByteArray(resource.getInputStream());
+ byte[] expected = IOUtils.toByteArray(expectedResource.getInputStream());
+
+ return StringUtils.equals(expectedResource.getFilename(), resource.getFilename())
+ && Arrays.equals(expected, actual);
+ }
+ catch (IOException e) {
+ throw new RuntimeException("Could not read resource content");
+ }
+ }
+ };
+ }
+
+ @Test
+ public void testResourceMultimapMatch() throws Exception {
+ MockMultipartFile foo = new MockMultipartFile("fooFile", "foo.txt", "text/plain", "Foo Lorem ipsum".getBytes());
+ MockMultipartFile bar = new MockMultipartFile("barFile", "bar.txt", "text/plain", "Bar Lorem ipsum".getBytes());
+
+ MultiValueMap map = new LinkedMultiValueMap<>();
+ map.add("fooParam", "foo value");
+ map.add("barParam", "bar value");
+ map.add(foo.getName(), foo.getResource());
+ map.add(bar.getName(), bar.getResource());
+
+ writeForm(map);
+
+ MultiValueMap files = new LinkedMultiValueMap<>();
+ files.add(foo.getName(), foo.getResource());
+ files.add(bar.getName(), bar.getResource());
+
+ multipartRequestMatchers.files(files).match(this.request);
+ }
+
+
+ private void writeForm(MultiValueMap payload) throws IOException {
+ FormHttpMessageConverter formHttpMessageConverter = new FormHttpMessageConverter();
+ formHttpMessageConverter.write(payload, MediaType.MULTIPART_FORM_DATA, new HttpOutputMessage() {
+ @Override
+ public OutputStream getBody() throws IOException {
+ return MultipartFormDatRequestMatchersTests.this.request.getBody();
+ }
+
+ @Override
+ public HttpHeaders getHeaders() {
+ return MultipartFormDatRequestMatchersTests.this.request.getHeaders();
+ }
+ });
+ }
+}