diff --git a/spring-test/spring-test.gradle b/spring-test/spring-test.gradle index 99b051667b0..231ee2e405c 100644 --- a/spring-test/spring-test.gradle +++ b/spring-test/spring-test.gradle @@ -41,6 +41,7 @@ dependencies { optional("org.xmlunit:xmlunit-matchers") optional("org.skyscreamer:jsonassert") optional("com.jayway.jsonpath:json-path") + optional("commons-fileupload:commons-fileupload") optional("org.jetbrains.kotlin:kotlin-reflect") optional("org.jetbrains.kotlin:kotlin-stdlib") optional("io.projectreactor:reactor-test") @@ -74,7 +75,6 @@ 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 f26449fc110..e01663f77b3 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -19,6 +19,8 @@ package org.springframework.test.web.client.match; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.util.List; +import java.util.Map; import javax.xml.transform.Source; import javax.xml.transform.dom.DOMSource; @@ -26,16 +28,23 @@ import javax.xml.transform.dom.DOMSource; import org.hamcrest.Matcher; import org.w3c.dom.Node; +import org.springframework.core.io.Resource; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpInputMessage; import org.springframework.http.MediaType; import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.converter.FormHttpMessageConverter; import org.springframework.mock.http.client.MockClientHttpRequest; +import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.test.util.JsonExpectationsHelper; import org.springframework.test.util.XmlExpectationsHelper; import org.springframework.test.web.client.RequestMatcher; +import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.util.StreamUtils; +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; @@ -159,10 +168,69 @@ public class ContentRequestMatchers { } /** - * Access to request body matchers. Matches content type {@link MediaType#MULTIPART_FORM_DATA} + * Parse the body as multipart data and assert it contains exactly the + * values from the given {@code MultiValueMap}. Values may be of type: + * + *

Note: This method uses the Apache Commons File Upload + * library to parse the multipart data and it must be on the test classpath. + * @param expectedMap the expected multipart values + * @since 5.3 */ - public MultipartFormDataRequestMatchers multipart() { - return new MultipartFormDataRequestMatchers(); + public RequestMatcher multipartData(MultiValueMap expectedMap) { + return multipartData(expectedMap, true); + } + + /** + * Variant of {@link #multipartData(MultiValueMap)} that does the same but + * only for a subset of the actual values. + * @param expectedMap the expected multipart values + * @since 5.3 + */ + public RequestMatcher multipartDataContains(Map expectedMap) { + MultiValueMap map = new LinkedMultiValueMap<>(expectedMap.size()); + expectedMap.forEach(map::add); + return multipartData(map, false); + } + + @SuppressWarnings("ConstantConditions") + private RequestMatcher multipartData(MultiValueMap expectedMap, boolean assertSize) { + return request -> { + MultiValueMap actualMap = MultipartHelper.parse(request); + if (assertSize) { + assertEquals("Multipart request content: " + actualMap, expectedMap.size(), actualMap.size()); + } + for (Map.Entry> entry : expectedMap.entrySet()) { + String name = entry.getKey(); + List values = entry.getValue(); + assertTrue("No Multipart '" + name + "'", actualMap.get(name) != null); + assertTrue("Multipart value count " + values.size(), assertSize ? + values.size() == actualMap.get(name).size() : + values.size() <= actualMap.get(name).size()); + for (int i = 0; i < values.size(); i++) { + Object expected = values.get(i); + Object actual = actualMap.get(name).get(i); + if (expected instanceof Resource) { + expected = StreamUtils.copyToByteArray(((Resource) expected).getInputStream()); + } + if (expected instanceof byte[]) { + assertTrue("Multipart is not a file", actual instanceof MultipartFile); + assertEquals("Multipart content", expected, ((MultipartFile) actual).getBytes()); + } + else if (expected instanceof String) { + assertTrue("Multipart is not a String", actual instanceof String); + assertEquals("Multipart content", expected, (String) actual); + } + else { + throw new IllegalArgumentException( + "Unexpected multipart value type: " + expected.getClass()); + } + } + } + }; } /** @@ -268,4 +336,32 @@ public class ContentRequestMatchers { protected abstract void matchInternal(MockClientHttpRequest request) throws Exception; } + + private static class MultipartHelper { + + public static MultiValueMap parse(ClientHttpRequest request) { + MultipartHttpServletRequest servletRequest = adaptToMultipartRequest(request); + MultiValueMap result = new LinkedMultiValueMap<>(); + for (Map.Entry> entry : servletRequest.getMultiFileMap().entrySet()) { + for (MultipartFile value : entry.getValue()) { + result.add(entry.getKey(), value); + } + } + for (Map.Entry entry : servletRequest.getParameterMap().entrySet()) { + for (String value : entry.getValue()) { + result.add(entry.getKey(), value); + } + } + return result; + } + + private static MultipartHttpServletRequest adaptToMultipartRequest(ClientHttpRequest request) { + MockClientHttpRequest source = (MockClientHttpRequest) request; + MockHttpServletRequest target = new MockHttpServletRequest(); + target.setContent(source.getBodyAsBytes()); + source.getHeaders().forEach((name, values) -> values.forEach(v -> target.addHeader(name, v))); + return new CommonsMultipartResolver().resolveMultipart(target); + } + } + } 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 deleted file mode 100644 index 74beeac7775..00000000000 --- a/spring-test/src/main/java/org/springframework/test/web/client/match/MultipartFormDataRequestMatchers.java +++ /dev/null @@ -1,238 +0,0 @@ -/* - * 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... 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... 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 deleted file mode 100644 index cdeaaa29cec..00000000000 --- a/spring-test/src/test/java/org/springframework/test/web/client/match/MultipartFormDatRequestMatchersTests.java +++ /dev/null @@ -1,320 +0,0 @@ -/* - * 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(); - } - }); - } -} 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 new file mode 100644 index 00000000000..9f70ea77980 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/client/match/MultipartRequestMatchersTests.java @@ -0,0 +1,190 @@ +/* + * Copyright 2002-2020 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.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +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 org.springframework.web.multipart.MultipartFile; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Unit tests for + * {@link ContentRequestMatchers#multipartData(MultiValueMap)} and. + * {@link ContentRequestMatchers#multipartDataContains(Map)}. + * + * @author Valentin Spac + * @author Rossen Stoyanchev + */ +public class MultipartRequestMatchersTests { + + private final MockClientHttpRequest request = new MockClientHttpRequest(); + + private final MultiValueMap input = new LinkedMultiValueMap<>(); + + private final MultiValueMap expected = new LinkedMultiValueMap<>(); + + + @BeforeEach + public void setUp() { + this.request.getHeaders().setContentType(MediaType.MULTIPART_FORM_DATA); + } + + + @Test + public void testContains() throws Exception { + this.input.add("foo", "bar"); + this.input.add("foo", "baz"); + this.input.add("lorem", "ipsum"); + + this.expected.add("foo", "bar"); + + writeAndAssertContains(); + } + + @Test + public void testDoesNotContain() { + this.input.add("foo", "bar"); + this.input.add("foo", "baz"); + this.input.add("lorem", "ipsum"); + + this.expected.add("foo", "wrongValue"); + + assertThatExceptionOfType(AssertionError.class).isThrownBy(this::writeAndAssert); + } + + @Test + public void testParamsMatch() throws Exception { + this.input.add("foo", "value 1"); + this.input.add("bar", "value A"); + this.input.add("baz", "value B"); + + this.expected.addAll(this.input); + + writeAndAssert(); + } + + @Test + public void testResourceMatch() throws Exception { + MultipartFile f1 = new MockMultipartFile("f1", "foo.txt", "text/plain", "Foo Lorem ipsum".getBytes()); + MultipartFile f2 = new MockMultipartFile("f2", "bar.txt", "text/plain", "Bar Lorem ipsum".getBytes()); + MultipartFile f3 = new MockMultipartFile("f3", "foobar.txt", "text/plain", "Foobar Lorem ipsum".getBytes()); + + this.input.add("fooParam", "foo value"); + this.input.add("barParam", "bar value"); + this.input.add(f1.getName(), f1.getResource()); + this.input.add(f2.getName(), f2.getResource()); + this.input.add(f3.getName(), f3.getResource()); + + this.expected.addAll(this.input); + + writeAndAssert(); + } + + @Test + public void testResourceNoMatch() { + MockMultipartFile foo = new MockMultipartFile("f1", "foo.txt", "text/plain", "Foo Lorem ipsum".getBytes()); + MockMultipartFile bar = new MockMultipartFile("f2", "bar.txt", "text/plain", "Bar Lorem ipsum".getBytes()); + + this.input.add("fooParam", "foo value"); + this.input.add("barParam", "bar value"); + this.input.add(foo.getName(), foo.getResource()); + this.input.add(bar.getName(), bar.getResource()); + + this.expected.addAll(this.input); + this.expected.set(foo.getName(), bar.getResource()); + + assertThatExceptionOfType(AssertionError.class).isThrownBy(this::writeAndAssert); + } + + @Test + public void testByteArrayMatch() throws Exception { + MultipartFile f1 = new MockMultipartFile("f1", "foo.txt", "text/plain", "Foo Lorem ipsum".getBytes()); + MultipartFile f2 = new MockMultipartFile("f2", "bar.txt", "text/plain", "Bar Lorem ipsum".getBytes()); + MultipartFile f3 = new MockMultipartFile("f3", "foobar.txt", "text/plain", "Foobar Lorem ipsum".getBytes()); + + this.input.add("fooParam", "foo value"); + this.input.add("barParam", "bar value"); + this.input.add(f1.getName(), f1.getResource()); + this.input.add(f2.getName(), f2.getResource()); + this.input.add(f3.getName(), f3.getResource()); + + this.expected.addAll(this.input); + this.expected.set(f1.getName(), f1.getBytes()); + this.expected.set(f2.getName(), f2.getBytes()); + this.expected.set(f3.getName(), f3.getBytes()); + + writeAndAssert(); + } + + @Test + public void testByteArrayNoMatch() throws Exception { + MultipartFile f1 = new MockMultipartFile("f1", "foo.txt", "text/plain", "Foo Lorem ipsum".getBytes()); + MultipartFile f2 = new MockMultipartFile("f2", "bar.txt", "text/plain", "Bar Lorem ipsum".getBytes()); + + this.input.add("fooParam", "foo value"); + this.input.add("barParam", "bar value"); + this.input.add(f1.getName(), f1.getResource()); + this.input.add(f2.getName(), f2.getResource()); + + this.expected.addAll(this.input); + this.expected.set(f1.getName(), f2.getBytes()); + + assertThatExceptionOfType(AssertionError.class).isThrownBy(this::writeAndAssert); + } + + + private void writeAndAssert() throws IOException { + writeForm(); + new ContentRequestMatchers().multipartData(this.expected).match(request); + } + + private void writeAndAssertContains() throws IOException { + writeForm(); + Map expectedMap = this.expected.toSingleValueMap(); + new ContentRequestMatchers().multipartDataContains(expectedMap).match(request); + } + + private void writeForm() throws IOException { + new FormHttpMessageConverter().write(this.input, MediaType.MULTIPART_FORM_DATA, + new HttpOutputMessage() { + @Override + public OutputStream getBody() throws IOException { + return MultipartRequestMatchersTests.this.request.getBody(); + } + + @Override + public HttpHeaders getHeaders() { + return MultipartRequestMatchersTests.this.request.getHeaders(); + } + }); + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/multipart/commons/CommonsMultipartFile.java b/spring-web/src/main/java/org/springframework/web/multipart/commons/CommonsMultipartFile.java index 54298a0a882..691b52519e2 100644 --- a/spring-web/src/main/java/org/springframework/web/multipart/commons/CommonsMultipartFile.java +++ b/spring-web/src/main/java/org/springframework/web/multipart/commons/CommonsMultipartFile.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 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. @@ -232,4 +232,11 @@ public class CommonsMultipartFile implements MultipartFile, Serializable { } } + @Override + public String toString() { + return "MultipartFile[field=\"" + this.fileItem.getFieldName() + "\"" + + (this.fileItem.getName() != null ? ", filename=" + this.fileItem.getName() : "" ) + + (this.fileItem.getContentType() != null ? ", contentType=" + this.fileItem.getContentType() : "") + + ", size=" + this.fileItem.getSize() + "]"; + } }