diff --git a/spring-test/src/main/java/org/springframework/test/http/HttpMessageContentConverter.java b/spring-test/src/main/java/org/springframework/test/http/HttpMessageContentConverter.java new file mode 100644 index 000000000000..f8a80b009849 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/http/HttpMessageContentConverter.java @@ -0,0 +1,156 @@ +/* + * Copyright 2002-2024 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.http; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.List; +import java.util.stream.StreamSupport; + +import org.springframework.core.ResolvableType; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.converter.GenericHttpMessageConverter; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.http.converter.SmartHttpMessageConverter; +import org.springframework.mock.http.MockHttpInputMessage; +import org.springframework.mock.http.MockHttpOutputMessage; +import org.springframework.util.Assert; +import org.springframework.util.function.SingletonSupplier; + +/** + * Convert HTTP message content for testing purposes. + * + * @author Stephane Nicoll + * @since 6.2 + */ +public class HttpMessageContentConverter { + + private static final MediaType JSON = MediaType.APPLICATION_JSON; + + private final List> messageConverters; + + HttpMessageContentConverter(Iterable> messageConverters) { + this.messageConverters = StreamSupport.stream(messageConverters.spliterator(), false).toList(); + Assert.notEmpty(this.messageConverters, "At least one message converter needs to be specified"); + } + + + /** + * Create an instance with an iterable of the candidates to use. + * @param candidates the candidates + */ + public static HttpMessageContentConverter of(Iterable> candidates) { + return new HttpMessageContentConverter(candidates); + } + + /** + * Create an instance with a vararg of the candidates to use. + * @param candidates the candidates + */ + public static HttpMessageContentConverter of(HttpMessageConverter... candidates) { + return new HttpMessageContentConverter(Arrays.asList(candidates)); + } + + + /** + * Convert the given {@link HttpInputMessage} whose content must match the + * given {@link MediaType} to the requested {@code targetType}. + * @param message an input message + * @param mediaType the media type of the input + * @param targetType the target type + * @param the converted object type + * @return a value of the given {@code targetType} + */ + @SuppressWarnings("unchecked") + public T convert(HttpInputMessage message, MediaType mediaType, ResolvableType targetType) + throws IOException, HttpMessageNotReadableException { + Class contextClass = targetType.getRawClass(); + SingletonSupplier javaType = SingletonSupplier.of(targetType::getType); + for (HttpMessageConverter messageConverter : this.messageConverters) { + if (messageConverter instanceof GenericHttpMessageConverter genericMessageConverter) { + Type type = javaType.obtain(); + if (genericMessageConverter.canRead(type, contextClass, mediaType)) { + return (T) genericMessageConverter.read(type, contextClass, message); + } + } + else if (messageConverter instanceof SmartHttpMessageConverter smartMessageConverter) { + if (smartMessageConverter.canRead(targetType, mediaType)) { + return (T) smartMessageConverter.read(targetType, message, null); + } + } + else { + Class targetClass = (contextClass != null ? contextClass : Object.class); + if (messageConverter.canRead(targetClass, mediaType)) { + HttpMessageConverter simpleMessageConverter = (HttpMessageConverter) messageConverter; + Class clazz = (Class) targetClass; + return simpleMessageConverter.read(clazz, message); + } + } + } + throw new IllegalStateException("No converter found to read [%s] to [%s]".formatted(mediaType, targetType)); + } + + /** + * Convert the given raw value to the given {@code targetType} by writing + * it first to JSON and reading it back. + * @param value the value to convert + * @param targetType the target type + * @param the converted object type + * @return a value of the given {@code targetType} + */ + public T convertViaJson(Object value, ResolvableType targetType) throws IOException { + MockHttpOutputMessage outputMessage = convertToJson(value, ResolvableType.forInstance(value)); + return convert(fromHttpOutputMessage(outputMessage), JSON, targetType); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private MockHttpOutputMessage convertToJson(Object value, ResolvableType valueType) throws IOException { + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + Class valueClass = value.getClass(); + SingletonSupplier javaType = SingletonSupplier.of(valueType::getType); + for (HttpMessageConverter messageConverter : this.messageConverters) { + if (messageConverter instanceof GenericHttpMessageConverter genericMessageConverter) { + Type type = javaType.obtain(); + if (genericMessageConverter.canWrite(type, valueClass, JSON)) { + genericMessageConverter.write(value, type, JSON, outputMessage); + return outputMessage; + } + } + else if (messageConverter instanceof SmartHttpMessageConverter smartMessageConverter) { + if (smartMessageConverter.canWrite(valueType, valueClass, JSON)) { + smartMessageConverter.write(value, valueType, JSON, outputMessage, null); + return outputMessage; + } + } + else if (messageConverter.canWrite(valueClass, JSON)) { + ((HttpMessageConverter) messageConverter).write(value, JSON, outputMessage); + return outputMessage; + } + } + throw new IllegalStateException("No converter found to convert [%s] to JSON".formatted(valueType)); + } + + private static HttpInputMessage fromHttpOutputMessage(MockHttpOutputMessage message) { + MockHttpInputMessage inputMessage = new MockHttpInputMessage(message.getBodyAsBytes()); + inputMessage.getHeaders().addAll(message.getHeaders()); + return inputMessage; + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/json/AbstractJsonContentAssert.java b/spring-test/src/main/java/org/springframework/test/json/AbstractJsonContentAssert.java index 75f4feb25499..e034682b598a 100644 --- a/spring-test/src/main/java/org/springframework/test/json/AbstractJsonContentAssert.java +++ b/spring-test/src/main/java/org/springframework/test/json/AbstractJsonContentAssert.java @@ -35,6 +35,7 @@ import org.assertj.core.error.BasicErrorMessageFactory; import org.assertj.core.internal.Failures; +import org.springframework.core.ResolvableType; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.FileSystemResource; @@ -43,9 +44,9 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpInputMessage; import org.springframework.http.MediaType; -import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.lang.Nullable; import org.springframework.mock.http.MockHttpInputMessage; +import org.springframework.test.http.HttpMessageContentConverter; import org.springframework.util.Assert; /** @@ -77,7 +78,7 @@ public abstract class AbstractJsonContentAssert jsonMessageConverter; + private final HttpMessageContentConverter contentConverter; @Nullable private Class resourceLoadClass; @@ -94,7 +95,7 @@ public abstract class AbstractJsonContentAssert selfType) { super(actual, selfType); - this.jsonMessageConverter = (actual != null ? actual.getJsonMessageConverter() : null); + this.contentConverter = (actual != null ? actual.getContentConverter() : null); this.jsonLoader = new JsonLoader(null, null); as("JSON content"); } @@ -131,15 +132,15 @@ public AbstractObjectAssert convertTo(Class target) { return assertFactory.createAssert(this::convertToTargetType); } - @SuppressWarnings("unchecked") private T convertToTargetType(Type targetType) { String json = this.actual.getJson(); - if (this.jsonMessageConverter == null) { + if (this.contentConverter == null) { throw new IllegalStateException( "No JSON message converter available to convert %s".formatted(json)); } try { - return (T) this.jsonMessageConverter.read(targetType, getClass(), fromJson(json)); + return this.contentConverter.convert(fromJson(json), MediaType.APPLICATION_JSON, + ResolvableType.forType(targetType)); } catch (Exception ex) { throw failure(new ValueProcessingFailed(json, @@ -165,7 +166,7 @@ private HttpInputMessage fromJson(String json) { */ public JsonPathValueAssert extractingPath(String path) { Object value = new JsonPathValue(path).getValue(); - return new JsonPathValueAssert(value, path, this.jsonMessageConverter); + return new JsonPathValueAssert(value, path, this.contentConverter); } /** @@ -176,7 +177,7 @@ public JsonPathValueAssert extractingPath(String path) { */ public SELF hasPathSatisfying(String path, Consumer> valueRequirements) { Object value = new JsonPathValue(path).assertHasPath(); - JsonPathValueAssert valueAssert = new JsonPathValueAssert(value, path, this.jsonMessageConverter); + JsonPathValueAssert valueAssert = new JsonPathValueAssert(value, path, this.contentConverter); valueRequirements.accept(() -> valueAssert); return this.myself; } diff --git a/spring-test/src/main/java/org/springframework/test/json/AbstractJsonValueAssert.java b/spring-test/src/main/java/org/springframework/test/json/AbstractJsonValueAssert.java index e36cbd275544..ffa5409d9846 100644 --- a/spring-test/src/main/java/org/springframework/test/json/AbstractJsonValueAssert.java +++ b/spring-test/src/main/java/org/springframework/test/json/AbstractJsonValueAssert.java @@ -33,12 +33,9 @@ import org.assertj.core.internal.Failures; import org.springframework.core.ResolvableType; -import org.springframework.http.HttpInputMessage; -import org.springframework.http.MediaType; import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.lang.Nullable; -import org.springframework.mock.http.MockHttpInputMessage; -import org.springframework.mock.http.MockHttpOutputMessage; +import org.springframework.test.http.HttpMessageContentConverter; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -68,14 +65,14 @@ public abstract class AbstractJsonValueAssert httpMessageConverter; + private final HttpMessageContentConverter contentConverter; protected AbstractJsonValueAssert(@Nullable Object actual, Class selfType, - @Nullable GenericHttpMessageConverter httpMessageConverter) { + @Nullable HttpMessageContentConverter contentConverter) { super(actual, selfType); - this.httpMessageConverter = httpMessageConverter; + this.contentConverter = contentConverter; } @@ -199,19 +196,13 @@ public SELF isNotEmpty() { return this.myself; } - - @SuppressWarnings("unchecked") private T convertToTargetType(Type targetType) { - if (this.httpMessageConverter == null) { + if (this.contentConverter == null) { throw new IllegalStateException( "No JSON message converter available to convert %s".formatted(actualToString())); } try { - MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); - this.httpMessageConverter.write(this.actual, ResolvableType.forInstance(this.actual).getType(), - MediaType.APPLICATION_JSON, outputMessage); - return (T) this.httpMessageConverter.read(targetType, getClass(), - fromHttpOutputMessage(outputMessage)); + return this.contentConverter.convertViaJson(this.actual, ResolvableType.forType(targetType)); } catch (Exception ex) { throw valueProcessingFailed("To convert successfully to:%n %s%nBut it failed:%n %s%n" @@ -219,12 +210,6 @@ private T convertToTargetType(Type targetType) { } } - private HttpInputMessage fromHttpOutputMessage(MockHttpOutputMessage message) { - MockHttpInputMessage inputMessage = new MockHttpInputMessage(message.getBodyAsBytes()); - inputMessage.getHeaders().addAll(message.getHeaders()); - return inputMessage; - } - protected String getExpectedErrorMessagePrefix() { return "Expected:"; } diff --git a/spring-test/src/main/java/org/springframework/test/json/JsonContent.java b/spring-test/src/main/java/org/springframework/test/json/JsonContent.java index 383b2c3c3d24..28b02082e40b 100644 --- a/spring-test/src/main/java/org/springframework/test/json/JsonContent.java +++ b/spring-test/src/main/java/org/springframework/test/json/JsonContent.java @@ -18,8 +18,8 @@ import org.assertj.core.api.AssertProvider; -import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.lang.Nullable; +import org.springframework.test.http.HttpMessageContentConverter; import org.springframework.util.Assert; /** @@ -35,22 +35,21 @@ public final class JsonContent implements AssertProvider { private final String json; @Nullable - private final GenericHttpMessageConverter jsonMessageConverter; + private final HttpMessageContentConverter contentConverter; /** * Create a new {@code JsonContent} instance with the message converter to * use to deserialize content. * @param json the actual JSON content - * @param jsonMessageConverter the message converter to use + * @param contentConverter the content converter to use */ - public JsonContent(String json, @Nullable GenericHttpMessageConverter jsonMessageConverter) { + public JsonContent(String json, @Nullable HttpMessageContentConverter contentConverter) { Assert.notNull(json, "JSON must not be null"); this.json = json; - this.jsonMessageConverter = jsonMessageConverter; + this.contentConverter = contentConverter; } - /** * Create a new {@code JsonContent} instance. * @param json the actual JSON content @@ -59,6 +58,7 @@ public JsonContent(String json) { this(json, null); } + /** * Use AssertJ's {@link org.assertj.core.api.Assertions#assertThat assertThat} * instead. @@ -76,11 +76,11 @@ public String getJson() { } /** - * Return the message converter to use to deserialize content. + * Return the {@link HttpMessageContentConverter} to use to deserialize content. */ @Nullable - GenericHttpMessageConverter getJsonMessageConverter() { - return this.jsonMessageConverter; + HttpMessageContentConverter getContentConverter() { + return this.contentConverter; } @Override diff --git a/spring-test/src/main/java/org/springframework/test/json/JsonPathValueAssert.java b/spring-test/src/main/java/org/springframework/test/json/JsonPathValueAssert.java index f598e93ea5e5..c30843e87c57 100644 --- a/spring-test/src/main/java/org/springframework/test/json/JsonPathValueAssert.java +++ b/spring-test/src/main/java/org/springframework/test/json/JsonPathValueAssert.java @@ -18,8 +18,8 @@ import com.jayway.jsonpath.JsonPath; -import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.lang.Nullable; +import org.springframework.test.http.HttpMessageContentConverter; /** * AssertJ {@linkplain org.assertj.core.api.Assert assertions} that can be applied @@ -35,9 +35,9 @@ public class JsonPathValueAssert extends AbstractJsonValueAssert httpMessageConverter) { + @Nullable HttpMessageContentConverter contentConverter) { - super(actual, JsonPathValueAssert.class, httpMessageConverter); + super(actual, JsonPathValueAssert.class, contentConverter); this.expression = expression; } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssert.java index 1bbfd051d94b..a6c98a6331e7 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssert.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssert.java @@ -23,9 +23,9 @@ import org.assertj.core.api.Assertions; import org.assertj.core.api.ByteArrayAssert; -import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.lang.Nullable; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.http.HttpMessageContentConverter; import org.springframework.test.json.AbstractJsonContentAssert; import org.springframework.test.json.JsonContent; import org.springframework.test.json.JsonContentAssert; @@ -44,13 +44,13 @@ public abstract class AbstractMockHttpServletResponseAssert { @Nullable - private final GenericHttpMessageConverter jsonMessageConverter; + private final HttpMessageContentConverter contentConverter; protected AbstractMockHttpServletResponseAssert( - @Nullable GenericHttpMessageConverter jsonMessageConverter, ACTUAL actual, Class selfType) { + @Nullable HttpMessageContentConverter contentConverter, ACTUAL actual, Class selfType) { super(actual, selfType); - this.jsonMessageConverter = jsonMessageConverter; + this.contentConverter = contentConverter; } @@ -93,7 +93,7 @@ public AbstractStringAssert bodyText() { * */ public AbstractJsonContentAssert bodyJson() { - return new JsonContentAssert(new JsonContent(readBody(), this.jsonMessageConverter)); + return new JsonContentAssert(new JsonContent(readBody(), this.contentConverter)); } private String readBody() { diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/DefaultMvcTestResult.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/DefaultMvcTestResult.java index 731e99c1b319..5a0303def556 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/DefaultMvcTestResult.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/DefaultMvcTestResult.java @@ -16,8 +16,8 @@ package org.springframework.test.web.servlet.assertj; -import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.lang.Nullable; +import org.springframework.test.http.HttpMessageContentConverter; import org.springframework.test.web.servlet.MvcResult; /** @@ -35,15 +35,15 @@ final class DefaultMvcTestResult implements MvcTestResult { private final Exception unresolvedException; @Nullable - private final GenericHttpMessageConverter jsonMessageConverter; + private final HttpMessageContentConverter contentConverter; DefaultMvcTestResult(@Nullable MvcResult mvcResult, @Nullable Exception unresolvedException, - @Nullable GenericHttpMessageConverter jsonMessageConverter) { + @Nullable HttpMessageContentConverter contentConverter) { this.mvcResult = mvcResult; this.unresolvedException = unresolvedException; - this.jsonMessageConverter = jsonMessageConverter; + this.contentConverter = contentConverter; } @@ -74,7 +74,7 @@ public Exception getResolvedException() { */ @Override public MvcTestResultAssert assertThat() { - return new MvcTestResultAssert(this, this.jsonMessageConverter); + return new MvcTestResultAssert(this, this.contentConverter); } } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MockMvcTester.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MockMvcTester.java index 4cbb61e92e44..e5ad39a4c1fe 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MockMvcTester.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MockMvcTester.java @@ -20,20 +20,17 @@ import java.time.Duration; import java.util.Arrays; import java.util.Collection; -import java.util.Map; import java.util.function.Function; -import java.util.stream.StreamSupport; import jakarta.servlet.DispatcherType; import org.assertj.core.api.AssertProvider; import org.springframework.http.HttpMethod; -import org.springframework.http.MediaType; -import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.lang.Nullable; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockMultipartHttpServletRequest; +import org.springframework.test.http.HttpMessageContentConverter; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.RequestBuilder; @@ -133,18 +130,16 @@ */ public final class MockMvcTester { - private static final MediaType JSON = MediaType.APPLICATION_JSON; - private final MockMvc mockMvc; @Nullable - private final GenericHttpMessageConverter jsonMessageConverter; + private final HttpMessageContentConverter contentConverter; - private MockMvcTester(MockMvc mockMvc, @Nullable GenericHttpMessageConverter jsonMessageConverter) { + private MockMvcTester(MockMvc mockMvc, @Nullable HttpMessageContentConverter contentConverter) { Assert.notNull(mockMvc, "mockMVC should not be null"); this.mockMvc = mockMvc; - this.jsonMessageConverter = jsonMessageConverter; + this.contentConverter = contentConverter; } /** @@ -238,7 +233,7 @@ public static MockMvcTester of(Object... controllers) { * @return a new instance using the specified converters */ public MockMvcTester withHttpMessageConverters(Iterable> httpMessageConverters) { - return new MockMvcTester(this.mockMvc, findJsonMessageConverter(httpMessageConverters)); + return new MockMvcTester(this.mockMvc, HttpMessageContentConverter.of(httpMessageConverters)); } /** @@ -380,10 +375,10 @@ public MockMvcRequestBuilder method(HttpMethod method) { public MvcTestResult perform(RequestBuilder requestBuilder) { Object result = getMvcResultOrFailure(requestBuilder); if (result instanceof MvcResult mvcResult) { - return new DefaultMvcTestResult(mvcResult, null, this.jsonMessageConverter); + return new DefaultMvcTestResult(mvcResult, null, this.contentConverter); } else { - return new DefaultMvcTestResult(null, (Exception) result, this.jsonMessageConverter); + return new DefaultMvcTestResult(null, (Exception) result, this.contentConverter); } } @@ -396,19 +391,6 @@ private Object getMvcResultOrFailure(RequestBuilder requestBuilder) { } } - @SuppressWarnings("unchecked") - @Nullable - private GenericHttpMessageConverter findJsonMessageConverter( - Iterable> messageConverters) { - - return StreamSupport.stream(messageConverters.spliterator(), false) - .filter(GenericHttpMessageConverter.class::isInstance) - .map(GenericHttpMessageConverter.class::cast) - .filter(converter -> converter.canWrite(null, Map.class, JSON)) - .filter(converter -> converter.canRead(Map.class, JSON)) - .findFirst().orElse(null); - } - /** * Execute the request using the specified {@link RequestBuilder}. If the * request is processing asynchronously, wait at most the given @@ -502,7 +484,7 @@ public MvcTestResult asyncExchange() { @Override public MvcTestResultAssert assertThat() { - return new MvcTestResultAssert(exchange(), MockMvcTester.this.jsonMessageConverter); + return new MvcTestResultAssert(exchange(), MockMvcTester.this.contentConverter); } } @@ -560,7 +542,7 @@ public MvcTestResult asyncExchange() { @Override public MvcTestResultAssert assertThat() { - return new MvcTestResultAssert(exchange(), MockMvcTester.this.jsonMessageConverter); + return new MvcTestResultAssert(exchange(), MockMvcTester.this.contentConverter); } } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcTestResultAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcTestResultAssert.java index 53ccf513677a..55fd9e0ec6ad 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcTestResultAssert.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcTestResultAssert.java @@ -31,10 +31,10 @@ import org.assertj.core.error.BasicErrorMessageFactory; import org.assertj.core.internal.Failures; -import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.lang.Nullable; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.http.HttpMessageContentConverter; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultHandler; import org.springframework.test.web.servlet.ResultMatcher; @@ -51,8 +51,8 @@ */ public class MvcTestResultAssert extends AbstractMockHttpServletResponseAssert { - MvcTestResultAssert(MvcTestResult actual, @Nullable GenericHttpMessageConverter jsonMessageConverter) { - super(jsonMessageConverter, actual, MvcTestResultAssert.class); + MvcTestResultAssert(MvcTestResult actual, @Nullable HttpMessageContentConverter contentConverter) { + super(contentConverter, actual, MvcTestResultAssert.class); } @Override diff --git a/spring-test/src/test/java/org/springframework/test/http/HttpMessageContentConverterTests.java b/spring-test/src/test/java/org/springframework/test/http/HttpMessageContentConverterTests.java new file mode 100644 index 000000000000..994f34915885 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/http/HttpMessageContentConverterTests.java @@ -0,0 +1,273 @@ +/* + * Copyright 2002-2024 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.http; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.List; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import org.springframework.core.ResolvableType; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.converter.GenericHttpMessageConverter; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.SmartHttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.lang.Nullable; +import org.springframework.mock.http.MockHttpInputMessage; +import org.springframework.mock.http.MockHttpOutputMessage; +import org.springframework.util.StreamUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.willAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +/** + * Tests for {@link HttpMessageContentConverter}. + * + * @author Stephane Nicoll + */ +class HttpMessageContentConverterTests { + + private static final MediaType JSON = MediaType.APPLICATION_JSON; + + private static final ResolvableType listOfIntegers = ResolvableType.forClassWithGenerics(List.class, Integer.class); + + private static final MappingJackson2HttpMessageConverter jacksonMessageConverter = + new MappingJackson2HttpMessageConverter(new ObjectMapper()); + + @Test + void createInstanceWithEmptyIterable() { + assertThatIllegalArgumentException() + .isThrownBy(() -> HttpMessageContentConverter.of(List.of())) + .withMessage("At least one message converter needs to be specified"); + } + + @Test + void createInstanceWithEmptyVarArg() { + assertThatIllegalArgumentException() + .isThrownBy(HttpMessageContentConverter::of) + .withMessage("At least one message converter needs to be specified"); + } + + @Test + void convertInvokesFirstMatchingConverter() throws IOException { + HttpInputMessage message = createMessage("1,2,3"); + SmartHttpMessageConverter firstConverter = mockSmartConverterForRead( + listOfIntegers, JSON, message, List.of(1, 2, 3)); + SmartHttpMessageConverter secondConverter = mockSmartConverterForRead( + listOfIntegers, JSON, message, List.of(3, 2, 1)); + HttpMessageContentConverter contentConverter = HttpMessageContentConverter.of( + List.of(firstConverter, secondConverter)); + List data = contentConverter.convert(message, JSON, listOfIntegers); + assertThat(data).containsExactly(1, 2, 3); + verify(firstConverter).canRead(listOfIntegers, JSON); + verifyNoInteractions(secondConverter); + } + + @Test + void convertInvokesGenericHttpMessageConverter() throws IOException { + GenericHttpMessageConverter firstConverter = mock(GenericHttpMessageConverter.class); + HttpMessageContentConverter contentConverter = HttpMessageContentConverter.of( + List.of(firstConverter, jacksonMessageConverter)); + List data = contentConverter.convert(createMessage("[2,3,4]"), JSON, listOfIntegers); + assertThat(data).containsExactly(2, 3, 4); + verify(firstConverter).canRead(listOfIntegers.getType(), List.class, JSON); + } + + @Test + void convertInvokesSmartHttpMessageConverter() throws IOException { + HttpInputMessage message = createMessage("dummy"); + GenericHttpMessageConverter firstConverter = mock(GenericHttpMessageConverter.class); + SmartHttpMessageConverter smartConverter = mockSmartConverterForRead( + listOfIntegers, JSON, message, List.of(1, 2, 3)); + HttpMessageContentConverter contentConverter = HttpMessageContentConverter.of( + List.of(firstConverter, smartConverter)); + List data = contentConverter.convert(message, JSON, listOfIntegers); + assertThat(data).containsExactly(1, 2, 3); + verify(smartConverter).canRead(listOfIntegers, JSON); + } + + @Test + void convertInvokesHttpMessageConverter() throws IOException { + HttpInputMessage message = createMessage("1,2,3"); + SmartHttpMessageConverter secondConverter = mockSmartConverterForRead( + listOfIntegers, JSON, message, List.of(1, 2, 3)); + HttpMessageConverter thirdConverter = mockSimpleConverterForRead( + List.class, MediaType.TEXT_PLAIN, message, List.of(1, 2, 3)); + HttpMessageContentConverter contentConverter = HttpMessageContentConverter.of( + List.of(jacksonMessageConverter, secondConverter, thirdConverter)); + List data = contentConverter.convert(message, MediaType.TEXT_PLAIN, listOfIntegers); + assertThat(data).containsExactly(1, 2, 3); + verify(secondConverter).canRead(listOfIntegers, MediaType.TEXT_PLAIN); + verify(thirdConverter).canRead(List.class, MediaType.TEXT_PLAIN); + } + + @Test + void convertFailsIfNoMatchingConverterIsFound() throws IOException { + HttpInputMessage message = createMessage("[1,2,3]"); + SmartHttpMessageConverter textConverter = mockSmartConverterForRead( + listOfIntegers, MediaType.TEXT_PLAIN, message, List.of(1, 2, 3)); + SmartHttpMessageConverter htmlConverter = mockSmartConverterForRead( + listOfIntegers, MediaType.TEXT_HTML, message, List.of(3, 2, 1)); + HttpMessageContentConverter contentConverter = HttpMessageContentConverter.of( + List.of(textConverter, htmlConverter)); + assertThatIllegalStateException() + .isThrownBy(() -> contentConverter.convert(message, JSON, listOfIntegers)) + .withMessage("No converter found to read [application/json] to [java.util.List]"); + verify(textConverter).canRead(listOfIntegers, JSON); + verify(htmlConverter).canRead(listOfIntegers, JSON); + } + + @Test + void convertViaJsonInvokesFirstMatchingConverter() throws IOException { + String value = "1,2,3"; + ResolvableType valueType = ResolvableType.forInstance(value); + SmartHttpMessageConverter readConverter = mockSmartConverterForRead(listOfIntegers, JSON, null, List.of(1, 2, 3)); + SmartHttpMessageConverter firstWriteJsonConverter = mockSmartConverterForWritingJson(value, valueType, "[1,2,3]"); + SmartHttpMessageConverter secondWriteJsonConverter = mockSmartConverterForWritingJson(value, valueType, "[3,2,1]"); + HttpMessageContentConverter contentConverter = HttpMessageContentConverter.of( + List.of(readConverter, firstWriteJsonConverter, secondWriteJsonConverter)); + List data = contentConverter.convertViaJson(value, listOfIntegers); + assertThat(data).containsExactly(1, 2, 3); + verify(readConverter).canRead(listOfIntegers, JSON); + verify(firstWriteJsonConverter).canWrite(valueType, String.class, JSON); + verifyNoInteractions(secondWriteJsonConverter); + } + + @Test + void convertViaJsonInvokesGenericHttpMessageConverter() throws IOException { + String value = "1,2,3"; + ResolvableType valueType = ResolvableType.forInstance(value); + SmartHttpMessageConverter readConverter = mockSmartConverterForRead(listOfIntegers, JSON, null, List.of(1, 2, 3)); + GenericHttpMessageConverter writeConverter = mockGenericConverterForWritingJson(value, valueType, "[3,2,1]"); + HttpMessageContentConverter contentConverter = HttpMessageContentConverter.of( + List.of(readConverter, writeConverter, jacksonMessageConverter)); + List data = contentConverter.convertViaJson("[1, 2, 3]", listOfIntegers); + assertThat(data).containsExactly(1, 2, 3); + verify(readConverter).canRead(listOfIntegers, JSON); + verify(writeConverter).canWrite(valueType.getType(), value.getClass(), JSON); + } + + @Test + void convertViaJsonInvokesSmartHttpMessageConverter() throws IOException { + String value = "1,2,3"; + ResolvableType valueType = ResolvableType.forInstance(value); + SmartHttpMessageConverter readConverter = mockSmartConverterForRead(listOfIntegers, JSON, null, List.of(1, 2, 3)); + SmartHttpMessageConverter writeConverter = mockSmartConverterForWritingJson(value, valueType, "[3,2,1]"); + HttpMessageContentConverter contentConverter = HttpMessageContentConverter.of( + List.of(readConverter, writeConverter, jacksonMessageConverter)); + List data = contentConverter.convertViaJson("[1, 2, 3]", listOfIntegers); + assertThat(data).containsExactly(1, 2, 3); + verify(readConverter).canRead(listOfIntegers, JSON); + verify(writeConverter).canWrite(valueType, value.getClass(), JSON); + } + + @Test + void convertViaJsonInvokesHttpMessageConverter() throws IOException { + String value = "1,2,3"; + SmartHttpMessageConverter readConverter = mockSmartConverterForRead(listOfIntegers, JSON, null, List.of(1, 2, 3)); + HttpMessageConverter writeConverter = mockSimpleConverterForWritingJson(value, "[3,2,1]"); + HttpMessageContentConverter contentConverter = HttpMessageContentConverter.of( + List.of(readConverter, writeConverter, jacksonMessageConverter)); + List data = contentConverter.convertViaJson("[1, 2, 3]", listOfIntegers); + assertThat(data).containsExactly(1, 2, 3); + verify(readConverter).canRead(listOfIntegers, JSON); + verify(writeConverter).canWrite(value.getClass(), JSON); + } + + @Test + void convertViaJsonFailsIfNoMatchingConverterIsFound() throws IOException { + String value = "1,2,3"; + ResolvableType valueType = ResolvableType.forInstance(value); + SmartHttpMessageConverter readConverter = mockSmartConverterForRead(listOfIntegers, JSON, null, List.of(1, 2, 3)); + HttpMessageContentConverter contentConverter = HttpMessageContentConverter.of(List.of(readConverter)); + assertThatIllegalStateException() + .isThrownBy(() -> contentConverter.convertViaJson(value, listOfIntegers)) + .withMessage("No converter found to convert [java.lang.String] to JSON"); + verify(readConverter).canWrite(valueType, value.getClass(), JSON); + } + + @SuppressWarnings("unchecked") + private static SmartHttpMessageConverter mockSmartConverterForRead( + ResolvableType type, MediaType mediaType, @Nullable HttpInputMessage message, Object value) throws IOException { + SmartHttpMessageConverter converter = mock(SmartHttpMessageConverter.class); + given(converter.canRead(type, mediaType)).willReturn(true); + given(converter.read(eq(type), (message != null ? eq(message) : any()), any())).willReturn(value); + return converter; + } + + @SuppressWarnings("unchecked") + private static SmartHttpMessageConverter mockSmartConverterForWritingJson(Object value, ResolvableType valueType, String json) throws IOException { + SmartHttpMessageConverter converter = mock(SmartHttpMessageConverter.class); + given(converter.canWrite(valueType, value.getClass(), JSON)).willReturn(true); + willAnswer(invocation -> { + MockHttpOutputMessage out = invocation.getArgument(3, MockHttpOutputMessage.class); + StreamUtils.copy(json, StandardCharsets.UTF_8, out.getBody()); + return null; + }).given(converter).write(eq(value), eq(valueType), eq(JSON), any(), any()); + return converter; + } + + @SuppressWarnings("unchecked") + private static GenericHttpMessageConverter mockGenericConverterForWritingJson(Object value, ResolvableType valueType, String json) throws IOException { + GenericHttpMessageConverter converter = mock(GenericHttpMessageConverter.class); + given(converter.canWrite(valueType.getType(), value.getClass(), JSON)).willReturn(true); + willAnswer(invocation -> { + MockHttpOutputMessage out = invocation.getArgument(4, MockHttpOutputMessage.class); + StreamUtils.copy(json, StandardCharsets.UTF_8, out.getBody()); + return null; + }).given(converter).write(eq(value), eq(valueType.getType()), eq(JSON), any()); + return converter; + } + + @SuppressWarnings("unchecked") + private static HttpMessageConverter mockSimpleConverterForRead( + Class rawType, MediaType mediaType, HttpInputMessage message, Object value) throws IOException { + HttpMessageConverter converter = mock(HttpMessageConverter.class); + given(converter.canRead(rawType, mediaType)).willReturn(true); + given(converter.read(rawType, message)).willReturn(value); + return converter; + } + + @SuppressWarnings("unchecked") + private static HttpMessageConverter mockSimpleConverterForWritingJson(Object value, String json) throws IOException { + HttpMessageConverter converter = mock(HttpMessageConverter.class); + given(converter.canWrite(value.getClass(), JSON)).willReturn(true); + willAnswer(invocation -> { + MockHttpOutputMessage out = invocation.getArgument(2, MockHttpOutputMessage.class); + StreamUtils.copy(json, StandardCharsets.UTF_8, out.getBody()); + return null; + }).given(converter).write(eq(value), eq(JSON), any()); + return converter; + } + + private static HttpInputMessage createMessage(String content) { + return new MockHttpInputMessage(content.getBytes(StandardCharsets.UTF_8)); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/json/AbstractJsonContentAssertTests.java b/spring-test/src/test/java/org/springframework/test/json/AbstractJsonContentAssertTests.java index 29caa39a074d..1c276b2463a2 100644 --- a/spring-test/src/test/java/org/springframework/test/json/AbstractJsonContentAssertTests.java +++ b/spring-test/src/test/java/org/springframework/test/json/AbstractJsonContentAssertTests.java @@ -51,9 +51,9 @@ import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.Resource; -import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.lang.Nullable; +import org.springframework.test.http.HttpMessageContentConverter; import org.springframework.util.FileCopyUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -85,8 +85,8 @@ class AbstractJsonContentAssertTests { private static final String DIFFERENT = loadJson("different.json"); - private static final MappingJackson2HttpMessageConverter jsonHttpMessageConverter = - new MappingJackson2HttpMessageConverter(new ObjectMapper()); + private static final HttpMessageContentConverter jsonContentConverter = HttpMessageContentConverter.of( + new MappingJackson2HttpMessageConverter(new ObjectMapper())); private static final JsonComparator comparator = JsonAssert.comparator(JsonCompareMode.LENIENT); @@ -108,14 +108,14 @@ class ConversionTests { @Test void convertToTargetType() { - assertThat(forJson(SIMPSONS, jsonHttpMessageConverter)) + assertThat(forJson(SIMPSONS, jsonContentConverter)) .convertTo(Family.class) .satisfies(family -> assertThat(family.familyMembers()).hasSize(5)); } @Test void convertToIncompatibleTargetTypeShouldFail() { - AbstractJsonContentAssert jsonAssert = assertThat(forJson(SIMPSONS, jsonHttpMessageConverter)); + AbstractJsonContentAssert jsonAssert = assertThat(forJson(SIMPSONS, jsonContentConverter)); assertThatExceptionOfType(AssertionError.class) .isThrownBy(() -> jsonAssert.convertTo(Member.class)) .withMessageContainingAll("To convert successfully to:", @@ -124,15 +124,15 @@ void convertToIncompatibleTargetTypeShouldFail() { @Test void convertUsingAssertFactory() { - assertThat(forJson(SIMPSONS, jsonHttpMessageConverter)) + assertThat(forJson(SIMPSONS, jsonContentConverter)) .convertTo(new FamilyAssertFactory()) .hasFamilyMember("Homer"); } private AssertProvider> forJson(@Nullable String json, - @Nullable GenericHttpMessageConverter jsonHttpMessageConverter) { + @Nullable HttpMessageContentConverter jsonContentConverter) { - return () -> new TestJsonContentAssert(json, jsonHttpMessageConverter); + return () -> new TestJsonContentAssert(json, jsonContentConverter); } private static class FamilyAssertFactory extends InstanceOfAssertFactory { @@ -320,14 +320,14 @@ void convertToWithoutHttpMessageConverterShouldFail() { @Test void convertToTargetType() { - assertThat(forJson(SIMPSONS, jsonHttpMessageConverter)) + assertThat(forJson(SIMPSONS, jsonContentConverter)) .extractingPath("$.familyMembers[0]").convertTo(Member.class) .satisfies(member -> assertThat(member.name).isEqualTo("Homer")); } @Test void convertToIncompatibleTargetTypeShouldFail() { - JsonPathValueAssert path = assertThat(forJson(SIMPSONS, jsonHttpMessageConverter)) + JsonPathValueAssert path = assertThat(forJson(SIMPSONS, jsonContentConverter)) .extractingPath("$.familyMembers[0]"); assertThatExceptionOfType(AssertionError.class) .isThrownBy(() -> path.convertTo(ExtractingPathTests.Customer.class)) @@ -337,7 +337,7 @@ void convertToIncompatibleTargetTypeShouldFail() { @Test void convertArrayUsingAssertFactory() { - assertThat(forJson(SIMPSONS, jsonHttpMessageConverter)) + assertThat(forJson(SIMPSONS, jsonContentConverter)) .extractingPath("$.familyMembers") .convertTo(InstanceOfAssertFactories.list(Member.class)) .hasSize(5).element(0).isEqualTo(new Member("Homer")); @@ -395,8 +395,8 @@ private AssertProvider> forJson(@Nullable String js return () -> new TestJsonContentAssert(json, null); } - private AssertProvider> forJson(@Nullable String json, GenericHttpMessageConverter jsonHttpMessageConverter) { - return () -> new TestJsonContentAssert(json, jsonHttpMessageConverter); + private AssertProvider> forJson(@Nullable String json, HttpMessageContentConverter jsonContentConverter) { + return () -> new TestJsonContentAssert(json, jsonContentConverter); } } @@ -895,8 +895,8 @@ record Family(List familyMembers) {} private static class TestJsonContentAssert extends AbstractJsonContentAssert { - public TestJsonContentAssert(@Nullable String json, @Nullable GenericHttpMessageConverter jsonMessageConverter) { - super((json != null ? new JsonContent(json, jsonMessageConverter) : null), TestJsonContentAssert.class); + public TestJsonContentAssert(@Nullable String json, @Nullable HttpMessageContentConverter jsonContentConverter) { + super((json != null ? new JsonContent(json, jsonContentConverter) : null), TestJsonContentAssert.class); } } diff --git a/spring-test/src/test/java/org/springframework/test/json/JsonContentTests.java b/spring-test/src/test/java/org/springframework/test/json/JsonContentTests.java index b87e8849b054..85a1c588a976 100644 --- a/spring-test/src/test/java/org/springframework/test/json/JsonContentTests.java +++ b/spring-test/src/test/java/org/springframework/test/json/JsonContentTests.java @@ -18,7 +18,8 @@ import org.junit.jupiter.api.Test; -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.test.http.HttpMessageContentConverter; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -61,10 +62,10 @@ void toStringShouldReturnString() { } @Test - void getJsonMessageConverterShouldReturnConverter() { - MappingJackson2HttpMessageConverter converter = mock(MappingJackson2HttpMessageConverter.class); - JsonContent content = new JsonContent(JSON, converter); - assertThat(content.getJsonMessageConverter()).isSameAs(converter); + void getJsonContentConverterShouldReturnConverter() { + HttpMessageContentConverter contentConverter = HttpMessageContentConverter.of(mock(HttpMessageConverter.class)); + JsonContent content = new JsonContent(JSON, contentConverter); + assertThat(content.getContentConverter()).isSameAs(contentConverter); } } diff --git a/spring-test/src/test/java/org/springframework/test/json/JsonPathValueAssertTests.java b/spring-test/src/test/java/org/springframework/test/json/JsonPathValueAssertTests.java index 54d4ac26f986..60a30b779970 100644 --- a/spring-test/src/test/java/org/springframework/test/json/JsonPathValueAssertTests.java +++ b/spring-test/src/test/java/org/springframework/test/json/JsonPathValueAssertTests.java @@ -30,6 +30,7 @@ import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.lang.Nullable; +import org.springframework.test.http.HttpMessageContentConverter; import org.springframework.util.StringUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -204,8 +205,8 @@ void asMapWithNullFails() { @Nested class ConvertToTests { - private static final MappingJackson2HttpMessageConverter jsonHttpMessageConverter = - new MappingJackson2HttpMessageConverter(new ObjectMapper()); + private static final HttpMessageContentConverter jsonContentConverter = HttpMessageContentConverter.of( + new MappingJackson2HttpMessageConverter(new ObjectMapper())); @Test void convertToWithoutHttpMessageConverter() { @@ -246,7 +247,7 @@ void convertObjectToPojoWithMissingMandatoryField() { private AssertProvider forValue(@Nullable Object actual) { - return () -> new JsonPathValueAssert(actual, "$.test", jsonHttpMessageConverter); + return () -> new JsonPathValueAssert(actual, "$.test", jsonContentConverter); } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/MockMvcTesterTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/MockMvcTesterTests.java index fef8bd7caa8c..60294267a1d1 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/MockMvcTesterTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/MockMvcTesterTests.java @@ -16,8 +16,8 @@ package org.springframework.test.web.servlet.assertj; +import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; @@ -125,8 +125,7 @@ void createWithControllerCanConfigureHttpMessageConverters() { } @Test - @SuppressWarnings("unchecked") - void withHttpMessageConverterDetectsJsonConverter() { + void withHttpMessageConverterUsesConverter() { MappingJackson2HttpMessageConverter converter = spy(jsonHttpMessageConverter); MockMvcTester mockMvc = MockMvcTester.of(HelloController.class) .withHttpMessageConverters(List.of(mock(), mock(), converter)); @@ -135,7 +134,7 @@ void withHttpMessageConverterDetectsJsonConverter() { assertThat(message.message()).isEqualTo("Hello World"); assertThat(message.counter()).isEqualTo(42); }); - verify(converter).canWrite(Map.class, MediaType.APPLICATION_JSON); + verify(converter).canWrite(LinkedHashMap.class, LinkedHashMap.class, MediaType.APPLICATION_JSON); } @Test