diff --git a/spring-test/src/main/java/org/springframework/test/web/client/match/MockRestRequestMatchers.java b/spring-test/src/main/java/org/springframework/test/web/client/match/MockRestRequestMatchers.java
index ff3f6c27a7d..17011605441 100644
--- a/spring-test/src/main/java/org/springframework/test/web/client/match/MockRestRequestMatchers.java
+++ b/spring-test/src/main/java/org/springframework/test/web/client/match/MockRestRequestMatchers.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2023 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.
@@ -23,6 +23,7 @@ import java.util.Map;
import javax.xml.xpath.XPathExpressionException;
import org.hamcrest.Matcher;
+import org.hamcrest.Matchers;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.ClientHttpRequest;
@@ -114,6 +115,11 @@ public abstract class MockRestRequestMatchers {
/**
* Assert request query parameter values with the given Hamcrest matcher(s).
+ *
Note that if the queryParam value list is larger than the number of provided
+ * {@code matchers}, extra values are considered acceptable.
+ * See {@link #queryParam(String, Matcher)} for a variant that takes a
+ * {@code Matcher} over the whole list of values.
+ * @see #queryParam(String, Matcher)
*/
@SafeVarargs
public static RequestMatcher queryParam(String name, Matcher super String>... matchers) {
@@ -128,6 +134,11 @@ public abstract class MockRestRequestMatchers {
/**
* Assert request query parameter values.
+ *
Note that if the queryParam value list is larger than {@code expectedValues},
+ * extra values are considered acceptable.
+ * See {@link #queryParam(String, Matcher)} for a variant that takes a
+ * {@code Matcher} over the whole list of values.
+ * @see #queryParam(String, Matcher)
*/
public static RequestMatcher queryParam(String name, String... expectedValues) {
return request -> {
@@ -139,6 +150,29 @@ public abstract class MockRestRequestMatchers {
};
}
+ /**
+ * Assert request query parameter, matching on the whole {@code List} of values.
+ *
This can be used to check that the list has at least one value matching a
+ * criteria ({@link Matchers#hasItem(Matcher)}), or that every value in the list
+ * matches a common criteria ({@link Matchers#everyItem(Matcher)}), or that each
+ * value in the list matches its corresponding dedicated criteria
+ * ({@link Matchers#contains(Matcher[])}, and more.
+ * @param name the name of the queryParam to consider
+ * @param matcher the matcher to apply to the whole list of values for that header
+ * @since 6.0.5
+ */
+ public static RequestMatcher queryParam(String name, Matcher super List> matcher) {
+ return request -> {
+ MultiValueMap params = getQueryParams(request);
+ List paramValues = params.get(name);
+ if (paramValues == null) {
+ fail("No queryParam [" + name + "]");
+ }
+ assertThat("Request queryParam values for [" + name + "]", paramValues, matcher);
+ };
+ }
+
+
private static MultiValueMap getQueryParams(ClientHttpRequest request) {
return UriComponentsBuilder.fromUri(request.getURI()).build().getQueryParams();
}
@@ -158,6 +192,11 @@ public abstract class MockRestRequestMatchers {
/**
* Assert request header values with the given Hamcrest matcher(s).
+ * Note that if the header's value list is larger than the number of provided
+ * {@code matchers}, extra values are considered acceptable.
+ * See {@link #header(String, Matcher)} for a variant that takes a {@code Matcher}
+ * over the whole list of values.
+ * @see #header(String, Matcher)
*/
@SafeVarargs
public static RequestMatcher header(String name, Matcher super String>... matchers) {
@@ -173,6 +212,11 @@ public abstract class MockRestRequestMatchers {
/**
* Assert request header values.
+ *
Note that if the header's value list is larger than {@code expectedValues},
+ * extra values are considered acceptable.
+ * See {@link #header(String, Matcher)} for a variant that takes a {@code Matcher}
+ * over the whole list of values.
+ * @see #header(String, Matcher)
*/
public static RequestMatcher header(String name, String... expectedValues) {
return request -> {
@@ -185,6 +229,27 @@ public abstract class MockRestRequestMatchers {
};
}
+ /**
+ * Assert request header, matching on the whole {@code List} of values.
+ *
This can be used to check that the list has at least one value matching a
+ * criteria ({@link Matchers#hasItem(Matcher)}), or that every value in the list
+ * matches a common criteria ({@link Matchers#everyItem(Matcher)}), or that each
+ * value in the list matches its corresponding dedicated criteria
+ * ({@link Matchers#contains(Matcher[])}, and more.
+ * @param name the name of the header to consider
+ * @param matcher the matcher to apply to the whole list of values for that header
+ * @since 6.0.5
+ */
+ public static RequestMatcher header(String name, Matcher super List> matcher) {
+ return request -> {
+ List headerValues = request.getHeaders().get(name);
+ if (headerValues == null) {
+ fail("No header values for header [" + name + "]");
+ }
+ assertThat("Request header values for [" + name + "]", headerValues, matcher);
+ };
+ }
+
/**
* Assert that the given request header does not exist.
* @since 5.2
diff --git a/spring-test/src/test/java/org/springframework/test/web/client/match/MockRestRequestMatchersTests.java b/spring-test/src/test/java/org/springframework/test/web/client/match/MockRestRequestMatchersTests.java
index 6481a3313bd..ec99b23a996 100644
--- a/spring-test/src/test/java/org/springframework/test/web/client/match/MockRestRequestMatchersTests.java
+++ b/spring-test/src/test/java/org/springframework/test/web/client/match/MockRestRequestMatchersTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2023 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.
@@ -16,18 +16,35 @@
package org.springframework.test.web.client.match;
+import java.io.IOException;
import java.net.URI;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import org.hamcrest.CoreMatchers;
+import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpMethod;
import org.springframework.mock.http.client.MockClientHttpRequest;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.any;
+import static org.hamcrest.Matchers.anything;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.endsWith;
+import static org.hamcrest.Matchers.everyItem;
+import static org.hamcrest.Matchers.hasItem;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.nullValue;
+import static org.hamcrest.Matchers.startsWith;
/**
* Unit tests for {@link MockRestRequestMatchers}.
@@ -146,6 +163,63 @@ public class MockRestRequestMatchersTests {
.hasMessageContaining("was \"bar\"");
}
+ @Test
+ void headerListMissing() {
+ assertThatThrownBy(() -> MockRestRequestMatchers.header("foo", hasSize(2)).match(this.request))
+ .isInstanceOf(AssertionError.class)
+ .hasMessage("No header values for header [foo]");
+ }
+
+ @Test
+ void headerListMatchers() throws IOException {
+ this.request.getHeaders().put("foo", Arrays.asList("bar", "baz"));
+
+ MockRestRequestMatchers.header("foo", containsInAnyOrder(endsWith("baz"), endsWith("bar"))).match(this.request);
+ MockRestRequestMatchers.header("foo", contains(is("bar"), is("baz"))).match(this.request);
+ MockRestRequestMatchers.header("foo", contains(is("bar"), Matchers.anything())).match(this.request);
+ MockRestRequestMatchers.header("foo", hasItem(endsWith("baz"))).match(this.request);
+ MockRestRequestMatchers.header("foo", everyItem(startsWith("ba"))).match(this.request);
+ MockRestRequestMatchers.header("foo", hasSize(2)).match(this.request);
+
+ //these can be a bit ambiguous when reading the test (the compiler selects the list matcher):
+ MockRestRequestMatchers.header("foo", notNullValue()).match(this.request);
+ MockRestRequestMatchers.header("foo", is(anything())).match(this.request);
+ MockRestRequestMatchers.header("foo", allOf(notNullValue(), notNullValue())).match(this.request);
+
+ //these are not as ambiguous thanks to an inner matcher that is either obviously list-oriented,
+ //string-oriented or obviously a vararg of matchers
+ //list matcher version
+ MockRestRequestMatchers.header("foo", allOf(notNullValue(), hasSize(2))).match(this.request);
+ //vararg version
+ MockRestRequestMatchers.header("foo", allOf(notNullValue(), endsWith("ar"))).match(this.request);
+ MockRestRequestMatchers.header("foo", is((any(String.class)))).match(this.request);
+ MockRestRequestMatchers.header("foo", CoreMatchers.either(is("bar")).or(is(nullValue()))).match(this.request);
+ MockRestRequestMatchers.header("foo", is(notNullValue()), is(notNullValue())).match(this.request);
+ }
+
+ @Test
+ void headerListContainsMismatch() {
+ this.request.getHeaders().put("foo", Arrays.asList("bar", "baz"));
+
+ assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> MockRestRequestMatchers
+ .header("foo", contains(containsString("ba"))).match(this.request))
+ .withMessage("Request header values for [foo]\n"
+ + "Expected: iterable containing [a string containing \"ba\"]\n"
+ + " but: not matched: \"baz\"");
+
+ assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> MockRestRequestMatchers
+ .header("foo", hasItem(endsWith("ba"))).match(this.request))
+ .withMessage("Request header values for [foo]\n"
+ + "Expected: a collection containing a string ending with \"ba\"\n"
+ + " but: mismatches were: [was \"bar\", was \"baz\"]");
+
+ assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> MockRestRequestMatchers
+ .header("foo", everyItem(endsWith("ar"))).match(this.request))
+ .withMessage("Request header values for [foo]\n"
+ + "Expected: every item is a string ending with \"ar\"\n"
+ + " but: an item was \"baz\"");
+ }
+
@Test
public void headers() throws Exception {
this.request.getHeaders().put("foo", Arrays.asList("bar", "baz"));
@@ -210,4 +284,62 @@ public class MockRestRequestMatchersTests {
.hasMessageContaining("was \"bar\"");
}
+
+ @Test
+ void queryParamListMissing() {
+ assertThatThrownBy(() -> MockRestRequestMatchers.queryParam("foo", hasSize(2)).match(this.request))
+ .isInstanceOf(AssertionError.class)
+ .hasMessage("No queryParam [foo]");
+ }
+
+ @Test
+ void queryParamListMatchers() throws IOException {
+ this.request.setURI(URI.create("http://www.foo.example/a?foo=bar&foo=baz"));
+
+ MockRestRequestMatchers.queryParam("foo", containsInAnyOrder(endsWith("baz"), endsWith("bar"))).match(this.request);
+ MockRestRequestMatchers.queryParam("foo", contains(is("bar"), is("baz"))).match(this.request);
+ MockRestRequestMatchers.queryParam("foo", contains(is("bar"), Matchers.anything())).match(this.request);
+ MockRestRequestMatchers.queryParam("foo", hasItem(endsWith("baz"))).match(this.request);
+ MockRestRequestMatchers.queryParam("foo", everyItem(startsWith("ba"))).match(this.request);
+ MockRestRequestMatchers.queryParam("foo", hasSize(2)).match(this.request);
+
+ //these can be a bit ambiguous when reading the test (the compiler selects the list matcher):
+ MockRestRequestMatchers.queryParam("foo", notNullValue()).match(this.request);
+ MockRestRequestMatchers.queryParam("foo", is(anything())).match(this.request);
+ MockRestRequestMatchers.queryParam("foo", allOf(notNullValue(), notNullValue())).match(this.request);
+
+ //these are not as ambiguous thanks to an inner matcher that is either obviously list-oriented,
+ //string-oriented or obviously a vararg of matchers
+ //list matcher version
+ MockRestRequestMatchers.queryParam("foo", allOf(notNullValue(), hasSize(2))).match(this.request);
+ //vararg version
+ MockRestRequestMatchers.queryParam("foo", allOf(notNullValue(), endsWith("ar"))).match(this.request);
+ MockRestRequestMatchers.queryParam("foo", is((any(String.class)))).match(this.request);
+ MockRestRequestMatchers.queryParam("foo", CoreMatchers.either(is("bar")).or(is(nullValue()))).match(this.request);
+ MockRestRequestMatchers.queryParam("foo", is(notNullValue()), is(notNullValue())).match(this.request);
+ }
+
+ @Test
+ void queryParamListContainsMismatch() {
+ this.request.setURI(URI.create("http://www.foo.example/a?foo=bar&foo=baz"));
+
+ assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> MockRestRequestMatchers
+ .queryParam("foo", contains(containsString("ba"))).match(this.request))
+ .withMessage("Request queryParam values for [foo]\n"
+ + "Expected: iterable containing [a string containing \"ba\"]\n"
+ + " but: not matched: \"baz\"");
+
+ assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> MockRestRequestMatchers
+ .queryParam("foo", hasItem(endsWith("ba"))).match(this.request))
+ .withMessage("Request queryParam values for [foo]\n"
+ + "Expected: a collection containing a string ending with \"ba\"\n"
+ + " but: mismatches were: [was \"bar\", was \"baz\"]");
+
+ assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> MockRestRequestMatchers
+ .queryParam("foo", everyItem(endsWith("ar"))).match(this.request))
+ .withMessage("Request queryParam values for [foo]\n"
+ + "Expected: every item is a string ending with \"ar\"\n"
+ + " but: an item was \"baz\"");
+ }
+
}