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 index 27f6ae5acab..8e6552e4d35 100644 --- a/spring-test/src/main/java/org/springframework/test/http/HttpHeadersAssert.java +++ b/spring-test/src/main/java/org/springframework/test/http/HttpHeadersAssert.java @@ -23,13 +23,12 @@ import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.function.Consumer; import org.assertj.core.api.AbstractCollectionAssert; import org.assertj.core.api.AbstractObjectAssert; import org.assertj.core.api.Assertions; import org.assertj.core.api.ObjectAssert; -import org.assertj.core.presentation.Representation; -import org.assertj.core.presentation.StandardRepresentation; import org.springframework.http.HttpHeaders; @@ -38,6 +37,7 @@ import org.springframework.http.HttpHeaders; * {@link HttpHeaders}. * * @author Stephane Nicoll + * @author Simon Baslé * @since 6.2 */ public class HttpHeadersAssert extends AbstractObjectAssert { @@ -50,15 +50,6 @@ public class HttpHeadersAssert extends AbstractObjectAssert> valueRequirements) { + containsHeader(name); + Assertions.assertThat(this.actual.get(name)) + .as("check primary value for HTTP header '%s'", name) + .satisfies(values -> valueRequirements.accept((List) values)); + return this.myself; + } + + /** + * Verify that the actual HTTP headers contain a header with the given + * {@code name} and {@link String} primary {@code value}. * @param name the name of the cookie * @param value the expected value of the header */ @@ -141,7 +148,7 @@ public class HttpHeadersAssert extends AbstractObjectAssertThis assertion fails if the header has secondary values. + * @param name the name of the cookie + * @param value the expected only value of the header + * @since 7.0 + */ + public HttpHeadersAssert hasSingleValue(String name, String value) { + doesNotHaveSecondaryValues(name); + return hasValue(name, value); + } + + /** + * Verify that the actual HTTP headers contain a header with the given + * {@code name} and {@link Long} primary {@code value}. + *

This assertion fails if the header has secondary values. + * @param name the name of the cookie + * @param value the expected value of the header + * @since 7.0 + */ + public HttpHeadersAssert hasSingleValue(String name, long value) { + doesNotHaveSecondaryValues(name); + return hasValue(name, value); + } + + /** + * Verify that the actual HTTP headers contain a header with the given + * {@code name} and {@link Instant} primary {@code value}. + *

This assertion fails if the header has secondary values. + * @param name the name of the cookie + * @param value the expected value of the header + * @since 7.0 + */ + public HttpHeadersAssert hasSingleValue(String name, Instant value) { + doesNotHaveSecondaryValues(name); + return hasValue(name, value); + } + + /** * Verify that the given header has a full list of values exactly equal to * the given list of values, and in the same order. @@ -224,80 +271,13 @@ public class HttpHeadersAssert extends AbstractObjectAssert '%i'", boundary) - .hasSizeGreaterThan(boundary); - return this.myself; - } - - /** - * Verify that the number of headers present is greater or equal to the - * given boundary, when considering header names in a case-insensitive - * manner. - * @param boundary the given value to compare actual header size to - */ - public HttpHeadersAssert hasSizeGreaterThanOrEqualTo(int boundary) { - this.namesAssert - .as("check headers have size >= '%i'", boundary) - .hasSizeGreaterThanOrEqualTo(boundary); - return this.myself; - } - - /** - * Verify that the number of headers present is strictly less than the - * given boundary, when considering header names in a case-insensitive - * manner. - * @param boundary the given value to compare actual header size to - */ - public HttpHeadersAssert hasSizeLessThan(int boundary) { - this.namesAssert - .as("check headers have size < '%i'", boundary) - .hasSizeLessThan(boundary); - return this.myself; - } - - /** - * Verify that the number of headers present is less than or equal to the - * given boundary, when considering header names in a case-insensitive - * manner. - * @param boundary the given value to compare actual header size to - */ - public HttpHeadersAssert hasSizeLessThanOrEqualTo(int boundary) { - this.namesAssert - .as("check headers have size <= '%i'", boundary) - .hasSizeLessThanOrEqualTo(boundary); - return this.myself; - } - - /** - * Verify that the number of headers present is between the given boundaries - * (inclusive), when considering header names in a case-insensitive manner. - * @param lowerBoundary the lower boundary compared to which actual size - * should be greater than or equal to - * @param higherBoundary the higher boundary compared to which actual size - * should be less than or equal to - */ - public HttpHeadersAssert hasSizeBetween(int lowerBoundary, int higherBoundary) { - this.namesAssert - .as("check headers have size between '%i' and '%i'", lowerBoundary, higherBoundary) - .hasSizeBetween(lowerBoundary, higherBoundary); - return this.myself; - } - - /** - * Verify that the number actual headers is the same as in the given + * Verify that the number of actual headers is the same as in the given * {@code HttpHeaders}. * @param other the {@code HttpHeaders} to compare size with * @since 7.0 @@ -308,4 +288,17 @@ public class HttpHeadersAssert extends AbstractObjectAssert values = this.actual.get(name); + int size = (values != null) ? values.size() : 0; + Assertions.assertThat(size) + .withFailMessage("Expected HTTP header '%s' to be present " + + "without secondary values, but found <%s> secondary values", name, size - 1) + .isOne(); + return this.myself; + } + } 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 index 93b75c61dd3..195543acac0 100644 --- a/spring-test/src/test/java/org/springframework/test/http/HttpHeadersAssertTests.java +++ b/spring-test/src/test/java/org/springframework/test/http/HttpHeadersAssertTests.java @@ -19,7 +19,9 @@ package org.springframework.test.http; import java.time.Instant; import java.util.List; import java.util.Map; +import java.util.function.Consumer; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; @@ -27,11 +29,16 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; /** * Tests for {@link HttpHeadersAssert}. * * @author Stephane Nicoll + * @author Simon Baslé */ class HttpHeadersAssertTests { @@ -62,6 +69,37 @@ class HttpHeadersAssertTests { .withMessageContainingAll("HTTP headers", "first", "wrong-name", "another-wrong-name"); } + @Test + void containsOnlyHeaders() { + HttpHeaders headers = new HttpHeaders(); + headers.add("name1", "value1"); + headers.add("name2", "value2"); + assertThat(headers).containsOnlyHeaders("name2", "name1"); + } + + @Test + void containsOnlyHeadersWithMissingOne() { + HttpHeaders headers = new HttpHeaders(); + headers.add("name1", "value1"); + headers.add("name2", "value2"); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(headers).containsOnlyHeaders("name1", "name2", "name3")) + .withMessageContainingAll("check headers contains only HTTP headers", + "could not find the following element(s)", "[\"name3\"]"); + } + + @Test + void containsOnlyHeadersWithExtraOne() { + HttpHeaders headers = new HttpHeaders(); + headers.add("name1", "value1"); + headers.add("name2", "value2"); + headers.add("name3", "value3"); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(headers).containsOnlyHeaders("name1", "name2")) + .withMessageContainingAll("check headers contains only HTTP headers", + "the following element(s) were unexpected", "[\"name3\"]"); + } + @Test void doesNotContainHeader() { assertThat(Map.of("first", "1")).doesNotContainHeader("second"); @@ -89,6 +127,36 @@ class HttpHeadersAssertTests { .withMessageContainingAll("HTTP headers", "first", "second"); } + @Test + @SuppressWarnings("unchecked") + void hasHeaderSatisfying() { + HttpHeaders headers = new HttpHeaders(); + headers.addAll("header", List.of("first", "second", "third")); + Consumer> mock = mock(Consumer.class); + assertThatNoException().isThrownBy(() -> assertThat(headers).hasHeaderSatisfying("header", mock)); + verify(mock).accept(List.of("first", "second", "third")); + } + + @Test + void hasHeaderSatisfyingWithExceptionInConsumer() { + HttpHeaders headers = new HttpHeaders(); + headers.addAll("header", List.of("first", "second", "third")); + IllegalStateException testException = new IllegalStateException("test"); + assertThatIllegalStateException() + .isThrownBy(() -> assertThat(headers).hasHeaderSatisfying("header", values -> { + throw testException; + })).isEqualTo(testException); + } + + @Test + void hasHeaderSatisfyingWithFailingAssertion() { + HttpHeaders headers = new HttpHeaders(); + headers.addAll("header", List.of("first", "second", "third")); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(headers).hasHeaderSatisfying("header", values -> + Assertions.assertThat(values).hasSize(42))) + .withMessageContainingAll("HTTP header", "header", "first", "second", "third", "42", "3"); + } @Test void hasValueWithStringMatch() { @@ -177,6 +245,210 @@ class HttpHeadersAssertTests { .withMessageContainingAll("HTTP header", "header", "wrong-name"); } + @Test + void hasSingleValueWithStringMatch() { + HttpHeaders headers = new HttpHeaders(); + headers.add("header", "a"); + assertThat(headers).hasSingleValue("header", "a"); + } + + @Test + void hasSingleValueWithSecondaryValues() { + HttpHeaders headers = new HttpHeaders(); + headers.addAll("header", List.of("first", "second", "third")); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(headers).hasSingleValue("header", "first")) + .withMessage("Expected HTTP header 'header' to be present without secondary values, " + + "but found <2> secondary values"); + } + + @Test + void hasSingleValueWithLongMatch() { + HttpHeaders headers = new HttpHeaders(); + headers.add("header", "123"); + assertThat(headers).hasSingleValue("header", 123); + } + + @Test + void hasSingleValueWithLongMatchButSecondaryValues() { + HttpHeaders headers = new HttpHeaders(); + headers.addAll("header", List.of("123", "456", "789")); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(headers).hasSingleValue("header", 123)) + .withMessage("Expected HTTP header 'header' to be present without secondary values, " + + "but found <2> secondary values"); + } + + @Test + void hasSingleValueWithInstantMatch() { + Instant instant = Instant.now(); + HttpHeaders headers = new HttpHeaders(); + headers.setInstant("header", instant); + assertThat(headers).hasSingleValue("header", instant); + } + + @Test + void hasSingleValueWithInstantAndSecondaryValues() { + Instant instant = Instant.now(); + HttpHeaders headers = new HttpHeaders(); + headers.setInstant("header", instant); + headers.add("header", "second"); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(headers).hasSingleValue("header", instant.minusSeconds(30))) + .withMessage("Expected HTTP header 'header' to be present without secondary values, " + + "but found <1> secondary values"); + } + + @Test + void hasExactlyValues() { + HttpHeaders headers = new HttpHeaders(); + headers.addAll("name", List.of("value1", "value2")); + headers.add("otherName", "otherValue"); + assertThat(headers).hasExactlyValues("name", List.of("value1", "value2")); + } + + @Test + void hasExactlyValuesWithMissingOne() { + HttpHeaders headers = new HttpHeaders(); + headers.addAll("name", List.of("value1", "value2")); + headers.add("otherName", "otherValue"); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(headers).hasExactlyValues("name", List.of("value1", "value2", "value3"))) + .withMessageContainingAll("check all values of HTTP header 'name'", + "to contain exactly (and in same order)", + "could not find the following elements", "[\"value3\"]"); + } + + @Test + void hasExactlyValuesWithExtraOne() { + HttpHeaders headers = new HttpHeaders(); + headers.addAll("name", List.of("value1", "value2")); + headers.add("otherName", "otherValue"); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(headers).hasExactlyValues("name", List.of("value1"))) + .withMessageContainingAll("check all values of HTTP header 'name'", + "to contain exactly (and in same order)", + "some elements were not expected", "[\"value2\"]"); + } + + @Test + void hasExactlyValuesWithWrongOrder() { + HttpHeaders headers = new HttpHeaders(); + headers.addAll("name", List.of("value1", "value2")); + headers.add("otherName", "otherValue"); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(headers).hasExactlyValues("name", List.of("value2", "value1"))) + .withMessageContainingAll("check all values of HTTP header 'name'", + "to contain exactly (and in same order)", + "there were differences at these indexes", + "element at index 0: expected \"value2\" but was \"value1\"", + "element at index 1: expected \"value1\" but was \"value2\""); + } + + @Test + void hasExactlyValuesInAnyOrder() { + HttpHeaders headers = new HttpHeaders(); + headers.addAll("name", List.of("value1", "value2")); + headers.add("otherName", "otherValue"); + assertThat(headers).hasExactlyValuesInAnyOrder("name", List.of("value1", "value2")); + } + + @Test + void hasExactlyValuesInAnyOrderWithDifferentOrder() { + HttpHeaders headers = new HttpHeaders(); + headers.addAll("name", List.of("value1", "value2")); + headers.add("otherName", "otherValue"); + assertThat(headers).hasExactlyValuesInAnyOrder("name", List.of("value2", "value1")); + } + + @Test + void hasExactlyValuesInAnyOrderWithMissingOne() { + HttpHeaders headers = new HttpHeaders(); + headers.addAll("name", List.of("value1", "value2")); + headers.add("otherName", "otherValue"); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(headers).hasExactlyValuesInAnyOrder("name", + List.of("value1", "value2", "value3"))) + .withMessageContainingAll("check all values of HTTP header 'name'", + "to contain exactly in any order", + "could not find the following elements", "[\"value3\"]"); + } + + @Test + void hasExactlyValuesInAnyOrderWithExtraOne() { + HttpHeaders headers = new HttpHeaders(); + headers.addAll("name", List.of("value1", "value2")); + headers.add("otherName", "otherValue"); + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(headers).hasExactlyValuesInAnyOrder("name", List.of("value1"))) + .withMessageContainingAll("check all values of HTTP header 'name'", + "to contain exactly in any order", + "the following elements were unexpected", "[\"value2\"]"); + } + + @Test + void isEmpty() { + assertThat(new HttpHeaders()).isEmpty(); + } + + @Test + void isEmptyWithHeaders() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(Map.of("first", "1", "second", "2")).isEmpty()) + .withMessageContainingAll("check headers are empty", "Expecting empty", "first", "second"); + } + + @Test + void isNotEmpty() { + assertThat(Map.of("header", "value")).isNotEmpty(); + } + + @Test + void isNotEmptyWithNoHeaders() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(new HttpHeaders()).isNotEmpty()) + .withMessageContainingAll("check headers are not empty", "Expecting actual not to be empty"); + } + + @Test + void hasSize() { + assertThat(Map.of("first", "1", "second", "2")).hasSize(2); + } + + @Test + void hasSizeWithWrongSize() { + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(Map.of("first", "1")).hasSize(42)) + .withMessageContainingAll("check headers have size '42'", "1"); + } + + @Test + void hasSameSizeAs() { + HttpHeaders headers = new HttpHeaders(); + headers.add("name1", "value1"); + headers.add("name2", "value2"); + + HttpHeaders other = new HttpHeaders(); + other.add("name3", "value3"); + other.add("name4", "value4"); + + assertThat(headers).hasSameSizeAs(other); + } + + @Test + void hasSameSizeAsWithSmallerOther() { + HttpHeaders headers = new HttpHeaders(); + headers.add("name1", "value1"); + headers.add("name2", "value2"); + + HttpHeaders other = new HttpHeaders(); + other.add("name3", "value3"); + + assertThatExceptionOfType(AssertionError.class) + .isThrownBy(() -> assertThat(headers).hasSameSizeAs(other)) + .withMessageContainingAll("check headers have same size as '", other.toString(), + "Expected size: 1 but was: 2"); + } private static HttpHeadersAssert assertThat(Map values) { MultiValueMap map = new LinkedMultiValueMap<>(); diff --git a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java index 912e89f5a72..26bbf0e2c11 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java +++ b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -1921,12 +1921,14 @@ public class HttpHeaders implements Serializable { } /** - * Get the list of values associated with the given header name. + * Get the list of values associated with the given header name, or null. + *

To ensure support for double-quoted values, see also + * {@link #getValuesAsList(String)}. * @param headerName the header name * @since 7.0 + * @see #getValuesAsList(String) */ - @Nullable - public List get(String headerName) { + public @Nullable List get(String headerName) { return this.headers.get(headerName); }