Configure jsonpath MappingProvider in WebTestClient
This commit improves jsonpath support in WebTestClient by detecting a suitable json encoder/decoder that can be applied to assert more complex data structure. Closes gh-31653
This commit is contained in:
parent
9f8038963f
commit
e73bbd4ad3
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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.Consumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import com.jayway.jsonpath.Configuration;
|
||||||
|
import com.jayway.jsonpath.spi.mapper.MappingProvider;
|
||||||
import org.hamcrest.Matcher;
|
import org.hamcrest.Matcher;
|
||||||
import org.hamcrest.MatcherAssert;
|
import org.hamcrest.MatcherAssert;
|
||||||
import org.reactivestreams.Publisher;
|
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.ClientRequest;
|
||||||
import org.springframework.web.reactive.function.client.ClientResponse;
|
import org.springframework.web.reactive.function.client.ClientResponse;
|
||||||
import org.springframework.web.reactive.function.client.ExchangeFunction;
|
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.UriBuilder;
|
||||||
import org.springframework.web.util.UriBuilderFactory;
|
import org.springframework.web.util.UriBuilderFactory;
|
||||||
|
|
||||||
|
|
@ -72,6 +75,9 @@ class DefaultWebTestClient implements WebTestClient {
|
||||||
|
|
||||||
private final WiretapConnector wiretapConnector;
|
private final WiretapConnector wiretapConnector;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private final JsonEncoderDecoder jsonEncoderDecoder;
|
||||||
|
|
||||||
private final ExchangeFunction exchangeFunction;
|
private final ExchangeFunction exchangeFunction;
|
||||||
|
|
||||||
private final UriBuilderFactory uriBuilderFactory;
|
private final UriBuilderFactory uriBuilderFactory;
|
||||||
|
|
@ -91,13 +97,15 @@ class DefaultWebTestClient implements WebTestClient {
|
||||||
private final AtomicLong requestIndex = new AtomicLong();
|
private final AtomicLong requestIndex = new AtomicLong();
|
||||||
|
|
||||||
|
|
||||||
DefaultWebTestClient(ClientHttpConnector connector,
|
DefaultWebTestClient(ClientHttpConnector connector, ExchangeStrategies exchangeStrategies,
|
||||||
Function<ClientHttpConnector, ExchangeFunction> exchangeFactory, UriBuilderFactory uriBuilderFactory,
|
Function<ClientHttpConnector, ExchangeFunction> exchangeFactory, UriBuilderFactory uriBuilderFactory,
|
||||||
@Nullable HttpHeaders headers, @Nullable MultiValueMap<String, String> cookies,
|
@Nullable HttpHeaders headers, @Nullable MultiValueMap<String, String> cookies,
|
||||||
Consumer<EntityExchangeResult<?>> entityResultConsumer,
|
Consumer<EntityExchangeResult<?>> entityResultConsumer,
|
||||||
@Nullable Duration responseTimeout, DefaultWebTestClientBuilder clientBuilder) {
|
@Nullable Duration responseTimeout, DefaultWebTestClientBuilder clientBuilder) {
|
||||||
|
|
||||||
this.wiretapConnector = new WiretapConnector(connector);
|
this.wiretapConnector = new WiretapConnector(connector);
|
||||||
|
this.jsonEncoderDecoder = JsonEncoderDecoder.from(
|
||||||
|
exchangeStrategies.messageWriters(), exchangeStrategies.messageReaders());
|
||||||
this.exchangeFunction = exchangeFactory.apply(this.wiretapConnector);
|
this.exchangeFunction = exchangeFactory.apply(this.wiretapConnector);
|
||||||
this.uriBuilderFactory = uriBuilderFactory;
|
this.uriBuilderFactory = uriBuilderFactory;
|
||||||
this.defaultHeaders = headers;
|
this.defaultHeaders = headers;
|
||||||
|
|
@ -362,6 +370,7 @@ class DefaultWebTestClient implements WebTestClient {
|
||||||
this.requestId, this.uriTemplate, getResponseTimeout());
|
this.requestId, this.uriTemplate, getResponseTimeout());
|
||||||
|
|
||||||
return new DefaultResponseSpec(result, response,
|
return new DefaultResponseSpec(result, response,
|
||||||
|
DefaultWebTestClient.this.jsonEncoderDecoder,
|
||||||
DefaultWebTestClient.this.entityResultConsumer, getResponseTimeout());
|
DefaultWebTestClient.this.entityResultConsumer, getResponseTimeout());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -399,6 +408,9 @@ class DefaultWebTestClient implements WebTestClient {
|
||||||
|
|
||||||
private final ClientResponse response;
|
private final ClientResponse response;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private final JsonEncoderDecoder jsonEncoderDecoder;
|
||||||
|
|
||||||
private final Consumer<EntityExchangeResult<?>> entityResultConsumer;
|
private final Consumer<EntityExchangeResult<?>> entityResultConsumer;
|
||||||
|
|
||||||
private final Duration timeout;
|
private final Duration timeout;
|
||||||
|
|
@ -406,11 +418,13 @@ class DefaultWebTestClient implements WebTestClient {
|
||||||
|
|
||||||
DefaultResponseSpec(
|
DefaultResponseSpec(
|
||||||
ExchangeResult exchangeResult, ClientResponse response,
|
ExchangeResult exchangeResult, ClientResponse response,
|
||||||
|
@Nullable JsonEncoderDecoder jsonEncoderDecoder,
|
||||||
Consumer<EntityExchangeResult<?>> entityResultConsumer,
|
Consumer<EntityExchangeResult<?>> entityResultConsumer,
|
||||||
Duration timeout) {
|
Duration timeout) {
|
||||||
|
|
||||||
this.exchangeResult = exchangeResult;
|
this.exchangeResult = exchangeResult;
|
||||||
this.response = response;
|
this.response = response;
|
||||||
|
this.jsonEncoderDecoder = jsonEncoderDecoder;
|
||||||
this.entityResultConsumer = entityResultConsumer;
|
this.entityResultConsumer = entityResultConsumer;
|
||||||
this.timeout = timeout;
|
this.timeout = timeout;
|
||||||
}
|
}
|
||||||
|
|
@ -466,7 +480,7 @@ class DefaultWebTestClient implements WebTestClient {
|
||||||
ByteArrayResource resource = this.response.bodyToMono(ByteArrayResource.class).block(this.timeout);
|
ByteArrayResource resource = this.response.bodyToMono(ByteArrayResource.class).block(this.timeout);
|
||||||
byte[] body = (resource != null ? resource.getByteArray() : null);
|
byte[] body = (resource != null ? resource.getByteArray() : null);
|
||||||
EntityExchangeResult<byte[]> entityResult = initEntityExchangeResult(body);
|
EntityExchangeResult<byte[]> entityResult = initEntityExchangeResult(body);
|
||||||
return new DefaultBodyContentSpec(entityResult);
|
return new DefaultBodyContentSpec(entityResult, this.jsonEncoderDecoder);
|
||||||
}
|
}
|
||||||
|
|
||||||
private <B> EntityExchangeResult<B> initEntityExchangeResult(@Nullable B body) {
|
private <B> EntityExchangeResult<B> initEntityExchangeResult(@Nullable B body) {
|
||||||
|
|
@ -625,10 +639,14 @@ class DefaultWebTestClient implements WebTestClient {
|
||||||
|
|
||||||
private final EntityExchangeResult<byte[]> result;
|
private final EntityExchangeResult<byte[]> result;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private final JsonEncoderDecoder jsonEncoderDecoder;
|
||||||
|
|
||||||
private final boolean isEmpty;
|
private final boolean isEmpty;
|
||||||
|
|
||||||
DefaultBodyContentSpec(EntityExchangeResult<byte[]> result) {
|
DefaultBodyContentSpec(EntityExchangeResult<byte[]> result, @Nullable JsonEncoderDecoder jsonEncoderDecoder) {
|
||||||
this.result = result;
|
this.result = result;
|
||||||
|
this.jsonEncoderDecoder = jsonEncoderDecoder;
|
||||||
this.isEmpty = (result.getResponseBody() == null || result.getResponseBody().length == 0);
|
this.isEmpty = (result.getResponseBody() == null || result.getResponseBody().length == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -666,8 +684,16 @@ class DefaultWebTestClient implements WebTestClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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) {
|
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
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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) {
|
if (connectorToUse == null) {
|
||||||
connectorToUse = initConnector();
|
connectorToUse = initConnector();
|
||||||
}
|
}
|
||||||
|
ExchangeStrategies exchangeStrategies = initExchangeStrategies();
|
||||||
Function<ClientHttpConnector, ExchangeFunction> exchangeFactory = connector -> {
|
Function<ClientHttpConnector, ExchangeFunction> exchangeFactory = connector -> {
|
||||||
ExchangeFunction exchange = ExchangeFunctions.create(connector, initExchangeStrategies());
|
ExchangeFunction exchange = ExchangeFunctions.create(connector, exchangeStrategies);
|
||||||
if (CollectionUtils.isEmpty(this.filters)) {
|
if (CollectionUtils.isEmpty(this.filters)) {
|
||||||
return exchange;
|
return exchange;
|
||||||
}
|
}
|
||||||
|
|
@ -305,7 +306,7 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder {
|
||||||
.orElse(exchange);
|
.orElse(exchange);
|
||||||
|
|
||||||
};
|
};
|
||||||
return new DefaultWebTestClient(connectorToUse, exchangeFactory, initUriBuilderFactory(),
|
return new DefaultWebTestClient(connectorToUse, exchangeStrategies, exchangeFactory, initUriBuilderFactory(),
|
||||||
this.defaultHeaders != null ? HttpHeaders.readOnlyHttpHeaders(this.defaultHeaders) : null,
|
this.defaultHeaders != null ? HttpHeaders.readOnlyHttpHeaders(this.defaultHeaders) : null,
|
||||||
this.defaultCookies != null ? CollectionUtils.unmodifiableMultiValueMap(this.defaultCookies) : null,
|
this.defaultCookies != null ? CollectionUtils.unmodifiableMultiValueMap(this.defaultCookies) : null,
|
||||||
this.entityResultConsumer, this.responseTimeout, new DefaultWebTestClientBuilder(this));
|
this.entityResultConsumer, this.responseTimeout, new DefaultWebTestClientBuilder(this));
|
||||||
|
|
|
||||||
|
|
@ -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> T map(Object source, Class<T> targetType, Configuration configuration) {
|
||||||
|
return mapToTargetType(source, ResolvableType.forClass(targetType));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public <T> T map(Object source, TypeRef<T> targetType, Configuration configuration) {
|
||||||
|
return mapToTargetType(source, ResolvableType.forType(targetType.getType()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Nullable
|
||||||
|
private <T> T mapToTargetType(Object source, ResolvableType targetType) {
|
||||||
|
DataBufferFactory bufferFactory = DefaultDataBufferFactory.sharedInstance;
|
||||||
|
MimeType mimeType = MimeTypeUtils.APPLICATION_JSON;
|
||||||
|
Map<String, Object> hints = Collections.emptyMap();
|
||||||
|
|
||||||
|
DataBuffer buffer = ((Encoder<T>) this.encoder).encodeValue(
|
||||||
|
(T) source, bufferFactory, ResolvableType.forInstance(source), mimeType, hints);
|
||||||
|
|
||||||
|
return ((Decoder<T>) this.decoder).decode(buffer, targetType, mimeType, hints);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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<HttpMessageWriter<?>> messageWriters,
|
||||||
|
Collection<HttpMessageReader<?>> 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<HttpMessageWriter<?>> writers) {
|
||||||
|
return findJsonEncoder(writers.stream()
|
||||||
|
.filter(writer -> writer instanceof EncoderHttpMessageWriter)
|
||||||
|
.map(writer -> ((EncoderHttpMessageWriter<?>) writer).getEncoder()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static Encoder<?> findJsonEncoder(Stream<Encoder<?>> 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<HttpMessageReader<?>> readers) {
|
||||||
|
return findJsonDecoder(readers.stream()
|
||||||
|
.filter(reader -> reader instanceof DecoderHttpMessageReader)
|
||||||
|
.map(reader -> ((DecoderHttpMessageReader<?>) reader).getDecoder()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static Decoder<?> findJsonDecoder(Stream<Decoder<?>> decoderStream) {
|
||||||
|
return decoderStream
|
||||||
|
.filter(decoder -> decoder.canDecode(MAP_TYPE, MediaType.APPLICATION_JSON))
|
||||||
|
.findFirst()
|
||||||
|
.orElse(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -18,8 +18,10 @@ package org.springframework.test.web.reactive.server;
|
||||||
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import com.jayway.jsonpath.Configuration;
|
||||||
import org.hamcrest.Matcher;
|
import org.hamcrest.Matcher;
|
||||||
|
|
||||||
|
import org.springframework.core.ParameterizedTypeReference;
|
||||||
import org.springframework.lang.Nullable;
|
import org.springframework.lang.Nullable;
|
||||||
import org.springframework.test.util.JsonPathExpectationsHelper;
|
import org.springframework.test.util.JsonPathExpectationsHelper;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
|
@ -28,6 +30,7 @@ import org.springframework.util.Assert;
|
||||||
* <a href="https://github.com/jayway/JsonPath">JsonPath</a> assertions.
|
* <a href="https://github.com/jayway/JsonPath">JsonPath</a> assertions.
|
||||||
*
|
*
|
||||||
* @author Rossen Stoyanchev
|
* @author Rossen Stoyanchev
|
||||||
|
* @author Stephane Nicoll
|
||||||
* @since 5.0
|
* @since 5.0
|
||||||
* @see <a href="https://github.com/jayway/JsonPath">https://github.com/jayway/JsonPath</a>
|
* @see <a href="https://github.com/jayway/JsonPath">https://github.com/jayway/JsonPath</a>
|
||||||
* @see JsonPathExpectationsHelper
|
* @see JsonPathExpectationsHelper
|
||||||
|
|
@ -41,11 +44,12 @@ public class JsonPathAssertions {
|
||||||
private final JsonPathExpectationsHelper pathHelper;
|
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");
|
Assert.hasText(expression, "expression must not be null or empty");
|
||||||
this.bodySpec = spec;
|
this.bodySpec = spec;
|
||||||
this.content = content;
|
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;
|
return this.bodySpec;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delegates to {@link JsonPathExpectationsHelper#assertValue(String, Matcher, ParameterizedTypeReference)}.
|
||||||
|
* @since 6.2
|
||||||
|
*/
|
||||||
|
public <T> WebTestClient.BodyContentSpec value(ParameterizedTypeReference<T> targetType, Matcher<? super T> matcher) {
|
||||||
|
this.pathHelper.assertValue(this.content, matcher, targetType);
|
||||||
|
return this.bodySpec;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Consume the result of the JSONPath evaluation.
|
* Consume the result of the JSONPath evaluation.
|
||||||
* @since 5.1
|
* @since 5.1
|
||||||
|
|
@ -199,6 +212,16 @@ public class JsonPathAssertions {
|
||||||
return value(targetType, consumer);
|
return value(targetType, consumer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consume the result of the JSONPath evaluation and provide a parameterized type.
|
||||||
|
* @since 6.2
|
||||||
|
*/
|
||||||
|
public <T> WebTestClient.BodyContentSpec value(ParameterizedTypeReference<T> targetType, Consumer<T> consumer) {
|
||||||
|
T value = this.pathHelper.evaluateJsonPath(this.content, targetType);
|
||||||
|
consumer.accept(value);
|
||||||
|
return this.bodySpec;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(@Nullable Object obj) {
|
public boolean equals(@Nullable Object obj) {
|
||||||
throw new AssertionError("Object#equals is disabled " +
|
throw new AssertionError("Object#equals is disabled " +
|
||||||
|
|
|
||||||
|
|
@ -1035,6 +1035,15 @@ public interface WebTestClient {
|
||||||
*/
|
*/
|
||||||
BodyContentSpec xml(String expectedXml);
|
BodyContentSpec xml(String expectedXml);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Access to response body assertions using a
|
||||||
|
* <a href="https://github.com/jayway/JsonPath">JsonPath</a> 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
|
* Access to response body assertions using a
|
||||||
* <a href="https://github.com/jayway/JsonPath">JsonPath</a> expression
|
* <a href="https://github.com/jayway/JsonPath">JsonPath</a> expression
|
||||||
|
|
@ -1043,7 +1052,9 @@ public interface WebTestClient {
|
||||||
* formatting specifiers as defined in {@link String#format}.
|
* formatting specifiers as defined in {@link String#format}.
|
||||||
* @param expression the JsonPath expression
|
* @param expression the JsonPath expression
|
||||||
* @param args arguments to parameterize the 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);
|
JsonPathAssertions jsonPath(String expression, Object... args);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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> data = this.mappingProvider.map(jsonData, new TypeRef<List<Data>>() {}, Configuration.defaultConfiguration());
|
||||||
|
assertThat(data).containsExactly(new Data("first", 1), new Data("second", 2), new Data("third", 3));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> jsonData(String name, int counter) {
|
||||||
|
return Map.of("name", name, "counter", counter);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
record Data(String name, int counter) {}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -16,15 +16,21 @@
|
||||||
|
|
||||||
package org.springframework.test.web.servlet.samples.client.standalone;
|
package org.springframework.test.web.servlet.samples.client.standalone;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.core.ParameterizedTypeReference;
|
||||||
import org.springframework.http.MediaType;
|
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.test.web.servlet.client.MockMvcWebTestClient;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.hamcrest.Matchers.equalTo;
|
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}.
|
* {@link org.springframework.test.web.servlet.samples.standalone.ResponseBodyTests}.
|
||||||
*
|
*
|
||||||
* @author Rossen Stoyanchev
|
* @author Rossen Stoyanchev
|
||||||
|
* @author Stephane Nicoll
|
||||||
*/
|
*/
|
||||||
class ResponseBodyTests {
|
class ResponseBodyTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void json() {
|
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<List<Person>>() {},
|
||||||
|
candidate -> assertThat(candidate).hasSize(3).extracting(Person::name)
|
||||||
|
.containsExactly("Rossen", "Juergen", "Arjen")));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void execute(String uri, Consumer<BodyContentSpec> assertions) {
|
||||||
|
assertions.accept(MockMvcWebTestClient.bindToController(new PersonController()).build()
|
||||||
.get()
|
.get()
|
||||||
.uri("/person/Lee")
|
.uri(uri)
|
||||||
.accept(MediaType.APPLICATION_JSON)
|
.accept(MediaType.APPLICATION_JSON)
|
||||||
.exchange()
|
.exchange()
|
||||||
.expectStatus().isOk()
|
.expectStatus().isOk()
|
||||||
.expectHeader().contentType(MediaType.APPLICATION_JSON)
|
.expectHeader().contentType(MediaType.APPLICATION_JSON)
|
||||||
.expectBody()
|
.expectBody());
|
||||||
.jsonPath("$.name").isEqualTo("Lee")
|
|
||||||
.jsonPath("$.age").isEqualTo(42)
|
|
||||||
.jsonPath("$.age").value(equalTo(42))
|
|
||||||
.jsonPath("$.age").value(Float.class, equalTo(42.0f));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
|
@SuppressWarnings("unused")
|
||||||
private static class PersonController {
|
private static class PersonController {
|
||||||
|
|
||||||
@GetMapping("/person/{name}")
|
@GetMapping("/persons")
|
||||||
|
List<Person> 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 get(@PathVariable String name) {
|
||||||
Person person = new Person(name);
|
return new Person(name, 42);
|
||||||
person.setAge(42);
|
|
||||||
return person;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
private record Person(@NotNull String name, int age) {}
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,12 +64,12 @@ public class JsonPathAssertionTests {
|
||||||
client.get().uri("/music/people")
|
client.get().uri("/music/people")
|
||||||
.exchange()
|
.exchange()
|
||||||
.expectBody()
|
.expectBody()
|
||||||
.jsonPath(composerByName, "Johann Sebastian Bach").exists()
|
.jsonPath(composerByName.formatted("Johann Sebastian Bach")).exists()
|
||||||
.jsonPath(composerByName, "Johannes Brahms").exists()
|
.jsonPath(composerByName.formatted("Johannes Brahms")).exists()
|
||||||
.jsonPath(composerByName, "Edvard Grieg").exists()
|
.jsonPath(composerByName.formatted("Edvard Grieg")).exists()
|
||||||
.jsonPath(composerByName, "Robert Schumann").exists()
|
.jsonPath(composerByName.formatted("Robert Schumann")).exists()
|
||||||
.jsonPath(performerByName, "Vladimir Ashkenazy").exists()
|
.jsonPath(performerByName.formatted("Vladimir Ashkenazy")).exists()
|
||||||
.jsonPath(performerByName, "Yehudi Menuhin").exists()
|
.jsonPath(performerByName.formatted("Yehudi Menuhin")).exists()
|
||||||
.jsonPath("$.composers[0]").exists()
|
.jsonPath("$.composers[0]").exists()
|
||||||
.jsonPath("$.composers[1]").exists()
|
.jsonPath("$.composers[1]").exists()
|
||||||
.jsonPath("$.composers[2]").exists()
|
.jsonPath("$.composers[2]").exists()
|
||||||
|
|
@ -117,16 +117,13 @@ public class JsonPathAssertionTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void hamcrestMatcherWithParameterizedJsonPath() {
|
public void hamcrestMatcherWithParameterizedJsonPath() {
|
||||||
String composerName = "$.composers[%s].name";
|
|
||||||
String performerName = "$.performers[%s].name";
|
|
||||||
|
|
||||||
client.get().uri("/music/people")
|
client.get().uri("/music/people")
|
||||||
.exchange()
|
.exchange()
|
||||||
.expectBody()
|
.expectBody()
|
||||||
.jsonPath(composerName, 0).value(startsWith("Johann"))
|
.jsonPath("$.composers[0].name").value(startsWith("Johann"))
|
||||||
.jsonPath(performerName, 0).value(endsWith("Ashkenazy"))
|
.jsonPath("$.performers[0].name").value(endsWith("Ashkenazy"))
|
||||||
.jsonPath(performerName, 1).value(containsString("di Me"))
|
.jsonPath("$.performers[1].name").value(containsString("di Me"))
|
||||||
.jsonPath(composerName, 1).value(is(in(Arrays.asList("Johann Sebastian Bach", "Johannes Brahms"))));
|
.jsonPath("$.composers[1].name").value(is(in(Arrays.asList("Johann Sebastian Bach", "Johannes Brahms"))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue