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); + } + +}