Skip to content

Commit 0c6f5d7

Browse files
committed
HttpHeaders are no longer a MultiValueMap
This change removes the `MultiValueMap` nature of `HttpHeaders`, since it inherits APIs that do not align well with underlying server implementations. Notably, methods that allows to iterate over the whole collection of headers are susceptible to artificially introduced duplicates when multiple casings are used for a given header, depending on the underlying implementation. This change includes a dedicated key set implementation to support iterator-based removal, and either keeps map method implementations that are relevant or introduces header-focused methods that have a similar responsibility (like `hasHeaderValues(String, List)` and `containsHeaderValue(String, String)`). In order to nudge users away from using an HttpHeaders as a Map, the `asSingleValueMap` view is deprecated. In order to offer an escape hatch to users that do make use of the `MultiValueMap` API, a similar `asMultiValueMap` view is introduced but is immediately marked as deprecated. This change also adds map-like but header-focused assertions to `HttpHeadersAssert`, since it cannot extend `AbstractMapAssert` anymore. Closes gh-33913
1 parent 1e0ef99 commit 0c6f5d7

File tree

100 files changed

+1101
-493
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

100 files changed

+1101
-493
lines changed

spring-test/src/main/java/org/springframework/mock/http/server/reactive/MockServerHttpRequest.java

+15
Original file line numberDiff line numberDiff line change
@@ -298,9 +298,17 @@ public interface BaseBuilder<B extends BaseBuilder<B>> {
298298
/**
299299
* Add the given header values.
300300
* @param headers the header values
301+
* @deprecated Use {@link #headers(HttpHeaders)}
301302
*/
303+
@Deprecated
302304
B headers(MultiValueMap<String, String> headers);
303305

306+
/**
307+
* Add the given header values.
308+
* @param headers the header values
309+
*/
310+
B headers(HttpHeaders headers);
311+
304312
/**
305313
* Set the list of acceptable {@linkplain MediaType media types}, as
306314
* specified by the {@code Accept} header.
@@ -484,11 +492,18 @@ public BodyBuilder header(String headerName, String... headerValues) {
484492
}
485493

486494
@Override
495+
@Deprecated
487496
public BodyBuilder headers(MultiValueMap<String, String> headers) {
488497
this.headers.putAll(headers);
489498
return this;
490499
}
491500

501+
@Override
502+
public BodyBuilder headers(HttpHeaders headers) {
503+
this.headers.putAll(headers);
504+
return this;
505+
}
506+
492507
@Override
493508
public BodyBuilder accept(MediaType... acceptableMediaTypes) {
494509
this.headers.setAccept(Arrays.asList(acceptableMediaTypes));

spring-test/src/main/java/org/springframework/mock/web/MockPart.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ public Collection<String> getHeaders(String name) {
129129

130130
@Override
131131
public Collection<String> getHeaderNames() {
132-
return this.headers.keySet();
132+
return this.headers.headerNames();
133133
}
134134

135135
/**

spring-test/src/main/java/org/springframework/test/http/HttpHeadersAssert.java

+190-13
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,16 @@
2020
import java.time.ZoneId;
2121
import java.time.ZonedDateTime;
2222
import java.time.temporal.ChronoUnit;
23+
import java.util.Arrays;
24+
import java.util.Collection;
2325
import java.util.List;
2426

25-
import org.assertj.core.api.AbstractMapAssert;
27+
import org.assertj.core.api.AbstractCollectionAssert;
28+
import org.assertj.core.api.AbstractObjectAssert;
2629
import org.assertj.core.api.Assertions;
30+
import org.assertj.core.api.ObjectAssert;
31+
import org.assertj.core.presentation.Representation;
32+
import org.assertj.core.presentation.StandardRepresentation;
2733

2834
import org.springframework.http.HttpHeaders;
2935

@@ -34,54 +40,87 @@
3440
* @author Stephane Nicoll
3541
* @since 6.2
3642
*/
37-
public class HttpHeadersAssert extends AbstractMapAssert<HttpHeadersAssert, HttpHeaders, String, List<String>> {
43+
public class HttpHeadersAssert extends AbstractObjectAssert<HttpHeadersAssert, HttpHeaders> {
3844

3945
private static final ZoneId GMT = ZoneId.of("GMT");
4046

47+
private final AbstractCollectionAssert<?, Collection<? extends String>, String, ObjectAssert<String>> namesAssert;
48+
4149

4250
public HttpHeadersAssert(HttpHeaders actual) {
4351
super(actual, HttpHeadersAssert.class);
4452
as("HTTP headers");
53+
withRepresentation(new Representation() {
54+
@Override
55+
public String toStringOf(Object object) {
56+
if (object instanceof HttpHeaders headers) {
57+
return headers.toString();
58+
}
59+
return StandardRepresentation.STANDARD_REPRESENTATION.toStringOf(object);
60+
}
61+
});
62+
this.namesAssert = Assertions.assertThat(actual.headerNames())
63+
.as("HTTP header names");
4564
}
4665

4766
/**
4867
* Verify that the actual HTTP headers contain a header with the given
4968
* {@code name}.
5069
* @param name the name of an expected HTTP header
51-
* @see #containsKey
5270
*/
5371
public HttpHeadersAssert containsHeader(String name) {
54-
return containsKey(name);
72+
this.namesAssert
73+
.as("check headers contain HTTP header '%s'", name)
74+
.contains(name);
75+
return this.myself;
5576
}
5677

5778
/**
5879
* Verify that the actual HTTP headers contain the headers with the given
5980
* {@code names}.
6081
* @param names the names of expected HTTP headers
61-
* @see #containsKeys
6282
*/
6383
public HttpHeadersAssert containsHeaders(String... names) {
64-
return containsKeys(names);
84+
this.namesAssert
85+
.as("check headers contain HTTP headers '%s'", Arrays.toString(names))
86+
.contains(names);
87+
return this.myself;
88+
}
89+
90+
/**
91+
* Verify that the actual HTTP headers contain only the headers with the
92+
* given {@code names}, in any order and in a case-insensitive manner.
93+
* @param names the names of expected HTTP headers
94+
*/
95+
public HttpHeadersAssert containsOnlyHeaders(String... names) {
96+
this.namesAssert
97+
.as("check headers contains only HTTP headers '%s'", Arrays.toString(names))
98+
.containsOnly(names);
99+
return this.myself;
65100
}
66101

67102
/**
68103
* Verify that the actual HTTP headers do not contain a header with the
69104
* given {@code name}.
70105
* @param name the name of an HTTP header that should not be present
71-
* @see #doesNotContainKey
72106
*/
73107
public HttpHeadersAssert doesNotContainHeader(String name) {
74-
return doesNotContainKey(name);
108+
this.namesAssert
109+
.as("check headers does not contain HTTP header '%s'", name)
110+
.doesNotContain(name);
111+
return this.myself;
75112
}
76113

77114
/**
78115
* Verify that the actual HTTP headers do not contain any of the headers
79116
* with the given {@code names}.
80117
* @param names the names of HTTP headers that should not be present
81-
* @see #doesNotContainKeys
82118
*/
83119
public HttpHeadersAssert doesNotContainsHeaders(String... names) {
84-
return doesNotContainKeys(names);
120+
this.namesAssert
121+
.as("check headers does not contain HTTP headers '%s'", Arrays.toString(names))
122+
.doesNotContain(names);
123+
return this.myself;
85124
}
86125

87126
/**
@@ -91,7 +130,7 @@ public HttpHeadersAssert doesNotContainsHeaders(String... names) {
91130
* @param value the expected value of the header
92131
*/
93132
public HttpHeadersAssert hasValue(String name, String value) {
94-
containsKey(name);
133+
containsHeader(name);
95134
Assertions.assertThat(this.actual.getFirst(name))
96135
.as("check primary value for HTTP header '%s'", name)
97136
.isEqualTo(value);
@@ -105,7 +144,7 @@ public HttpHeadersAssert hasValue(String name, String value) {
105144
* @param value the expected value of the header
106145
*/
107146
public HttpHeadersAssert hasValue(String name, long value) {
108-
containsKey(name);
147+
containsHeader(name);
109148
Assertions.assertThat(this.actual.getFirst(name))
110149
.as("check primary long value for HTTP header '%s'", name)
111150
.asLong().isEqualTo(value);
@@ -119,11 +158,149 @@ public HttpHeadersAssert hasValue(String name, long value) {
119158
* @param value the expected value of the header
120159
*/
121160
public HttpHeadersAssert hasValue(String name, Instant value) {
122-
containsKey(name);
161+
containsHeader(name);
123162
Assertions.assertThat(this.actual.getFirstZonedDateTime(name))
124163
.as("check primary date value for HTTP header '%s'", name)
125164
.isCloseTo(ZonedDateTime.ofInstant(value, GMT), Assertions.within(999, ChronoUnit.MILLIS));
126165
return this.myself;
127166
}
128167

168+
/**
169+
* Verify that the given header has a full list of values exactly equal to
170+
* the given list of values, and in the same order.
171+
* @param name the considered header name (case-insensitive)
172+
* @param values the exhaustive list of expected values
173+
*/
174+
public HttpHeadersAssert hasExactlyValues(String name, List<String> values) {
175+
containsHeader(name);
176+
Assertions.assertThat(this.actual.get(name))
177+
.as("check all values of HTTP header '%s'", name)
178+
.containsExactlyElementsOf(values);
179+
return this.myself;
180+
}
181+
182+
/**
183+
* Verify that the given header has a full list of values exactly equal to
184+
* the given list of values, in any order.
185+
* @param name the considered header name (case-insensitive)
186+
* @param values the exhaustive list of expected values
187+
*/
188+
public HttpHeadersAssert hasExactlyValuesInAnyOrder(String name, List<String> values) {
189+
containsHeader(name);
190+
Assertions.assertThat(this.actual.get(name))
191+
.as("check all values of HTTP header '%s' in any order", name)
192+
.containsExactlyInAnyOrderElementsOf(values);
193+
return this.myself;
194+
}
195+
196+
/**
197+
* Verify that headers are empty and no headers are present.
198+
*/
199+
public HttpHeadersAssert isEmpty() {
200+
this.namesAssert
201+
.as("check headers are empty")
202+
.isEmpty();
203+
return this.myself;
204+
}
205+
206+
/**
207+
* Verify that headers are not empty and at least one header is present.
208+
*/
209+
public HttpHeadersAssert isNotEmpty() {
210+
this.namesAssert
211+
.as("check headers are not empty")
212+
.isNotEmpty();
213+
return this.myself;
214+
}
215+
216+
/**
217+
* Verify that there is exactly {@code expected} headers present, when
218+
* considering header names in a case-insensitive manner.
219+
* @param expected the expected number of headers
220+
*/
221+
public HttpHeadersAssert hasSize(int expected) {
222+
this.namesAssert
223+
.as("check headers have size '%i'", expected)
224+
.hasSize(expected);
225+
return this.myself;
226+
}
227+
228+
/**
229+
* Verify that the number of headers present is strictly greater than the
230+
* given boundary, when considering header names in a case-insensitive
231+
* manner.
232+
* @param boundary the given value to compare actual header size to
233+
*/
234+
public HttpHeadersAssert hasSizeGreaterThan(int boundary) {
235+
this.namesAssert
236+
.as("check headers have size > '%i'", boundary)
237+
.hasSizeGreaterThan(boundary);
238+
return this.myself;
239+
}
240+
241+
/**
242+
* Verify that the number of headers present is greater or equal to the
243+
* given boundary, when considering header names in a case-insensitive
244+
* manner.
245+
* @param boundary the given value to compare actual header size to
246+
*/
247+
public HttpHeadersAssert hasSizeGreaterThanOrEqualTo(int boundary) {
248+
this.namesAssert
249+
.as("check headers have size >= '%i'", boundary)
250+
.hasSizeGreaterThanOrEqualTo(boundary);
251+
return this.myself;
252+
}
253+
254+
/**
255+
* Verify that the number of headers present is strictly less than the
256+
* given boundary, when considering header names in a case-insensitive
257+
* manner.
258+
* @param boundary the given value to compare actual header size to
259+
*/
260+
public HttpHeadersAssert hasSizeLessThan(int boundary) {
261+
this.namesAssert
262+
.as("check headers have size < '%i'", boundary)
263+
.hasSizeLessThan(boundary);
264+
return this.myself;
265+
}
266+
267+
/**
268+
* Verify that the number of headers present is less than or equal to the
269+
* given boundary, when considering header names in a case-insensitive
270+
* manner.
271+
* @param boundary the given value to compare actual header size to
272+
*/
273+
public HttpHeadersAssert hasSizeLessThanOrEqualTo(int boundary) {
274+
this.namesAssert
275+
.as("check headers have size <= '%i'", boundary)
276+
.hasSizeLessThanOrEqualTo(boundary);
277+
return this.myself;
278+
}
279+
280+
/**
281+
* Verify that the number of headers present is between the given boundaries
282+
* (inclusive), when considering header names in a case-insensitive manner.
283+
* @param lowerBoundary the lower boundary compared to which actual size
284+
* should be greater than or equal to
285+
* @param higherBoundary the higher boundary compared to which actual size
286+
* should be less than or equal to
287+
*/
288+
public HttpHeadersAssert hasSizeBetween(int lowerBoundary, int higherBoundary) {
289+
this.namesAssert
290+
.as("check headers have size between '%i' and '%i'", lowerBoundary, higherBoundary)
291+
.hasSizeBetween(lowerBoundary, higherBoundary);
292+
return this.myself;
293+
}
294+
295+
/**
296+
* Verify that the number actual headers is the same as in the given
297+
* {@code HttpHeaders}.
298+
* @param other the {@code HttpHeaders} to compare size with
299+
*/
300+
public HttpHeadersAssert hasSameSizeAs(HttpHeaders other) {
301+
this.namesAssert
302+
.as("check headers have same size as '%s'", other)
303+
.hasSize(other.size());
304+
return this.myself;
305+
}
129306
}

spring-test/src/main/java/org/springframework/test/web/client/match/MockRestRequestMatchers.java

+19-7
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
import org.hamcrest.Matcher;
2626

27+
import org.springframework.http.HttpHeaders;
2728
import org.springframework.http.HttpMethod;
2829
import org.springframework.http.client.ClientHttpRequest;
2930
import org.springframework.test.web.client.MockRestServiceServer;
@@ -162,7 +163,7 @@ public static RequestMatcher queryParamList(String name, Matcher<? super List<St
162163
public static RequestMatcher queryParam(String name, Matcher<? super String>... matchers) {
163164
return request -> {
164165
MultiValueMap<String, String> params = getQueryParams(request);
165-
assertValueCount("query param", name, params, matchers.length);
166+
assertValueCount(name, params, matchers.length);
166167
for (int i = 0 ; i < matchers.length; i++) {
167168
assertThat("Query param", params.get(name).get(i), matchers[i]);
168169
}
@@ -190,7 +191,7 @@ public static RequestMatcher queryParam(String name, Matcher<? super String>...
190191
public static RequestMatcher queryParam(String name, String... expectedValues) {
191192
return request -> {
192193
MultiValueMap<String, String> params = getQueryParams(request);
193-
assertValueCount("query param", name, params, expectedValues.length);
194+
assertValueCount(name, params, expectedValues.length);
194195
for (int i = 0 ; i < expectedValues.length; i++) {
195196
assertEquals("Query param [" + name + "]", expectedValues[i], params.get(name).get(i));
196197
}
@@ -246,7 +247,7 @@ public static RequestMatcher headerList(String name, Matcher<? super List<String
246247
@SafeVarargs
247248
public static RequestMatcher header(String name, Matcher<? super String>... matchers) {
248249
return request -> {
249-
assertValueCount("header", name, request.getHeaders(), matchers.length);
250+
assertValueCount(name, request.getHeaders(), matchers.length);
250251
List<String> headerValues = request.getHeaders().get(name);
251252
Assert.state(headerValues != null, "No header values");
252253
for (int i = 0; i < matchers.length; i++) {
@@ -273,7 +274,7 @@ public static RequestMatcher header(String name, Matcher<? super String>... matc
273274
*/
274275
public static RequestMatcher header(String name, String... expectedValues) {
275276
return request -> {
276-
assertValueCount("header", name, request.getHeaders(), expectedValues.length);
277+
assertValueCount(name, request.getHeaders(), expectedValues.length);
277278
List<String> headerValues = request.getHeaders().get(name);
278279
Assert.state(headerValues != null, "No header values");
279280
for (int i = 0; i < expectedValues.length; i++) {
@@ -356,11 +357,22 @@ public static XpathRequestMatchers xpath(String expression, Map<String, String>
356357
}
357358

358359

359-
private static void assertValueCount(
360-
String valueType, String name, MultiValueMap<String, String> map, int count) {
360+
private static void assertValueCount(String name, MultiValueMap<String, String> map, int count) {
361361

362362
List<String> values = map.get(name);
363-
String message = "Expected " + valueType + " <" + name + ">";
363+
String message = "Expected query param <" + name + ">";
364+
if (values == null) {
365+
fail(message + " to exist but was null");
366+
}
367+
else if (count > values.size()) {
368+
fail(message + " to have at least <" + count + "> values but found " + values);
369+
}
370+
}
371+
372+
private static void assertValueCount(String name, HttpHeaders headers, int count) {
373+
374+
List<String> values = headers.get(name);
375+
String message = "Expected header <" + name + ">";
364376
if (values == null) {
365377
fail(message + " to exist but was null");
366378
}

0 commit comments

Comments
 (0)