From e97ae434fb3cc47c6f23a70fb1f9661eb624d810 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Fri, 15 Mar 2024 13:22:15 +0100 Subject: [PATCH] Add AssertJ support for HTTP requests and responses This commit adds AssertJ compatible assertions for HTTP request and responses, including headers, media type, and response body. The latter delegate to the recently included JSON assert support. See gh-21178 --- .../test/http/HttpHeadersAssert.java | 129 ++++++++++++ .../test/http/MediaTypeAssert.java | 107 ++++++++++ .../test/http/package-info.java | 9 + .../springframework/test/web/UriAssert.java | 101 ++++++++++ .../AbstractHttpServletRequestAssert.java | 122 +++++++++++ .../AbstractHttpServletResponseAssert.java | 167 +++++++++++++++ .../AbstractMockHttpServletRequestAssert.java | 38 ++++ ...AbstractMockHttpServletResponseAssert.java | 109 ++++++++++ .../servlet/assertj/ResponseBodyAssert.java | 125 ++++++++++++ .../web/servlet/assertj/package-info.java | 9 + .../test/http/HttpHeadersAssertTests.java | 190 ++++++++++++++++++ .../test/http/MediaTypeAssertTests.java | 157 +++++++++++++++ .../test/web/UriAssertTests.java | 77 +++++++ ...AbstractHttpServletRequestAssertTests.java | 143 +++++++++++++ ...bstractHttpServletResponseAssertTests.java | 138 +++++++++++++ ...ractMockHttpServletRequestAssertTests.java | 48 +++++ ...actMockHttpServletResponseAssertTests.java | 106 ++++++++++ .../assertj/ResponseBodyAssertTests.java | 88 ++++++++ 18 files changed, 1863 insertions(+) create mode 100644 spring-test/src/main/java/org/springframework/test/http/HttpHeadersAssert.java create mode 100644 spring-test/src/main/java/org/springframework/test/http/MediaTypeAssert.java create mode 100644 spring-test/src/main/java/org/springframework/test/http/package-info.java create mode 100644 spring-test/src/main/java/org/springframework/test/web/UriAssert.java create mode 100644 spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletRequestAssert.java create mode 100644 spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletResponseAssert.java create mode 100644 spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletRequestAssert.java create mode 100644 spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssert.java create mode 100644 spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssert.java create mode 100644 spring-test/src/main/java/org/springframework/test/web/servlet/assertj/package-info.java create mode 100644 spring-test/src/test/java/org/springframework/test/http/HttpHeadersAssertTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/http/MediaTypeAssertTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/UriAssertTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletRequestAssertTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletResponseAssertTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletRequestAssertTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssertTests.java create mode 100644 spring-test/src/test/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssertTests.java diff --git a/spring-test/src/main/java/org/springframework/test/http/HttpHeadersAssert.java b/spring-test/src/main/java/org/springframework/test/http/HttpHeadersAssert.java new file mode 100644 index 0000000000..04f3ca41d5 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/http/HttpHeadersAssert.java @@ -0,0 +1,129 @@ +/* + * 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.http; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.util.List; + +import org.assertj.core.api.AbstractMapAssert; +import org.assertj.core.api.Assertions; + +import org.springframework.http.HttpHeaders; + +/** + * AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied to + * {@link HttpHeaders}. + * + * @author Stephane Nicoll + * @since 6.2 + */ +public class HttpHeadersAssert extends AbstractMapAssert> { + + private static final ZoneId GMT = ZoneId.of("GMT"); + + + public HttpHeadersAssert(HttpHeaders actual) { + super(actual, HttpHeadersAssert.class); + as("HTTP headers"); + } + + /** + * Verify that the actual HTTP headers contain a header with the given + * {@code name}. + * @param name the name of an expected HTTP header + * @see #containsKey + */ + public HttpHeadersAssert containsHeader(String name) { + return containsKey(name); + } + + /** + * Verify that the actual HTTP headers contain the headers with the given + * {@code names}. + * @param names the names of expected HTTP headers + * @see #containsKeys + */ + public HttpHeadersAssert containsHeaders(String... names) { + return containsKeys(names); + } + + /** + * Verify that the actual HTTP headers do not contain a header with the + * given {@code name}. + * @param name the name of an HTTP header that should not be present + * @see #doesNotContainKey + */ + public HttpHeadersAssert doesNotContainsHeader(String name) { + return doesNotContainKey(name); + } + + /** + * Verify that the actual HTTP headers do not contain any of the headers + * with the given {@code names}. + * @param names the names of HTTP headers that should not be present + * @see #doesNotContainKeys + */ + public HttpHeadersAssert doesNotContainsHeaders(String... names) { + return doesNotContainKeys(names); + } + + /** + * Verify that the actual HTTP headers contain a header with the given + * {@code name} and {@link String} {@code value}. + * @param name the name of the cookie + * @param value the expected value of the header + */ + public HttpHeadersAssert hasValue(String name, String value) { + containsKey(name); + Assertions.assertThat(this.actual.getFirst(name)) + .as("check primary value for HTTP header '%s'", name) + .isEqualTo(value); + return this.myself; + } + + /** + * Verify that the actual HTTP headers contain a header with the given + * {@code name} and {@link Long} {@code value}. + * @param name the name of the cookie + * @param value the expected value of the header + */ + public HttpHeadersAssert hasValue(String name, long value) { + containsKey(name); + Assertions.assertThat(this.actual.getFirst(name)) + .as("check primary long value for HTTP header '%s'", name) + .asLong().isEqualTo(value); + return this.myself; + } + + /** + * Verify that the actual HTTP headers contain a header with the given + * {@code name} and {@link Instant} {@code value}. + * @param name the name of the cookie + * @param value the expected value of the header + */ + public HttpHeadersAssert hasValue(String name, Instant value) { + containsKey(name); + Assertions.assertThat(this.actual.getFirstZonedDateTime(name)) + .as("check primary date value for HTTP header '%s'", name) + .isCloseTo(ZonedDateTime.ofInstant(value, GMT), Assertions.within(999, ChronoUnit.MILLIS)); + return this.myself; + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/http/MediaTypeAssert.java b/spring-test/src/main/java/org/springframework/test/http/MediaTypeAssert.java new file mode 100644 index 0000000000..599a1ccb44 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/http/MediaTypeAssert.java @@ -0,0 +1,107 @@ +/* + * 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.http; + +import org.assertj.core.api.AbstractObjectAssert; +import org.assertj.core.api.Assertions; +import org.assertj.core.error.BasicErrorMessageFactory; +import org.assertj.core.internal.Failures; + +import org.springframework.http.InvalidMediaTypeException; +import org.springframework.http.MediaType; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +/** + * AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied + * to a {@link MediaType}. + * + * @author Brian Clozel + * @author Stephane Nicoll + * @since 6.2 + */ +public class MediaTypeAssert extends AbstractObjectAssert { + + public MediaTypeAssert(@Nullable MediaType mediaType) { + super(mediaType, MediaTypeAssert.class); + as("Media type"); + } + + public MediaTypeAssert(@Nullable String actual) { + this(StringUtils.hasText(actual) ? MediaType.parseMediaType(actual) : null); + } + + /** + * Verify that the actual media type is equal to the given string + * representation. + * @param expected the expected media type + */ + public MediaTypeAssert isEqualTo(String expected) { + return isEqualTo(parseMediaType(expected)); + } + + /** + * Verify that the actual media type is + * {@linkplain MediaType#isCompatibleWith(MediaType) compatible} with the + * given one. Example:

+	 * // Check that actual is compatible with "application/json"
+	 * assertThat(mediaType).isCompatibleWith(MediaType.APPLICATION_JSON);
+	 * 
+ * @param mediaType the media type with which to compare + */ + public MediaTypeAssert isCompatibleWith(MediaType mediaType) { + Assertions.assertThat(this.actual) + .withFailMessage("Expecting null to be compatible with '%s'", mediaType).isNotNull(); + Assertions.assertThat(mediaType) + .withFailMessage("Expecting '%s' to be compatible with null", this.actual).isNotNull(); + Assertions.assertThat(this.actual.isCompatibleWith(mediaType)) + .as("check media type '%s' is compatible with '%s'", this.actual.toString(), mediaType.toString()) + .isTrue(); + return this; + } + + /** + * Verify that the actual media type is + * {@linkplain MediaType#isCompatibleWith(MediaType) compatible} with the + * given one. Example:

+	 * // Check that actual is compatible with "text/plain"
+	 * assertThat(mediaType).isCompatibleWith("text/plain");
+	 * 
+ * @param mediaType the media type with which to compare + */ + public MediaTypeAssert isCompatibleWith(String mediaType) { + return isCompatibleWith(parseMediaType(mediaType)); + } + + + private MediaType parseMediaType(String value) { + try { + return MediaType.parseMediaType(value); + } + catch (InvalidMediaTypeException ex) { + throw Failures.instance().failure(this.info, new ShouldBeValidMediaType(value, ex.getMessage())); + } + } + + private static final class ShouldBeValidMediaType extends BasicErrorMessageFactory { + + private ShouldBeValidMediaType(String mediaType, String errorMessage) { + super("%nExpecting:%n %s%nTo be a valid media type but got:%n %s%n", mediaType, errorMessage); + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/http/package-info.java b/spring-test/src/main/java/org/springframework/test/http/package-info.java new file mode 100644 index 0000000000..6613b8a012 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/http/package-info.java @@ -0,0 +1,9 @@ +/** + * Test support for HTTP concepts. + */ +@NonNullApi +@NonNullFields +package org.springframework.test.http; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-test/src/main/java/org/springframework/test/web/UriAssert.java b/spring-test/src/main/java/org/springframework/test/web/UriAssert.java new file mode 100644 index 0000000000..d916b7de59 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/UriAssert.java @@ -0,0 +1,101 @@ +/* + * 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; + +import org.assertj.core.api.AbstractStringAssert; +import org.assertj.core.api.Assertions; +import org.assertj.core.error.BasicErrorMessageFactory; +import org.assertj.core.internal.Failures; + +import org.springframework.lang.Nullable; +import org.springframework.util.AntPathMatcher; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied + * to a {@link String} representing a URI. + * + * @author Stephane Nicoll + * @since 6.2 + */ +public class UriAssert extends AbstractStringAssert { + + private static final AntPathMatcher pathMatcher = new AntPathMatcher(); + + private final String displayName; + + public UriAssert(@Nullable String actual, String displayName) { + super(actual, UriAssert.class); + this.displayName = displayName; + as(displayName); + } + + /** + * Verify that the actual URI is equal to the URI built using the given + * {@code uriTemplate} and {@code uriVars}. + * Example:

+	 * // Verify that uri is equal to "/orders/1/items/2"
+	 * assertThat(uri).isEqualToTemplate("/orders/{orderId}/items/{itemId}", 1, 2));
+	 * 
+ * @param uriTemplate the expected URI string, with a number of URI + * template variables + * @param uriVars the values to replace the URI template variables + * @see UriComponentsBuilder#buildAndExpand(Object...) + */ + public UriAssert isEqualToTemplate(String uriTemplate, Object... uriVars) { + String uri = buildUri(uriTemplate, uriVars); + return isEqualTo(uri); + } + + /** + * Verify that the actual URI matches the given {@linkplain AntPathMatcher + * Ant-style} {@code uriPattern}. + * Example:

+	 * // Verify that pattern matches "/orders/1/items/2"
+	 * assertThat(uri).matchPattern("/orders/*"));
+	 * 
+ * @param uriPattern the pattern that is expected to match + */ + public UriAssert matchPattern(String uriPattern) { + Assertions.assertThat(pathMatcher.isPattern(uriPattern)) + .withFailMessage("'%s' is not an Ant-style path pattern", uriPattern).isTrue(); + Assertions.assertThat(pathMatcher.match(uriPattern, this.actual)) + .withFailMessage("%s '%s' does not match the expected URI pattern '%s'", + this.displayName, this.actual, uriPattern).isTrue(); + return this; + } + + private String buildUri(String uriTemplate, Object... uriVars) { + try { + return UriComponentsBuilder.fromUriString(uriTemplate) + .buildAndExpand(uriVars).encode().toUriString(); + } + catch (Exception ex) { + throw Failures.instance().failure(this.info, + new ShouldBeValidUriTemplate(uriTemplate, ex.getMessage())); + } + } + + + private static final class ShouldBeValidUriTemplate extends BasicErrorMessageFactory { + + private ShouldBeValidUriTemplate(String uriTemplate, String errorMessage) { + super("%nExpecting:%n %s%nTo be a valid URI template but got:%n %s%n", uriTemplate, errorMessage); + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletRequestAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletRequestAssert.java new file mode 100644 index 0000000000..0934e5d13b --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletRequestAssert.java @@ -0,0 +1,122 @@ +/* + * 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.servlet.assertj; + +import java.util.Enumeration; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.function.Function; +import java.util.function.Supplier; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; +import org.assertj.core.api.AbstractObjectAssert; +import org.assertj.core.api.Assertions; +import org.assertj.core.api.MapAssert; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.util.function.SingletonSupplier; +import org.springframework.web.context.request.async.DeferredResult; + +/** + * Base AssertJ {@link org.assertj.core.api.Assert assertions} that can be + * applied to a {@link HttpServletRequest}. + * + * @author Stephane Nicoll + * @since 6.2 + * @param the type of assertions + * @param the type of the object to assert + */ +public abstract class AbstractHttpServletRequestAssert, ACTUAL extends HttpServletRequest> + extends AbstractObjectAssert { + + private final Supplier> attributesAssertProvider; + + private final Supplier> sessionAttributesAssertProvider; + + protected AbstractHttpServletRequestAssert(ACTUAL actual, Class selfType) { + super(actual, selfType); + this.attributesAssertProvider = SingletonSupplier.of(() -> createAttributesAssert(actual)); + this.sessionAttributesAssertProvider = SingletonSupplier.of(() -> createSessionAttributesAssert(actual)); + } + + private static MapAssert createAttributesAssert(HttpServletRequest request) { + Map map = toMap(request.getAttributeNames(), request::getAttribute); + return Assertions.assertThat(map).as("Request Attributes"); + } + + private static MapAssert createSessionAttributesAssert(HttpServletRequest request) { + HttpSession session = request.getSession(); + Assertions.assertThat(session).as("HTTP session").isNotNull(); + Map map = toMap(session.getAttributeNames(), session::getAttribute); + return Assertions.assertThat(map).as("Session Attributes"); + } + + /** + * Return a new {@linkplain MapAssert assertion} object that uses the request + * attributes as the object to test, with values mapped by attribute name. + * Examples:

+	 * // Check for the presence of a request attribute named "attributeName":
+	 * assertThat(request).attributes().containsKey("attributeName");
+	 * 
+ */ + public MapAssert attributes() { + return this.attributesAssertProvider.get(); + } + + /** + * Return a new {@linkplain MapAssert assertion} object that uses the session + * attributes as the object to test, with values mapped by attribute name. + * Examples:

+	 * // Check for the presence of a session attribute named "username":
+	 * assertThat(request).sessionAttributes().containsKey("username");
+	 * 
+ */ + public MapAssert sessionAttributes() { + return this.sessionAttributesAssertProvider.get(); + } + + /** + * Verify that whether asynchronous processing started, usually as a result + * of a controller method returning {@link Callable} or {@link DeferredResult}. + *

The test will await the completion of a {@code Callable} so that + * {@link MvcResultAssert#asyncResult()} can be used to assert the resulting + * value. + *

Neither a {@code Callable} nor a {@code DeferredResult} will complete + * processing all the way since a {@link MockHttpServletRequest} does not + * perform asynchronous dispatches. + * @param started whether asynchronous processing should have started + */ + public SELF hasAsyncStarted(boolean started) { + Assertions.assertThat(this.actual.isAsyncStarted()) + .withFailMessage("Async expected to %s started", (started ? "have" : "not have")) + .isEqualTo(started); + return this.myself; + } + + + private static Map toMap(Enumeration keys, Function valueProvider) { + Map map = new LinkedHashMap<>(); + while (keys.hasMoreElements()) { + String key = keys.nextElement(); + map.put(key, valueProvider.apply(key)); + } + return map; + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletResponseAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletResponseAssert.java new file mode 100644 index 0000000000..f5388116cc --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletResponseAssert.java @@ -0,0 +1,167 @@ +/* + * 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.servlet.assertj; + +import java.util.ArrayList; +import java.util.function.Supplier; + +import jakarta.servlet.http.HttpServletResponse; +import org.assertj.core.api.AbstractIntegerAssert; +import org.assertj.core.api.AbstractMapAssert; +import org.assertj.core.api.AbstractObjectAssert; +import org.assertj.core.api.Assertions; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatus.Series; +import org.springframework.test.http.HttpHeadersAssert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.function.SingletonSupplier; + +/** + * Base AssertJ {@link org.assertj.core.api.Assert assertions} that can be + * applied to any object that provides an {@link HttpServletResponse}. This + * allows to provide direct access to response assertions while providing + * access to a different top-level object. + * + * @author Stephane Nicoll + * @since 6.2 + * @param the type of {@link HttpServletResponse} + * @param the type of assertions + * @param the type of the object to assert + */ +public abstract class AbstractHttpServletResponseAssert, ACTUAL> + extends AbstractObjectAssert { + + private final Supplier> statusAssert; + + private final Supplier headersAssertSupplier; + + + protected AbstractHttpServletResponseAssert(ACTUAL actual, Class selfType) { + super(actual, selfType); + this.statusAssert = SingletonSupplier.of(() -> Assertions.assertThat(getResponse().getStatus()).as("HTTP status code")); + this.headersAssertSupplier = SingletonSupplier.of(() -> new HttpHeadersAssert(getHttpHeaders(getResponse()))); + } + + /** + * Provide the response to use if it is available. Throw an + * {@link AssertionError} if the request has failed to process and the + * response is not available. + * @return the response to use + */ + protected abstract R getResponse(); + + /** + * Return a new {@linkplain HttpHeadersAssert assertion} object that uses + * the {@link HttpHeaders} as the object to test. The return assertion + * object provides all the regular {@linkplain AbstractMapAssert map + * assertions}, with headers mapped by header name. + * Examples:


+	 * // Check for the presence of the Accept header:
+	 * assertThat(response).headers().containsHeader(HttpHeaders.ACCEPT);
+	 * // Check for the absence of the Content-Length header:
+	 * assertThat(response).headers().doesNotContainsHeader(HttpHeaders.CONTENT_LENGTH);
+	 * 
+ */ + public HttpHeadersAssert headers() { + return this.headersAssertSupplier.get(); + } + + /** + * Verify that the HTTP status is equal to the specified status code. + * @param status the expected HTTP status code + */ + public SELF hasStatus(int status) { + status().isEqualTo(status); + return this.myself; + } + + /** + * Verify that the HTTP status is equal to the specified + * {@linkplain HttpStatus status}. + * @param status the expected HTTP status code + */ + public SELF hasStatus(HttpStatus status) { + return hasStatus(status.value()); + } + + /** + * Verify that the HTTP status is equal to {@link HttpStatus#OK}. + * @see #hasStatus(HttpStatus) + */ + public SELF hasStatusOk() { + return hasStatus(HttpStatus.OK); + } + + /** + * Verify that the HTTP status code is in the 1xx range. + * @see RFC 2616 + */ + public SELF hasStatus1xxInformational() { + return hasStatusSeries(Series.INFORMATIONAL); + } + + /** + * Verify that the HTTP status code is in the 2xx range. + * @see RFC 2616 + */ + public SELF hasStatus2xxSuccessful() { + return hasStatusSeries(Series.SUCCESSFUL); + } + + /** + * Verify that the HTTP status code is in the 3xx range. + * @see RFC 2616 + */ + public SELF hasStatus3xxRedirection() { + return hasStatusSeries(Series.REDIRECTION); + } + + /** + * Verify that the HTTP status code is in the 4xx range. + * @see RFC 2616 + */ + public SELF hasStatus4xxClientError() { + return hasStatusSeries(Series.CLIENT_ERROR); + } + + /** + * Verify that the HTTP status code is in the 5xx range. + * @see RFC 2616 + */ + public SELF hasStatus5xxServerError() { + return hasStatusSeries(Series.SERVER_ERROR); + } + + private SELF hasStatusSeries(Series series) { + Assertions.assertThat(Series.resolve(getResponse().getStatus())).as("HTTP status series").isEqualTo(series); + return this.myself; + } + + private AbstractIntegerAssert status() { + return this.statusAssert.get(); + } + + private static HttpHeaders getHttpHeaders(HttpServletResponse response) { + MultiValueMap headers = new LinkedMultiValueMap<>(); + response.getHeaderNames().forEach(name -> headers.put(name, new ArrayList<>(response.getHeaders(name)))); + return new HttpHeaders(headers); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletRequestAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletRequestAssert.java new file mode 100644 index 0000000000..db549a4376 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletRequestAssert.java @@ -0,0 +1,38 @@ +/* + * 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.servlet.assertj; + +import org.springframework.mock.web.MockHttpServletRequest; + +/** + * AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied to + * {@link MockHttpServletRequest}. + * + * @author Stephane Nicoll + * @since 6.2 + * @param the type of assertions + */ +public abstract class AbstractMockHttpServletRequestAssert> + extends AbstractHttpServletRequestAssert { + + protected AbstractMockHttpServletRequestAssert(MockHttpServletRequest request, Class selfType) { + super(request, selfType); + } + + + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssert.java new file mode 100644 index 0000000000..2df9de488f --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssert.java @@ -0,0 +1,109 @@ +/* + * 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.servlet.assertj; + +import java.nio.charset.Charset; + +import org.springframework.http.converter.GenericHttpMessageConverter; +import org.springframework.lang.Nullable; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.web.UriAssert; + +/** + * Extension of {@link AbstractHttpServletResponseAssert} for + * {@link MockHttpServletResponse}. + * + * @author Stephane Nicoll + * @since 6.2 + * @param the type of assertions + * @param the type of the object to assert + */ +public abstract class AbstractMockHttpServletResponseAssert, ACTUAL> + extends AbstractHttpServletResponseAssert { + + @Nullable + private final GenericHttpMessageConverter jsonMessageConverter; + + protected AbstractMockHttpServletResponseAssert( + @Nullable GenericHttpMessageConverter jsonMessageConverter, ACTUAL actual, Class selfType) { + + super(actual, selfType); + this.jsonMessageConverter = jsonMessageConverter; + } + + /** + * Return a new {@linkplain ResponseBodyAssert assertion} object that uses + * the response body as the object to test. The return assertion object + * provides access to the raw byte array, a String value decoded using the + * response's character encoding, and dedicated json testing support. + * Examples:

+	 * // Check that the response body is equal to "Hello World":
+	 * assertThat(response).body().isEqualTo("Hello World");
+	 * // Check that the response body is strictly equal to the content of "test.json":
+	 * assertThat(response).body().json().isStrictlyEqualToJson("test.json");
+	 * 
+ */ + public ResponseBodyAssert body() { + return new ResponseBodyAssert(getResponse().getContentAsByteArray(), + Charset.forName(getResponse().getCharacterEncoding()), this.jsonMessageConverter); + } + + /** + * Return a new {@linkplain UriAssert assertion} object that uses the + * forwarded URL as the object to test. If a simple equality check is + * required consider using {@link #hasForwardedUrl(String)} instead. + * Example:

+	 * // Check that the forwarded URL starts with "/orders/":
+	 * assertThat(response).forwardedUrl().matchPattern("/orders/*);
+	 * 
+ */ + public UriAssert forwardedUrl() { + return new UriAssert(getResponse().getForwardedUrl(), "Forwarded URL"); + } + + /** + * Return a new {@linkplain UriAssert assertion} object that uses the + * redirected URL as the object to test. If a simple equality check is + * required consider using {@link #hasRedirectedUrl(String)} instead. + * Example:

+	 * // Check that the redirected URL starts with "/orders/":
+	 * assertThat(response).redirectedUrl().matchPattern("/orders/*);
+	 * 
+ */ + public UriAssert redirectedUrl() { + return new UriAssert(getResponse().getRedirectedUrl(), "Redirected URL"); + } + + /** + * Verify that the forwarded URL is equal to the given value. + * @param forwardedUrl the expected forwarded URL (can be null) + */ + public SELF hasForwardedUrl(@Nullable String forwardedUrl) { + forwardedUrl().isEqualTo(forwardedUrl); + return this.myself; + } + + /** + * Verify that the redirected URL is equal to the given value. + * @param redirectedUrl the expected redirected URL (can be null) + */ + public SELF hasRedirectedUrl(@Nullable String redirectedUrl) { + redirectedUrl().isEqualTo(redirectedUrl); + return this.myself; + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssert.java new file mode 100644 index 0000000000..3edad9b262 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssert.java @@ -0,0 +1,125 @@ +/* + * 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.servlet.assertj; + +import java.nio.charset.Charset; + +import jakarta.servlet.http.HttpServletResponse; +import org.assertj.core.api.AbstractByteArrayAssert; +import org.assertj.core.api.AbstractStringAssert; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.http.converter.GenericHttpMessageConverter; +import org.springframework.lang.Nullable; +import org.springframework.test.json.JsonContentAssert; +import org.springframework.test.json.JsonPathAssert; + +/** + * AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied to + * the response body. + * + * @author Stephane Nicoll + * @author Brian Clozel + * @since 6.2 + */ +public class ResponseBodyAssert extends AbstractByteArrayAssert { + + private final Charset characterEncoding; + + @Nullable + private final GenericHttpMessageConverter jsonMessageConverter; + + ResponseBodyAssert(byte[] actual, Charset characterEncoding, + @Nullable GenericHttpMessageConverter jsonMessageConverter) { + + super(actual, ResponseBodyAssert.class); + this.characterEncoding = characterEncoding; + this.jsonMessageConverter = jsonMessageConverter; + as("Response body"); + } + + /** + * Return a new {@linkplain JsonPathAssert assertion} object that provides + * {@linkplain com.jayway.jsonpath.JsonPath JSON path} assertions on the + * response body. + */ + public JsonPathAssert jsonPath() { + return new JsonPathAssert(getJson(), this.jsonMessageConverter); + } + + /** + * Return a new {@linkplain JsonContentAssert assertion} object that + * provides {@linkplain org.skyscreamer.jsonassert.JSONCompareMode JSON + * assert} comparison to expected json input that can be loaded from the + * classpath. Only absolute locations are supported, consider using + * {@link #json(Class)} to load json documents relative to a given class. + * Example:

+	 * // Check that the response is strictly equal to the content of
+	 * // "/com/acme/web/person/person-created.json":
+	 * assertThat(...).body().json()
+	 *         .isStrictlyEqualToJson("/com/acme/web/person/person-created.json");
+	 * 
+ */ + public JsonContentAssert json() { + return json(null); + } + + /** + * Return a new {@linkplain JsonContentAssert assertion} object that + * provides {@linkplain org.skyscreamer.jsonassert.JSONCompareMode JSON + * assert} comparison to expected json input that can be loaded from the + * classpath. Documents can be absolute using a leading slash, or relative + * to the given {@code resourceLoadClass}. + * Example:

+	 * // Check that the response is strictly equal to the content of
+	 * // the specified file:
+	 * assertThat(...).body().json(PersonController.class)
+	 *         .isStrictlyEqualToJson("person-created.json");
+	 * 
+ * @param resourceLoadClass the class used to load relative json documents + * @see ClassPathResource#ClassPathResource(String, Class) + */ + public JsonContentAssert json(@Nullable Class resourceLoadClass) { + return new JsonContentAssert(getJson(), resourceLoadClass, this.characterEncoding); + } + + /** + * Verifies that the response body is equal to the given {@link String}. + *

Convert the actual byte array to a String using the character encoding + * of the {@link HttpServletResponse}. + * @param expected the expected content of the response body + * @see #asString() + */ + public ResponseBodyAssert isEqualTo(String expected) { + asString().isEqualTo(expected); + return this; + } + + /** + * Override that uses the character encoding of {@link HttpServletResponse} to + * convert the byte[] to a String, rather than the platform's default charset. + */ + @Override + public AbstractStringAssert asString() { + return asString(this.characterEncoding); + } + + private String getJson() { + return new String(this.actual, this.characterEncoding); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/package-info.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/package-info.java new file mode 100644 index 0000000000..6fe626a516 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/package-info.java @@ -0,0 +1,9 @@ +/** + * AssertJ support for MockMvc. + */ +@NonNullApi +@NonNullFields +package org.springframework.test.web.servlet.assertj; + +import org.springframework.lang.NonNullApi; +import org.springframework.lang.NonNullFields; diff --git a/spring-test/src/test/java/org/springframework/test/http/HttpHeadersAssertTests.java b/spring-test/src/test/java/org/springframework/test/http/HttpHeadersAssertTests.java new file mode 100644 index 0000000000..c82bb119a5 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/http/HttpHeadersAssertTests.java @@ -0,0 +1,190 @@ +/* + * 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.http; + +import java.time.Instant; +import java.util.List; +import java.util.Map; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +import org.springframework.http.HttpHeaders; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +/** + * Tests for {@link HttpHeadersAssert}. + * + * @author Stephane Nicoll + */ +class HttpHeadersAssertTests { + + @Test + void containsHeader() { + assertThat(Map.of("first", "1")).containsHeader("first"); + } + + @Test + void containsHeaderWithNameNotPresent() { + Map map = Map.of("first", "1"); + Assertions.assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(map).containsHeader("wrong-name")) + .withMessageContainingAll("HTTP headers", "first", "wrong-name"); + } + + @Test + void containsHeaders() { + assertThat(Map.of("first", "1", "second", "2", "third", "3")) + .containsHeaders("first", "third"); + } + + @Test + void containsHeadersWithSeveralNamesNotPresent() { + Map map = Map.of("first", "1", "second", "2", "third", "3"); + Assertions.assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(map).containsHeaders("first", "wrong-name", "another-wrong-name", "third")) + .withMessageContainingAll("HTTP headers", "first", "wrong-name", "another-wrong-name"); + } + + @Test + void doesNotContainsHeader() { + assertThat(Map.of("first", "1")).doesNotContainsHeader("second"); + } + + @Test + void doesNotContainsHeaderWithNamePresent() { + Map map = Map.of("first", "1"); + Assertions.assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(map).doesNotContainKey("first")) + .withMessageContainingAll("HTTP headers", "first"); + } + + @Test + void doesNotContainsHeaders() { + assertThat(Map.of("first", "1", "third", "3")) + .doesNotContainsHeaders("second", "fourth"); + } + + @Test + void doesNotContainsHeadersWithSeveralNamesPresent() { + Map map = Map.of("first", "1", "second", "2", "third", "3"); + Assertions.assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(map).doesNotContainsHeaders("first", "another-wrong-name", "second")) + .withMessageContainingAll("HTTP headers", "first", "second"); + } + + + @Test + void hasValueWithStringMatch() { + HttpHeaders headers = new HttpHeaders(); + headers.addAll("header", List.of("a", "b", "c")); + assertThat(headers).hasValue("header", "a"); + } + + @Test + void hasValueWithStringMatchOnSecondaryValue() { + HttpHeaders headers = new HttpHeaders(); + headers.addAll("header", List.of("first", "second", "third")); + Assertions.assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(headers).hasValue("header", "second")) + .withMessageContainingAll("check primary value for HTTP header 'header'", "first", "second"); + } + + @Test + void hasValueWithNoStringMatch() { + HttpHeaders headers = new HttpHeaders(); + headers.addAll("header", List.of("first", "second", "third")); + Assertions.assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(headers).hasValue("wrong-name", "second")) + .withMessageContainingAll("HTTP headers", "header", "wrong-name"); + } + + @Test + void hasValueWithNonPresentHeader() { + HttpHeaders map = new HttpHeaders(); + map.add("test-header", "a"); + Assertions.assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(map).hasValue("wrong-name", "a")) + .withMessageContainingAll("HTTP headers", "test-header", "wrong-name"); + } + + @Test + void hasValueWithLongMatch() { + HttpHeaders headers = new HttpHeaders(); + headers.addAll("header", List.of("123", "456", "789")); + assertThat(headers).hasValue("header", 123); + } + + @Test + void hasValueWithLongMatchOnSecondaryValue() { + HttpHeaders map = new HttpHeaders(); + map.addAll("header", List.of("123", "456", "789")); + Assertions.assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(map).hasValue("header", 456)) + .withMessageContainingAll("check primary long value for HTTP header 'header'", "123", "456"); + } + + @Test + void hasValueWithNoLongMatch() { + HttpHeaders map = new HttpHeaders(); + map.addAll("header", List.of("123", "456", "789")); + Assertions.assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(map).hasValue("wrong-name", 456)) + .withMessageContainingAll("HTTP headers", "header", "wrong-name"); + } + + @Test + void hasValueWithInstantMatch() { + Instant instant = Instant.now(); + HttpHeaders headers = new HttpHeaders(); + headers.setInstant("header", instant); + assertThat(headers).hasValue("header", instant); + } + + @Test + void hasValueWithNoInstantMatch() { + Instant instant = Instant.now(); + HttpHeaders map = new HttpHeaders(); + map.setInstant("header", instant); + Assertions.assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(map).hasValue("wrong-name", instant.minusSeconds(30))) + .withMessageContainingAll("HTTP headers", "header", "wrong-name"); + } + + @Test + void hasValueWithNoInstantMatchOneSecOfDifference() { + Instant instant = Instant.now(); + HttpHeaders map = new HttpHeaders(); + map.setInstant("header", instant); + Assertions.assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(map).hasValue("wrong-name", instant.minusSeconds(1))) + .withMessageContainingAll("HTTP headers", "header", "wrong-name"); + } + + + private static HttpHeadersAssert assertThat(Map values) { + MultiValueMap map = new LinkedMultiValueMap<>(); + values.forEach(map::add); + return assertThat(new HttpHeaders(map)); + } + + private static HttpHeadersAssert assertThat(HttpHeaders values) { + return new HttpHeadersAssert(values); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/http/MediaTypeAssertTests.java b/spring-test/src/test/java/org/springframework/test/http/MediaTypeAssertTests.java new file mode 100644 index 0000000000..232d25400f --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/http/MediaTypeAssertTests.java @@ -0,0 +1,157 @@ +/* + * 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.http; + + +import org.junit.jupiter.api.Test; + +import org.springframework.http.MediaType; +import org.springframework.lang.Nullable; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link MediaTypeAssert}. + * + * @author Brian Clozel + * @author Stephane Nicoll + */ +class MediaTypeAssertTests { + + @Test + void actualCanBeNull() { + new MediaTypeAssert((MediaType) null).isNull(); + } + + @Test + void actualStringCanBeNull() { + new MediaTypeAssert((String) null).isNull(); + } + + @Test + void isEqualWhenSameShouldPass() { + assertThat(mediaType("application/json")).isEqualTo("application/json"); + } + + @Test + void isEqualWhenDifferentShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(mediaType("application/json")).isEqualTo("text/html")) + .withMessageContaining("Media type"); + } + + @Test + void isEqualWhenActualIsNullShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(null).isEqualTo(MediaType.APPLICATION_JSON)) + .withMessageContaining("Media type"); + } + + @Test + void isEqualWhenSameTypeShouldPass() { + assertThat(mediaType("application/json")).isEqualTo(MediaType.APPLICATION_JSON); + } + + @Test + void isEqualWhenDifferentTypeShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(mediaType("application/json")).isEqualTo(MediaType.TEXT_HTML)) + .withMessageContaining("Media type"); + } + + @Test + void isCompatibleWhenSameShouldPass() { + assertThat(mediaType("application/json")).isCompatibleWith("application/json"); + } + + @Test + void isCompatibleWhenCompatibleShouldPass() { + assertThat(mediaType("application/json")).isCompatibleWith("application/*"); + } + + @Test + void isCompatibleWhenDifferentShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(mediaType("application/json")).isCompatibleWith("text/html")) + .withMessageContaining("check media type 'application/json' is compatible with 'text/html'"); + } + + @Test + void isCompatibleWithStringAndNullActual() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(null).isCompatibleWith("text/html")) + .withMessageContaining("Expecting null to be compatible with 'text/html'"); + } + + @Test + void isCompatibleWithStringAndNullExpected() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(mediaType("application/json")).isCompatibleWith((String) null)) + .withMessageContainingAll("Expecting:", "null", "To be a valid media type but got:", + "'mimeType' must not be empty"); + } + + @Test + void isCompatibleWithStringAndEmptyExpected() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(mediaType("application/json")).isCompatibleWith("")) + .withMessageContainingAll("Expecting:", "", "To be a valid media type but got:", + "'mimeType' must not be empty"); + } + + @Test + void isCompatibleWithMediaTypeAndNullActual() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(null).isCompatibleWith(MediaType.TEXT_HTML)) + .withMessageContaining("Expecting null to be compatible with 'text/html'"); + } + + @Test + void isCompatibleWithMediaTypeAndNullExpected() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(mediaType("application/json")).isCompatibleWith((MediaType) null)) + .withMessageContaining("Expecting 'application/json' to be compatible with null"); + } + + @Test + void isCompatibleWhenSameTypeShouldPass() { + assertThat(mediaType("application/json")).isCompatibleWith(MediaType.APPLICATION_JSON); + } + + @Test + void isCompatibleWhenCompatibleTypeShouldPass() { + assertThat(mediaType("application/json")).isCompatibleWith(MediaType.parseMediaType("application/*")); + } + + @Test + void isCompatibleWhenDifferentTypeShouldFail() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(mediaType("application/json")).isCompatibleWith(MediaType.TEXT_HTML)) + .withMessageContaining("check media type 'application/json' is compatible with 'text/html'"); + } + + + @Nullable + private static MediaType mediaType(@Nullable String mediaType) { + return (mediaType != null ? MediaType.parseMediaType(mediaType) : null); + } + + private static MediaTypeAssert assertThat(@Nullable MediaType mediaType) { + return new MediaTypeAssert(mediaType); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/UriAssertTests.java b/spring-test/src/test/java/org/springframework/test/web/UriAssertTests.java new file mode 100644 index 0000000000..f5eacdd1cf --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/UriAssertTests.java @@ -0,0 +1,77 @@ +/* + * 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; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link UriAssert}. + * + * @author Stephane Nicoll + */ +class UriAssertTests { + + @Test + void isEqualToTemplate() { + assertThat("/orders/1/items/2").isEqualToTemplate("/orders/{orderId}/items/{itemId}", 1, 2); + } + + @Test + void isEqualToTemplateWithWrongValue() { + String expected = "/orders/1/items/3"; + String actual = "/orders/1/items/2"; + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(expected).isEqualToTemplate("/orders/{orderId}/items/{itemId}", 1, 2)) + .withMessageContainingAll("Test URI", expected, actual); + } + + @Test + void isEqualToTemplateMissingArg() { + String template = "/orders/{orderId}/items/{itemId}"; + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat("/orders/1/items/2").isEqualToTemplate(template, 1)) + .withMessageContainingAll("Expecting:", template, + "Not enough variable values available to expand 'itemId'"); + } + + @Test + void matchPattern() { + assertThat("/orders/1").matchPattern("/orders/*"); + } + + @Test + void matchPatternWithNonValidPattern() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat("/orders/1").matchPattern("/orders/")) + .withMessage("'/orders/' is not an Ant-style path pattern"); + } + + @Test + void matchPatternWithWrongValue() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat("/orders/1").matchPattern("/resources/*")) + .withMessageContainingAll("Test URI", "/resources/*", "/orders/1"); + } + + + UriAssert assertThat(String uri) { + return new UriAssert(uri, "Test URI"); + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletRequestAssertTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletRequestAssertTests.java new file mode 100644 index 0000000000..01c6a06fb7 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletRequestAssertTests.java @@ -0,0 +1,143 @@ +/* + * 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.servlet.assertj; + +import java.util.LinkedHashMap; +import java.util.Map; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.mock.web.MockHttpServletRequest; + +import static java.util.Map.entry; + +/** + * Tests for {@link AbstractHttpServletRequestAssert}. + * + * @author Stephane Nicoll + */ +public class AbstractHttpServletRequestAssertTests { + + + @Nested + class AttributesTests { + + @Test + void attributesAreCopied() { + Map map = new LinkedHashMap<>(); + map.put("one", 1); + map.put("two", 2); + assertThat(createRequest(map)).attributes() + .containsExactly(entry("one", 1), entry("two", 2)); + } + + @Test + void attributesWithWrongKey() { + HttpServletRequest request = createRequest(Map.of("one", 1)); + Assertions.assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(request).attributes().containsKey("two")) + .withMessageContainingAll("Request Attributes", "two", "one"); + } + + private HttpServletRequest createRequest(Map attributes) { + MockHttpServletRequest request = new MockHttpServletRequest(); + attributes.forEach(request::setAttribute); + return request; + } + + } + + @Nested + class SessionAttributesTests { + + @Test + void sessionAttributesAreCopied() { + Map map = new LinkedHashMap<>(); + map.put("one", 1); + map.put("two", 2); + assertThat(createRequest(map)).sessionAttributes() + .containsExactly(entry("one", 1), entry("two", 2)); + } + + @Test + void sessionAttributesWithWrongKey() { + HttpServletRequest request = createRequest(Map.of("one", 1)); + Assertions.assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(request).sessionAttributes().containsKey("two")) + .withMessageContainingAll("Session Attributes", "two", "one"); + } + + + private HttpServletRequest createRequest(Map attributes) { + MockHttpServletRequest request = new MockHttpServletRequest(); + HttpSession session = request.getSession(); + Assertions.assertThat(session).isNotNull(); + attributes.forEach(session::setAttribute); + return request; + } + + } + + @Test + void hasAsyncStartedTrue() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setAsyncStarted(true); + assertThat(request).hasAsyncStarted(true); + } + + @Test + void hasAsyncStartedTrueWithFalse() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setAsyncStarted(false); + Assertions.assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(request).hasAsyncStarted(true)) + .withMessage("Async expected to have started"); + } + + @Test + void hasAsyncStartedFalse() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setAsyncStarted(false); + assertThat(request).hasAsyncStarted(false); + } + + @Test + void hasAsyncStartedFalseWithTrue() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setAsyncStarted(true); + Assertions.assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(request).hasAsyncStarted(false)) + .withMessage("Async expected to not have started"); + + } + + private static ResponseAssert assertThat(HttpServletRequest response) { + return new ResponseAssert(response); + } + + + private static final class ResponseAssert extends AbstractHttpServletRequestAssert { + + ResponseAssert(HttpServletRequest actual) { + super(actual, ResponseAssert.class); + } + } +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletResponseAssertTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletResponseAssertTests.java new file mode 100644 index 0000000000..3c8aee938c --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractHttpServletResponseAssertTests.java @@ -0,0 +1,138 @@ +/* + * 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.servlet.assertj; + +import java.util.Map; + +import jakarta.servlet.http.HttpServletResponse; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.MockHttpServletResponse; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link AbstractHttpServletResponseAssert}. + * + * @author Stephane Nicoll + */ +class AbstractHttpServletResponseAssertTests { + + @Nested + class HeadersTests { + + @Test + void headersAreMatching() { + MockHttpServletResponse response = createResponse(Map.of("n1", "v1", "n2", "v2", "n3", "v3")); + assertThat(response).headers().containsHeaders("n1", "n2", "n3"); + } + + + private MockHttpServletResponse createResponse(Map headers) { + MockHttpServletResponse response = new MockHttpServletResponse(); + headers.forEach(response::addHeader); + return response; + } + } + + + @Nested + class StatusTests { + + @Test + void hasStatusWithCode() { + assertThat(createResponse(200)).hasStatus(200); + } + + @Test + void hasStatusWithHttpStatus() { + assertThat(createResponse(200)).hasStatus(HttpStatus.OK); + } + + @Test + void hasStatusOK() { + assertThat(createResponse(200)).hasStatusOk(); + } + + @Test + void hasStatusWithWrongCode() { + MockHttpServletResponse response = createResponse(200); + assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertThat(response).hasStatus(300)) + .withMessageContainingAll("HTTP status code", "200", "300"); + } + + @Test + void hasStatus1xxInformational() { + assertThat(createResponse(199)).hasStatus1xxInformational(); + } + + @Test + void hasStatus2xxSuccessful() { + assertThat(createResponse(299)).hasStatus2xxSuccessful(); + } + + @Test + void hasStatus3xxRedirection() { + assertThat(createResponse(399)).hasStatus3xxRedirection(); + } + + @Test + void hasStatus4xxClientError() { + assertThat(createResponse(499)).hasStatus4xxClientError(); + } + + @Test + void hasStatus5xxServerError() { + assertThat(createResponse(599)).hasStatus5xxServerError(); + } + + @Test + void hasStatusWithWrongSeries() { + MockHttpServletResponse response = createResponse(500); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(response).hasStatus2xxSuccessful()) + .withMessageContainingAll("HTTP status series", "SUCCESSFUL", "SERVER_ERROR"); + } + + private MockHttpServletResponse createResponse(int status) { + MockHttpServletResponse response = new MockHttpServletResponse(); + response.setStatus(status); + return response; + } + } + + private static ResponseAssert assertThat(HttpServletResponse response) { + return new ResponseAssert(response); + } + + + private static final class ResponseAssert extends AbstractHttpServletResponseAssert { + + ResponseAssert(HttpServletResponse actual) { + super(actual, ResponseAssert.class); + } + + @Override + protected HttpServletResponse getResponse() { + return this.actual; + } + + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletRequestAssertTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletRequestAssertTests.java new file mode 100644 index 0000000000..d1c5087660 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletRequestAssertTests.java @@ -0,0 +1,48 @@ +/* + * 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.servlet.assertj; + +import org.junit.jupiter.api.Test; + +import org.springframework.mock.web.MockHttpServletRequest; + +/** + * Tests for {@link AbstractMockHttpServletRequestAssert}. + * + * @author Stephane Nicoll + */ +class AbstractMockHttpServletRequestAssertTests { + + @Test + void requestCanBeAsserted() { + MockHttpServletRequest request = new MockHttpServletRequest(); + assertThat(request).satisfies(actual -> assertThat(actual).isSameAs(request)); + } + + + private static RequestAssert assertThat(MockHttpServletRequest request) { + return new RequestAssert(request); + } + + private static final class RequestAssert extends AbstractMockHttpServletRequestAssert { + + RequestAssert(MockHttpServletRequest actual) { + super(actual, RequestAssert.class); + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssertTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssertTests.java new file mode 100644 index 0000000000..badfb6d4f6 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssertTests.java @@ -0,0 +1,106 @@ +/* + * 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.servlet.assertj; + + +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +import org.springframework.http.HttpHeaders; +import org.springframework.mock.web.MockHttpServletResponse; + +/** + * Tests for {@link AbstractMockHttpServletResponseAssert}. + * + * @author Stephane Nicoll + */ +public class AbstractMockHttpServletResponseAssertTests { + + @Test + void hasForwardedUrl() { + String forwardedUrl = "https://example.com/42"; + MockHttpServletResponse response = new MockHttpServletResponse(); + response.setForwardedUrl(forwardedUrl); + assertThat(response).hasForwardedUrl(forwardedUrl); + } + + @Test + void hasForwardedUrlWithWrongValue() { + String forwardedUrl = "https://example.com/42"; + MockHttpServletResponse response = new MockHttpServletResponse(); + response.setForwardedUrl(forwardedUrl); + Assertions.assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(response).hasForwardedUrl("another")) + .withMessageContainingAll("Forwarded URL", forwardedUrl, "another"); + } + + @Test + void hasRedirectedUrl() { + String redirectedUrl = "https://example.com/42"; + MockHttpServletResponse response = new MockHttpServletResponse(); + response.addHeader(HttpHeaders.LOCATION, redirectedUrl); + assertThat(response).hasRedirectedUrl(redirectedUrl); + } + + @Test + void hasRedirectedUrlWithWrongValue() { + String redirectedUrl = "https://example.com/42"; + MockHttpServletResponse response = new MockHttpServletResponse(); + response.addHeader(HttpHeaders.LOCATION, redirectedUrl); + Assertions.assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(response).hasRedirectedUrl("another")) + .withMessageContainingAll("Redirected URL", redirectedUrl, "another"); + } + + @Test + void bodyHasContent() throws UnsupportedEncodingException { + MockHttpServletResponse response = new MockHttpServletResponse(); + response.getWriter().write("OK"); + assertThat(response).body().asString().isEqualTo("OK"); + } + + @Test + void bodyHasContentWithResponseCharacterEncoding() throws UnsupportedEncodingException { + byte[] bytes = "OK".getBytes(StandardCharsets.UTF_8); + MockHttpServletResponse response = new MockHttpServletResponse(); + response.getWriter().write("OK"); + response.setContentType(StandardCharsets.UTF_8.name()); + assertThat(response).body().isEqualTo(bytes); + } + + + private static ResponseAssert assertThat(MockHttpServletResponse response) { + return new ResponseAssert(response); + } + + + private static final class ResponseAssert extends AbstractMockHttpServletResponseAssert { + + ResponseAssert(MockHttpServletResponse actual) { + super(null, actual, ResponseAssert.class); + } + + @Override + protected MockHttpServletResponse getResponse() { + return this.actual; + } + + } +} diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssertTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssertTests.java new file mode 100644 index 0000000000..0284636c3d --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/ResponseBodyAssertTests.java @@ -0,0 +1,88 @@ +/* + * 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.servlet.assertj; + + +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import org.assertj.core.api.AssertProvider; +import org.junit.jupiter.api.Test; + +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.json.JsonContent; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ResponseBodyAssert}. + * + * @author Brian Clozel + * @author Stephane Nicoll + */ +class ResponseBodyAssertTests { + + @Test + void isEqualToWithByteArray() { + MockHttpServletResponse response = createResponse("hello"); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + assertThat(fromResponse(response)).isEqualTo("hello".getBytes(StandardCharsets.UTF_8)); + } + + @Test + void isEqualToWithString() { + MockHttpServletResponse response = createResponse("hello"); + assertThat(fromResponse(response)).isEqualTo("hello"); + } + + @Test + void jsonPathWithJsonResponseShouldPass() { + MockHttpServletResponse response = createResponse("{\"message\": \"hello\"}"); + assertThat(fromResponse(response)).jsonPath().extractingPath("$.message").isEqualTo("hello"); + } + + @Test + void jsonPathWithJsonCompatibleResponseShouldPass() { + MockHttpServletResponse response = createResponse("{\"albumById\": {\"name\": \"Greatest hits\"}}"); + assertThat(fromResponse(response)).jsonPath() + .extractingPath("$.albumById.name").isEqualTo("Greatest hits"); + } + + @Test + void jsonCanLoadResourceRelativeToClass() { + MockHttpServletResponse response = createResponse("{ \"name\" : \"Spring\", \"age\" : 123 }"); + // See org/springframework/test/json/example.json + assertThat(fromResponse(response)).json(JsonContent.class).isLenientlyEqualTo("example.json"); + } + + private MockHttpServletResponse createResponse(String body) { + try { + MockHttpServletResponse response = new MockHttpServletResponse(); + response.getWriter().print(body); + return response; + } + catch (UnsupportedEncodingException ex) { + throw new IllegalStateException(ex); + } + } + + private AssertProvider fromResponse(MockHttpServletResponse response) { + return () -> new ResponseBodyAssert(response.getContentAsByteArray(), Charset.forName(response.getCharacterEncoding()), null); + } + +}