Skip to content

Commit 7445f54

Browse files
committed
AssertJ support for WebTestClient
Closes gh-35737
1 parent cd86902 commit 7445f54

File tree

12 files changed

+995
-5
lines changed

12 files changed

+995
-5
lines changed

spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,10 @@ class DefaultWebTestClient implements WebTestClient {
111111
Consumer<EntityExchangeResult<?>> entityResultConsumer,
112112
@Nullable Duration responseTimeout, DefaultWebTestClientBuilder clientBuilder) {
113113

114-
this.wiretapConnector = new WiretapConnector(connector);
115114
this.jsonEncoderDecoder = JsonEncoderDecoder.from(
116115
exchangeStrategies.messageWriters(), exchangeStrategies.messageReaders());
116+
117+
this.wiretapConnector = new WiretapConnector(connector, this.jsonEncoderDecoder);
117118
this.exchangeFunction = exchangeFactory.apply(this.wiretapConnector);
118119
this.uriBuilderFactory = uriBuilderFactory;
119120
this.defaultHeaders = headers;

spring-test/src/main/java/org/springframework/test/web/reactive/server/ExchangeResult.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.springframework.http.ResponseCookie;
3737
import org.springframework.http.client.reactive.ClientHttpRequest;
3838
import org.springframework.http.client.reactive.ClientHttpResponse;
39+
import org.springframework.test.json.JsonConverterDelegate;
3940
import org.springframework.util.Assert;
4041
import org.springframework.util.MultiValueMap;
4142

@@ -78,6 +79,8 @@ public class ExchangeResult {
7879

7980
private final @Nullable Object mockServerResult;
8081

82+
private final @Nullable JsonConverterDelegate converterDelegate;
83+
8184
/** Ensure single logging, for example, for expectAll. */
8285
private boolean diagnosticsLogged;
8386

@@ -93,10 +96,11 @@ public class ExchangeResult {
9396
* @param timeout how long to wait for content to materialize
9497
* @param uriTemplate the URI template used to set up the request, if any
9598
* @param serverResult the result of a mock server exchange if applicable.
99+
* @param converterDelegate for JSON decoding in AssertJ support
96100
*/
97101
ExchangeResult(ClientHttpRequest request, ClientHttpResponse response,
98102
Mono<byte[]> requestBody, Mono<byte[]> responseBody, Duration timeout, @Nullable String uriTemplate,
99-
@Nullable Object serverResult) {
103+
@Nullable Object serverResult, @Nullable JsonConverterDelegate converterDelegate) {
100104

101105
Assert.notNull(request, "ClientHttpRequest is required");
102106
Assert.notNull(response, "ClientHttpResponse is required");
@@ -110,6 +114,7 @@ public class ExchangeResult {
110114
this.timeout = timeout;
111115
this.uriTemplate = uriTemplate;
112116
this.mockServerResult = serverResult;
117+
this.converterDelegate = converterDelegate;
113118
}
114119

115120
/**
@@ -123,6 +128,7 @@ public class ExchangeResult {
123128
this.timeout = other.timeout;
124129
this.uriTemplate = other.uriTemplate;
125130
this.mockServerResult = other.mockServerResult;
131+
this.converterDelegate = other.converterDelegate;
126132
this.diagnosticsLogged = other.diagnosticsLogged;
127133
}
128134

@@ -206,6 +212,15 @@ public MultiValueMap<String, ResponseCookie> getResponseCookies() {
206212
return this.mockServerResult;
207213
}
208214

215+
/**
216+
* Return a {@link JsonConverterDelegate} based on the configured codecs.
217+
* Mainly for internal use from AssertJ support classes.
218+
* @since 7.0
219+
*/
220+
public @Nullable JsonConverterDelegate getJsonConverterDelegate() {
221+
return this.converterDelegate;
222+
}
223+
209224
/**
210225
* Execute the given Runnable, catch any {@link AssertionError}, log details
211226
* about the request and response at ERROR level under the class log

spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonEncoderDecoder.java

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.test.web.reactive.server;
1818

19+
import java.nio.charset.StandardCharsets;
1920
import java.util.Collection;
2021
import java.util.Map;
2122
import java.util.stream.Stream;
@@ -25,11 +26,17 @@
2526
import org.springframework.core.ResolvableType;
2627
import org.springframework.core.codec.Decoder;
2728
import org.springframework.core.codec.Encoder;
29+
import org.springframework.core.io.buffer.DataBuffer;
30+
import org.springframework.core.io.buffer.DataBufferUtils;
31+
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
2832
import org.springframework.http.MediaType;
2933
import org.springframework.http.codec.DecoderHttpMessageReader;
3034
import org.springframework.http.codec.EncoderHttpMessageWriter;
3135
import org.springframework.http.codec.HttpMessageReader;
3236
import org.springframework.http.codec.HttpMessageWriter;
37+
import org.springframework.test.json.JsonConverterDelegate;
38+
import org.springframework.util.Assert;
39+
import org.springframework.util.MimeTypeUtils;
3340

3441
/**
3542
* {@link Encoder} and {@link Decoder} that is able to encode and decode
@@ -49,6 +56,14 @@ record JsonEncoderDecoder(Encoder<?> encoder, Decoder<?> decoder) {
4956
private static final ResolvableType MAP_TYPE = ResolvableType.forClass(Map.class);
5057

5158

59+
/**
60+
* Return a {@link JsonConverterDelegate} that uses the encoder and decoder.
61+
*/
62+
public JsonConverterDelegate createJsonConverterDelegate() {
63+
return new CodecsJsonConverterDelegate();
64+
}
65+
66+
5267
/**
5368
* Create a {@link JsonEncoderDecoder} instance based on the specified
5469
* infrastructure.
@@ -107,4 +122,34 @@ record JsonEncoderDecoder(Encoder<?> encoder, Decoder<?> decoder) {
107122
.orElse(null);
108123
}
109124

125+
126+
/**
127+
* Implementation that delegates to the contained Encoder and Decoder.
128+
*/
129+
private class CodecsJsonConverterDelegate implements JsonConverterDelegate {
130+
131+
@SuppressWarnings("unchecked")
132+
@Override
133+
public <T> T read(String content, ResolvableType targetType) {
134+
byte[] bytes = content.getBytes(StandardCharsets.UTF_8);
135+
DataBuffer buffer = DefaultDataBufferFactory.sharedInstance.wrap(bytes);
136+
Object value = decoder().decode(buffer, targetType, MediaType.APPLICATION_JSON, null);
137+
Assert.state(value != null, () -> "Could not decode JSON content: " + content);
138+
return (T) value;
139+
}
140+
141+
@SuppressWarnings("unchecked")
142+
@Override
143+
public <T> T map(Object value, ResolvableType targetType) {
144+
DataBuffer buffer = ((Encoder<T>) encoder()).encodeValue((T) value,
145+
DefaultDataBufferFactory.sharedInstance, targetType, MimeTypeUtils.APPLICATION_JSON, null);
146+
try {
147+
return read(buffer.toString(StandardCharsets.UTF_8), targetType);
148+
}
149+
finally {
150+
DataBufferUtils.release(buffer);
151+
}
152+
}
153+
}
154+
110155
}

spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapConnector.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import org.springframework.http.client.reactive.ClientHttpRequestDecorator;
3939
import org.springframework.http.client.reactive.ClientHttpResponse;
4040
import org.springframework.http.client.reactive.ClientHttpResponseDecorator;
41+
import org.springframework.test.json.JsonConverterDelegate;
4142
import org.springframework.util.Assert;
4243

4344
/**
@@ -53,11 +54,14 @@ class WiretapConnector implements ClientHttpConnector {
5354

5455
private final ClientHttpConnector delegate;
5556

57+
private final @Nullable JsonConverterDelegate converterDelegate;
58+
5659
private final Map<String, ClientExchangeInfo> exchanges = new ConcurrentHashMap<>();
5760

5861

59-
WiretapConnector(ClientHttpConnector delegate) {
62+
WiretapConnector(ClientHttpConnector delegate, @Nullable JsonEncoderDecoder encoderDecoder) {
6063
this.delegate = delegate;
64+
this.converterDelegate = (encoderDecoder != null ? encoderDecoder.createJsonConverterDelegate() : null);
6165
}
6266

6367

@@ -96,7 +100,8 @@ ExchangeResult getExchangeResult(String requestId, @Nullable String uriTemplate,
96100
clientInfo.getRequest().getRecorder().getContent(),
97101
clientInfo.getResponse().getRecorder().getContent(),
98102
timeout, uriTemplate,
99-
clientInfo.getResponse().getMockServerResult());
103+
clientInfo.getResponse().getMockServerResult(),
104+
this.converterDelegate);
100105
}
101106

102107

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 2002-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.web.reactive.server.assertj;
18+
19+
20+
import org.springframework.test.web.reactive.server.ExchangeResult;
21+
22+
/**
23+
* Default implementation of {@link WebTestClientResponse}.
24+
*
25+
* @author Rossen Stoyanchev
26+
* @since 7.0
27+
*/
28+
final class DefaultWebTestClientResponse implements WebTestClientResponse {
29+
30+
private final ExchangeResult exchangeResult;
31+
32+
33+
DefaultWebTestClientResponse(ExchangeResult exchangeResult) {
34+
this.exchangeResult = exchangeResult;
35+
}
36+
37+
38+
@Override
39+
public ExchangeResult getExchangeResult() {
40+
return this.exchangeResult;
41+
}
42+
43+
/**
44+
* Use AssertJ's {@link org.assertj.core.api.Assertions#assertThat assertThat} instead.
45+
*/
46+
@Override
47+
public WebTestClientResponseAssert assertThat() {
48+
return new WebTestClientResponseAssert(this);
49+
}
50+
51+
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
/*
2+
* Copyright 2002-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.web.reactive.server.assertj;
18+
19+
import java.time.Duration;
20+
import java.util.LinkedHashMap;
21+
import java.util.Map;
22+
import java.util.function.Consumer;
23+
24+
import org.assertj.core.api.AbstractMapAssert;
25+
import org.assertj.core.api.Assertions;
26+
27+
import org.springframework.http.ResponseCookie;
28+
29+
/**
30+
* AssertJ {@linkplain org.assertj.core.api.Assert assertions} that can be applied
31+
* to {@link ResponseCookie cookies}.
32+
*
33+
* @author Rossen Stoyanchev
34+
* @since 7.0
35+
*/
36+
public class ResponseCookieMapAssert
37+
extends AbstractMapAssert<ResponseCookieMapAssert, Map<String, ResponseCookie>, String, ResponseCookie> {
38+
39+
40+
public ResponseCookieMapAssert(ResponseCookie[] actual) {
41+
super(toMap(actual), ResponseCookieMapAssert.class);
42+
as("Cookies");
43+
}
44+
45+
private static Map<String, ResponseCookie> toMap(ResponseCookie[] cookies) {
46+
Map<String, ResponseCookie> map = new LinkedHashMap<>();
47+
for (ResponseCookie cookie : cookies) {
48+
map.putIfAbsent(cookie.getName(), cookie);
49+
}
50+
return map;
51+
}
52+
53+
54+
/**
55+
* Verify that the actual cookies contain a cookie with the given {@code name}.
56+
* @param name the name of an expected cookie
57+
* @see #containsKey
58+
*/
59+
public ResponseCookieMapAssert containsCookie(String name) {
60+
return containsKey(name);
61+
}
62+
63+
/**
64+
* Verify that the actual cookies contain cookies with the given {@code names}.
65+
* @param names the names of expected cookies
66+
* @see #containsKeys
67+
*/
68+
public ResponseCookieMapAssert containsCookies(String... names) {
69+
return containsKeys(names);
70+
}
71+
72+
/**
73+
* Verify that the actual cookies do not contain a cookie with the given
74+
* {@code name}.
75+
* @param name the name of a cookie that should not be present
76+
* @see #doesNotContainKey
77+
*/
78+
public ResponseCookieMapAssert doesNotContainCookie(String name) {
79+
return doesNotContainKey(name);
80+
}
81+
82+
/**
83+
* Verify that the actual cookies do not contain any cookies with the given
84+
* {@code names}.
85+
* @param names the names of cookies that should not be present
86+
* @see #doesNotContainKeys
87+
*/
88+
public ResponseCookieMapAssert doesNotContainCookies(String... names) {
89+
return doesNotContainKeys(names);
90+
}
91+
92+
/**
93+
* Verify that the actual cookies contain a cookie with the given {@code name}
94+
* that satisfies the given {@code cookieRequirements}.
95+
* @param name the name of an expected cookie
96+
* @param cookieRequirements the requirements for the cookie
97+
*/
98+
public ResponseCookieMapAssert hasCookieSatisfying(String name, Consumer<ResponseCookie> cookieRequirements) {
99+
return hasEntrySatisfying(name, cookieRequirements);
100+
}
101+
102+
/**
103+
* Verify that the actual cookies contain a cookie with the given {@code name}
104+
* whose {@linkplain ResponseCookie#getValue() value} is equal to the given one.
105+
* @param name the name of the cookie
106+
* @param expected the expected value of the cookie
107+
*/
108+
public ResponseCookieMapAssert hasValue(String name, String expected) {
109+
return hasCookieSatisfying(name, cookie -> Assertions.assertThat(cookie.getValue()).isEqualTo(expected));
110+
}
111+
112+
/**
113+
* Verify that the actual cookies contain a cookie with the given {@code name}
114+
* whose {@linkplain ResponseCookie#getMaxAge() max age} is equal to the given one.
115+
* @param name the name of the cookie
116+
* @param expected the expected max age of the cookie
117+
*/
118+
public ResponseCookieMapAssert hasMaxAge(String name, Duration expected) {
119+
return hasCookieSatisfying(name, cookie -> Assertions.assertThat(cookie.getMaxAge()).isEqualTo(expected));
120+
}
121+
122+
/**
123+
* Verify that the actual cookies contain a cookie with the given {@code name}
124+
* whose {@linkplain ResponseCookie#getPath() path} is equal to the given one.
125+
* @param name the name of the cookie
126+
* @param expected the expected path of the cookie
127+
*/
128+
public ResponseCookieMapAssert hasPath(String name, String expected) {
129+
return hasCookieSatisfying(name, cookie -> Assertions.assertThat(cookie.getPath()).isEqualTo(expected));
130+
}
131+
132+
/**
133+
* Verify that the actual cookies contain a cookie with the given {@code name}
134+
* whose {@linkplain ResponseCookie#getDomain() domain} is equal to the given one.
135+
* @param name the name of the cookie
136+
* @param expected the expected domain of the cookie
137+
*/
138+
public ResponseCookieMapAssert hasDomain(String name, String expected) {
139+
return hasCookieSatisfying(name, cookie -> Assertions.assertThat(cookie.getDomain()).isEqualTo(expected));
140+
}
141+
142+
/**
143+
* Verify that the actual cookies contain a cookie with the given {@code name}
144+
* whose {@linkplain ResponseCookie#isSecure() secure flag} is equal to the give one.
145+
* @param name the name of the cookie
146+
* @param expected whether the cookie is secure
147+
*/
148+
public ResponseCookieMapAssert isSecure(String name, boolean expected) {
149+
return hasCookieSatisfying(name, cookie -> Assertions.assertThat(cookie.isSecure()).isEqualTo(expected));
150+
}
151+
152+
/**
153+
* Verify that the actual cookies contain a cookie with the given {@code name}
154+
* whose {@linkplain ResponseCookie#isHttpOnly() http only flag} is equal to the given
155+
* one.
156+
* @param name the name of the cookie
157+
* @param expected whether the cookie is http only
158+
*/
159+
public ResponseCookieMapAssert isHttpOnly(String name, boolean expected) {
160+
return hasCookieSatisfying(name, cookie -> Assertions.assertThat(cookie.isHttpOnly()).isEqualTo(expected));
161+
}
162+
163+
}

0 commit comments

Comments
 (0)