diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java index 7e23910cf3..da784ae0fa 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * 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. @@ -30,6 +30,8 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; import java.util.function.Function; +import com.jayway.jsonpath.Configuration; +import com.jayway.jsonpath.spi.mapper.MappingProvider; import org.hamcrest.Matcher; import org.hamcrest.MatcherAssert; import org.reactivestreams.Publisher; @@ -57,6 +59,7 @@ import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.client.ClientRequest; import org.springframework.web.reactive.function.client.ClientResponse; import org.springframework.web.reactive.function.client.ExchangeFunction; +import org.springframework.web.reactive.function.client.ExchangeStrategies; import org.springframework.web.util.UriBuilder; import org.springframework.web.util.UriBuilderFactory; @@ -72,6 +75,9 @@ class DefaultWebTestClient implements WebTestClient { private final WiretapConnector wiretapConnector; + @Nullable + private final JsonEncoderDecoder jsonEncoderDecoder; + private final ExchangeFunction exchangeFunction; private final UriBuilderFactory uriBuilderFactory; @@ -91,13 +97,15 @@ class DefaultWebTestClient implements WebTestClient { private final AtomicLong requestIndex = new AtomicLong(); - DefaultWebTestClient(ClientHttpConnector connector, + DefaultWebTestClient(ClientHttpConnector connector, ExchangeStrategies exchangeStrategies, Function exchangeFactory, UriBuilderFactory uriBuilderFactory, @Nullable HttpHeaders headers, @Nullable MultiValueMap cookies, Consumer> entityResultConsumer, @Nullable Duration responseTimeout, DefaultWebTestClientBuilder clientBuilder) { this.wiretapConnector = new WiretapConnector(connector); + this.jsonEncoderDecoder = JsonEncoderDecoder.from( + exchangeStrategies.messageWriters(), exchangeStrategies.messageReaders()); this.exchangeFunction = exchangeFactory.apply(this.wiretapConnector); this.uriBuilderFactory = uriBuilderFactory; this.defaultHeaders = headers; @@ -362,6 +370,7 @@ class DefaultWebTestClient implements WebTestClient { this.requestId, this.uriTemplate, getResponseTimeout()); return new DefaultResponseSpec(result, response, + DefaultWebTestClient.this.jsonEncoderDecoder, DefaultWebTestClient.this.entityResultConsumer, getResponseTimeout()); } @@ -399,6 +408,9 @@ class DefaultWebTestClient implements WebTestClient { private final ClientResponse response; + @Nullable + private final JsonEncoderDecoder jsonEncoderDecoder; + private final Consumer> entityResultConsumer; private final Duration timeout; @@ -406,11 +418,13 @@ class DefaultWebTestClient implements WebTestClient { DefaultResponseSpec( ExchangeResult exchangeResult, ClientResponse response, + @Nullable JsonEncoderDecoder jsonEncoderDecoder, Consumer> entityResultConsumer, Duration timeout) { this.exchangeResult = exchangeResult; this.response = response; + this.jsonEncoderDecoder = jsonEncoderDecoder; this.entityResultConsumer = entityResultConsumer; this.timeout = timeout; } @@ -466,7 +480,7 @@ class DefaultWebTestClient implements WebTestClient { ByteArrayResource resource = this.response.bodyToMono(ByteArrayResource.class).block(this.timeout); byte[] body = (resource != null ? resource.getByteArray() : null); EntityExchangeResult entityResult = initEntityExchangeResult(body); - return new DefaultBodyContentSpec(entityResult); + return new DefaultBodyContentSpec(entityResult, this.jsonEncoderDecoder); } private EntityExchangeResult initEntityExchangeResult(@Nullable B body) { @@ -625,10 +639,14 @@ class DefaultWebTestClient implements WebTestClient { private final EntityExchangeResult result; + @Nullable + private final JsonEncoderDecoder jsonEncoderDecoder; + private final boolean isEmpty; - DefaultBodyContentSpec(EntityExchangeResult result) { + DefaultBodyContentSpec(EntityExchangeResult result, @Nullable JsonEncoderDecoder jsonEncoderDecoder) { this.result = result; + this.jsonEncoderDecoder = jsonEncoderDecoder; this.isEmpty = (result.getResponseBody() == null || result.getResponseBody().length == 0); } @@ -666,8 +684,16 @@ class DefaultWebTestClient implements WebTestClient { } @Override + public JsonPathAssertions jsonPath(String expression) { + return new JsonPathAssertions(this, getBodyAsString(), expression, + JsonPathConfigurationProvider.getConfiguration(this.jsonEncoderDecoder)); + } + + @Override + @SuppressWarnings("removal") public JsonPathAssertions jsonPath(String expression, Object... args) { - return new JsonPathAssertions(this, getBodyAsString(), expression, args); + Assert.hasText(expression, "expression must not be null or empty"); + return jsonPath(expression.formatted(args)); } @Override @@ -697,4 +723,18 @@ class DefaultWebTestClient implements WebTestClient { } } + + private static class JsonPathConfigurationProvider { + + static Configuration getConfiguration(@Nullable JsonEncoderDecoder jsonEncoderDecoder) { + Configuration jsonPathConfiguration = Configuration.defaultConfiguration(); + if (jsonEncoderDecoder != null) { + MappingProvider mappingProvider = new EncoderDecoderMappingProvider( + jsonEncoderDecoder.encoder(), jsonEncoderDecoder.decoder()); + return jsonPathConfiguration.mappingProvider(mappingProvider); + } + return jsonPathConfiguration; + } + } + } diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java index 996db49b8b..61a5e47f5a 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClientBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * 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. @@ -294,8 +294,9 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder { if (connectorToUse == null) { connectorToUse = initConnector(); } + ExchangeStrategies exchangeStrategies = initExchangeStrategies(); Function exchangeFactory = connector -> { - ExchangeFunction exchange = ExchangeFunctions.create(connector, initExchangeStrategies()); + ExchangeFunction exchange = ExchangeFunctions.create(connector, exchangeStrategies); if (CollectionUtils.isEmpty(this.filters)) { return exchange; } @@ -305,7 +306,7 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder { .orElse(exchange); }; - return new DefaultWebTestClient(connectorToUse, exchangeFactory, initUriBuilderFactory(), + return new DefaultWebTestClient(connectorToUse, exchangeStrategies, exchangeFactory, initUriBuilderFactory(), this.defaultHeaders != null ? HttpHeaders.readOnlyHttpHeaders(this.defaultHeaders) : null, this.defaultCookies != null ? CollectionUtils.unmodifiableMultiValueMap(this.defaultCookies) : null, this.entityResultConsumer, this.responseTimeout, new DefaultWebTestClientBuilder(this)); diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/EncoderDecoderMappingProvider.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/EncoderDecoderMappingProvider.java new file mode 100644 index 0000000000..f6a6dc1cdd --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/EncoderDecoderMappingProvider.java @@ -0,0 +1,84 @@ +/* + * 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.web.reactive.server; + +import java.util.Collections; +import java.util.Map; + +import com.jayway.jsonpath.Configuration; +import com.jayway.jsonpath.TypeRef; +import com.jayway.jsonpath.spi.mapper.MappingProvider; + +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.Decoder; +import org.springframework.core.codec.Encoder; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.lang.Nullable; +import org.springframework.util.MimeType; +import org.springframework.util.MimeTypeUtils; + +/** + * JSON Path {@link MappingProvider} implementation using {@link Encoder} + * and {@link Decoder}. + * + * @author Rossen Stoyanchev + * @author Stephane Nicoll + * @since 6.2 + */ +final class EncoderDecoderMappingProvider implements MappingProvider { + + private final Encoder encoder; + + private final Decoder decoder; + + /** + * Create an instance with the specified writers and readers. + */ + public EncoderDecoderMappingProvider(Encoder encoder, Decoder decoder) { + this.encoder = encoder; + this.decoder = decoder; + } + + + @Nullable + @Override + public T map(Object source, Class targetType, Configuration configuration) { + return mapToTargetType(source, ResolvableType.forClass(targetType)); + } + + @Nullable + @Override + public T map(Object source, TypeRef targetType, Configuration configuration) { + return mapToTargetType(source, ResolvableType.forType(targetType.getType())); + } + + @SuppressWarnings("unchecked") + @Nullable + private T mapToTargetType(Object source, ResolvableType targetType) { + DataBufferFactory bufferFactory = DefaultDataBufferFactory.sharedInstance; + MimeType mimeType = MimeTypeUtils.APPLICATION_JSON; + Map hints = Collections.emptyMap(); + + DataBuffer buffer = ((Encoder) this.encoder).encodeValue( + (T) source, bufferFactory, ResolvableType.forInstance(source), mimeType, hints); + + return ((Decoder) this.decoder).decode(buffer, targetType, mimeType, hints); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonEncoderDecoder.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonEncoderDecoder.java new file mode 100644 index 0000000000..bb1bc6fc72 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonEncoderDecoder.java @@ -0,0 +1,112 @@ +/* + * 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.web.reactive.server; + +import java.util.Collection; +import java.util.Map; +import java.util.stream.Stream; + +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.Decoder; +import org.springframework.core.codec.Encoder; +import org.springframework.http.MediaType; +import org.springframework.http.codec.DecoderHttpMessageReader; +import org.springframework.http.codec.EncoderHttpMessageWriter; +import org.springframework.http.codec.HttpMessageReader; +import org.springframework.http.codec.HttpMessageWriter; +import org.springframework.lang.Nullable; + +/** + * {@link Encoder} and {@link Decoder} that is able to handle a map to and from + * json. Used to configure the jsonpath infrastructure without having a hard + * dependency on the library. + * + * @param encoder the json encoder + * @param decoder the json decoder + * @author Stephane Nicoll + * @author Rossen Stoyanchev + * @since 6.2 + */ +record JsonEncoderDecoder(Encoder encoder, Decoder decoder) { + + private static final ResolvableType MAP_TYPE = ResolvableType.forClass(Map.class); + + + /** + * Create a {@link JsonEncoderDecoder} instance based on the specified + * infrastructure. + * @param messageWriters the HTTP message writers + * @param messageReaders the HTTP message readers + * @return a {@link JsonEncoderDecoder} or {@code null} if a suitable codec + * is not available + */ + @Nullable + static JsonEncoderDecoder from(Collection> messageWriters, + Collection> messageReaders) { + + Encoder jsonEncoder = findJsonEncoder(messageWriters); + Decoder jsonDecoder = findJsonDecoder(messageReaders); + if (jsonEncoder != null && jsonDecoder != null) { + return new JsonEncoderDecoder(jsonEncoder, jsonDecoder); + } + return null; + } + + + /** + * Find the first suitable {@link Encoder} that can encode a {@link Map} + * to json. + * @param writers the writers to inspect + * @return a suitable json {@link Encoder} or {@code null} + */ + @Nullable + private static Encoder findJsonEncoder(Collection> writers) { + return findJsonEncoder(writers.stream() + .filter(writer -> writer instanceof EncoderHttpMessageWriter) + .map(writer -> ((EncoderHttpMessageWriter) writer).getEncoder())); + } + + @Nullable + private static Encoder findJsonEncoder(Stream> stream) { + return stream + .filter(encoder -> encoder.canEncode(MAP_TYPE, MediaType.APPLICATION_JSON)) + .findFirst() + .orElse(null); + } + + /** + * Find the first suitable {@link Decoder} that can decode a {@link Map} to + * json. + * @param readers the readers to inspect + * @return a suitable json {@link Decoder} or {@code null} + */ + @Nullable + private static Decoder findJsonDecoder(Collection> readers) { + return findJsonDecoder(readers.stream() + .filter(reader -> reader instanceof DecoderHttpMessageReader) + .map(reader -> ((DecoderHttpMessageReader) reader).getDecoder())); + } + + @Nullable + private static Decoder findJsonDecoder(Stream> decoderStream) { + return decoderStream + .filter(decoder -> decoder.canDecode(MAP_TYPE, MediaType.APPLICATION_JSON)) + .findFirst() + .orElse(null); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonPathAssertions.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonPathAssertions.java index 9bdbe7eefd..d761575ac8 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonPathAssertions.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonPathAssertions.java @@ -18,8 +18,10 @@ package org.springframework.test.web.reactive.server; import java.util.function.Consumer; +import com.jayway.jsonpath.Configuration; import org.hamcrest.Matcher; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.lang.Nullable; import org.springframework.test.util.JsonPathExpectationsHelper; import org.springframework.util.Assert; @@ -28,6 +30,7 @@ import org.springframework.util.Assert; * JsonPath assertions. * * @author Rossen Stoyanchev + * @author Stephane Nicoll * @since 5.0 * @see https://github.com/jayway/JsonPath * @see JsonPathExpectationsHelper @@ -41,11 +44,12 @@ public class JsonPathAssertions { private final JsonPathExpectationsHelper pathHelper; - JsonPathAssertions(WebTestClient.BodyContentSpec spec, String content, String expression, Object... args) { + JsonPathAssertions(WebTestClient.BodyContentSpec spec, String content, String expression, + @Nullable Configuration configuration) { Assert.hasText(expression, "expression must not be null or empty"); this.bodySpec = spec; this.content = content; - this.pathHelper = new JsonPathExpectationsHelper(expression.formatted(args)); + this.pathHelper = new JsonPathExpectationsHelper(expression, configuration); } @@ -168,6 +172,15 @@ public class JsonPathAssertions { return this.bodySpec; } + /** + * Delegates to {@link JsonPathExpectationsHelper#assertValue(String, Matcher, ParameterizedTypeReference)}. + * @since 6.2 + */ + public WebTestClient.BodyContentSpec value(ParameterizedTypeReference targetType, Matcher matcher) { + this.pathHelper.assertValue(this.content, matcher, targetType); + return this.bodySpec; + } + /** * Consume the result of the JSONPath evaluation. * @since 5.1 @@ -199,6 +212,16 @@ public class JsonPathAssertions { return value(targetType, consumer); } + /** + * Consume the result of the JSONPath evaluation and provide a parameterized type. + * @since 6.2 + */ + public WebTestClient.BodyContentSpec value(ParameterizedTypeReference targetType, Consumer consumer) { + T value = this.pathHelper.evaluateJsonPath(this.content, targetType); + consumer.accept(value); + return this.bodySpec; + } + @Override public boolean equals(@Nullable Object obj) { throw new AssertionError("Object#equals is disabled " + diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java index 6b4e220728..96938ac7b7 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClient.java @@ -1035,6 +1035,15 @@ public interface WebTestClient { */ BodyContentSpec xml(String expectedXml); + /** + * Access to response body assertions using a + * JsonPath expression + * to inspect a specific subset of the body. + * @param expression the JsonPath expression + * @since 6.2 + */ + JsonPathAssertions jsonPath(String expression); + /** * Access to response body assertions using a * JsonPath expression @@ -1043,7 +1052,9 @@ public interface WebTestClient { * formatting specifiers as defined in {@link String#format}. * @param expression the JsonPath expression * @param args arguments to parameterize the expression + * @deprecated in favor of calling {@link String#formatted(Object...)} upfront */ + @Deprecated(since = "6.2", forRemoval = true) JsonPathAssertions jsonPath(String expression, Object... args); /** diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/EncoderDecoderMappingProviderTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/EncoderDecoderMappingProviderTests.java new file mode 100644 index 0000000000..e2c9d27a37 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/EncoderDecoderMappingProviderTests.java @@ -0,0 +1,65 @@ +/* + * 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.web.reactive.server; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.jayway.jsonpath.Configuration; +import com.jayway.jsonpath.TypeRef; +import org.junit.jupiter.api.Test; + +import org.springframework.http.codec.json.Jackson2JsonDecoder; +import org.springframework.http.codec.json.Jackson2JsonEncoder; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link EncoderDecoderMappingProvider}. + * + * @author Stephane Nicoll + */ +class EncoderDecoderMappingProviderTests { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private final EncoderDecoderMappingProvider mappingProvider = new EncoderDecoderMappingProvider( + new Jackson2JsonEncoder(objectMapper), new Jackson2JsonDecoder(objectMapper)); + + + @Test + void mapType() { + Data data = this.mappingProvider.map(jsonData("test", 42), Data.class, Configuration.defaultConfiguration()); + assertThat(data).isEqualTo(new Data("test", 42)); + } + + @Test + void mapGenericType() { + List jsonData = List.of(jsonData("first", 1), jsonData("second", 2), jsonData("third", 3)); + List data = this.mappingProvider.map(jsonData, new TypeRef>() {}, Configuration.defaultConfiguration()); + assertThat(data).containsExactly(new Data("first", 1), new Data("second", 2), new Data("third", 3)); + } + + private Map jsonData(String name, int counter) { + return Map.of("name", name, "counter", counter); + } + + + record Data(String name, int counter) {} + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/JsonEncoderDecoderTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/JsonEncoderDecoderTests.java new file mode 100644 index 0000000000..6655b071f7 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/JsonEncoderDecoderTests.java @@ -0,0 +1,80 @@ +/* + * 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.web.reactive.server; + +import java.util.List; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import org.springframework.http.codec.DecoderHttpMessageReader; +import org.springframework.http.codec.EncoderHttpMessageWriter; +import org.springframework.http.codec.HttpMessageReader; +import org.springframework.http.codec.HttpMessageWriter; +import org.springframework.http.codec.ResourceHttpMessageReader; +import org.springframework.http.codec.ResourceHttpMessageWriter; +import org.springframework.http.codec.json.Jackson2JsonDecoder; +import org.springframework.http.codec.json.Jackson2JsonEncoder; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link JsonEncoderDecoder}. + * + * @author Stephane Nicoll + */ +class JsonEncoderDecoderTests { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private static final HttpMessageWriter jacksonMessageWriter = new EncoderHttpMessageWriter<>( + new Jackson2JsonEncoder(objectMapper)); + + private static final HttpMessageReader jacksonMessageReader = new DecoderHttpMessageReader<>( + new Jackson2JsonDecoder(objectMapper)); + + @Test + void fromWithEmptyWriters() { + assertThat(JsonEncoderDecoder.from(List.of(), List.of(jacksonMessageReader))).isNull(); + } + + @Test + void fromWithEmptyReaders() { + assertThat(JsonEncoderDecoder.from(List.of(jacksonMessageWriter), List.of())).isNull(); + } + + @Test + void fromWithSuitableWriterAndNoReader() { + assertThat(JsonEncoderDecoder.from(List.of(jacksonMessageWriter), List.of(new ResourceHttpMessageReader()))).isNull(); + } + + @Test + void fromWithSuitableReaderAndNoWriter() { + assertThat(JsonEncoderDecoder.from(List.of(new ResourceHttpMessageWriter()), List.of(jacksonMessageReader))).isNull(); + } + + @Test + void fromWithNoSuitableReaderAndWriter() { + JsonEncoderDecoder jsonEncoderDecoder = JsonEncoderDecoder.from( + List.of(new ResourceHttpMessageWriter(), jacksonMessageWriter), + List.of(new ResourceHttpMessageReader(), jacksonMessageReader)); + assertThat(jsonEncoderDecoder).isNotNull(); + assertThat(jsonEncoderDecoder.encoder()).isInstanceOf(Jackson2JsonEncoder.class); + assertThat(jsonEncoderDecoder.decoder()).isInstanceOf(Jackson2JsonDecoder.class); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/ResponseBodyTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/ResponseBodyTests.java index d5de16450d..2b01b907fd 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/ResponseBodyTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/ResponseBodyTests.java @@ -16,15 +16,21 @@ package org.springframework.test.web.servlet.samples.client.standalone; +import java.util.List; +import java.util.function.Consumer; + import jakarta.validation.constraints.NotNull; import org.junit.jupiter.api.Test; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.WebTestClient.BodyContentSpec; import org.springframework.test.web.servlet.client.MockMvcWebTestClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; +import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.equalTo; /** @@ -32,60 +38,64 @@ import static org.hamcrest.Matchers.equalTo; * {@link org.springframework.test.web.servlet.samples.standalone.ResponseBodyTests}. * * @author Rossen Stoyanchev + * @author Stephane Nicoll */ class ResponseBodyTests { @Test void json() { - MockMvcWebTestClient.bindToController(new PersonController()).build() + execute("/persons/Lee", body -> body.jsonPath("$.name").isEqualTo("Lee") + .jsonPath("$.age").isEqualTo(42) + .jsonPath("$.age").value(equalTo(42)) + .jsonPath("$.age").value(Float.class, equalTo(42.0f))); + } + + @Test + void jsonPathWithCustomType() { + execute("/persons/Lee", body -> body.jsonPath("$").isEqualTo(new Person("Lee", 42))); + } + + @Test + void jsonPathWithResolvedValue() { + execute("/persons/Lee", body -> body.jsonPath("$").value(Person.class, + candidate -> assertThat(candidate).isEqualTo(new Person("Lee", 42)))); + } + + @Test + void jsonPathWithResolvedGenericValue() { + execute("/persons", body -> body.jsonPath("$").value(new ParameterizedTypeReference>() {}, + candidate -> assertThat(candidate).hasSize(3).extracting(Person::name) + .containsExactly("Rossen", "Juergen", "Arjen"))); + } + + private void execute(String uri, Consumer assertions) { + assertions.accept(MockMvcWebTestClient.bindToController(new PersonController()).build() .get() - .uri("/person/Lee") + .uri(uri) .accept(MediaType.APPLICATION_JSON) .exchange() .expectStatus().isOk() .expectHeader().contentType(MediaType.APPLICATION_JSON) - .expectBody() - .jsonPath("$.name").isEqualTo("Lee") - .jsonPath("$.age").isEqualTo(42) - .jsonPath("$.age").value(equalTo(42)) - .jsonPath("$.age").value(Float.class, equalTo(42.0f)); + .expectBody()); } @RestController + @SuppressWarnings("unused") private static class PersonController { - @GetMapping("/person/{name}") + @GetMapping("/persons") + List getAll() { + return List.of(new Person("Rossen", 42), new Person("Juergen", 42), + new Person("Arjen", 42)); + } + + @GetMapping("/persons/{name}") Person get(@PathVariable String name) { - Person person = new Person(name); - person.setAge(42); - return person; + return new Person(name, 42); } } - @SuppressWarnings("unused") - private static class Person { - - @NotNull - private final String name; - - private int age; - - public Person(String name) { - this.name = name; - } - - public String getName() { - return this.name; - } - - public int getAge() { - return this.age; - } - - public void setAge(int age) { - this.age = age; - } - } + private record Person(@NotNull String name, int age) {} } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/JsonPathAssertionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/JsonPathAssertionTests.java index efcd967bc2..8c4b786692 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/JsonPathAssertionTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/resultmatches/JsonPathAssertionTests.java @@ -64,12 +64,12 @@ public class JsonPathAssertionTests { client.get().uri("/music/people") .exchange() .expectBody() - .jsonPath(composerByName, "Johann Sebastian Bach").exists() - .jsonPath(composerByName, "Johannes Brahms").exists() - .jsonPath(composerByName, "Edvard Grieg").exists() - .jsonPath(composerByName, "Robert Schumann").exists() - .jsonPath(performerByName, "Vladimir Ashkenazy").exists() - .jsonPath(performerByName, "Yehudi Menuhin").exists() + .jsonPath(composerByName.formatted("Johann Sebastian Bach")).exists() + .jsonPath(composerByName.formatted("Johannes Brahms")).exists() + .jsonPath(composerByName.formatted("Edvard Grieg")).exists() + .jsonPath(composerByName.formatted("Robert Schumann")).exists() + .jsonPath(performerByName.formatted("Vladimir Ashkenazy")).exists() + .jsonPath(performerByName.formatted("Yehudi Menuhin")).exists() .jsonPath("$.composers[0]").exists() .jsonPath("$.composers[1]").exists() .jsonPath("$.composers[2]").exists() @@ -117,16 +117,13 @@ public class JsonPathAssertionTests { @Test public void hamcrestMatcherWithParameterizedJsonPath() { - String composerName = "$.composers[%s].name"; - String performerName = "$.performers[%s].name"; - client.get().uri("/music/people") .exchange() .expectBody() - .jsonPath(composerName, 0).value(startsWith("Johann")) - .jsonPath(performerName, 0).value(endsWith("Ashkenazy")) - .jsonPath(performerName, 1).value(containsString("di Me")) - .jsonPath(composerName, 1).value(is(in(Arrays.asList("Johann Sebastian Bach", "Johannes Brahms")))); + .jsonPath("$.composers[0].name").value(startsWith("Johann")) + .jsonPath("$.performers[0].name").value(endsWith("Ashkenazy")) + .jsonPath("$.performers[1].name").value(containsString("di Me")) + .jsonPath("$.composers[1].name").value(is(in(Arrays.asList("Johann Sebastian Bach", "Johannes Brahms")))); }