From 1948161731438ec2dc1c7ac04d3151fbab402576 Mon Sep 17 00:00:00 2001 From: Scott Leberknight <174812+sleberknight@users.noreply.github.com> Date: Wed, 2 Feb 2022 11:46:17 -0500 Subject: [PATCH] Add methods to KiwiResources to validate MultivaluedMaps (#667) * Add validateOneIntParameter * Add validateExactlyOneIntParameter * Add validateOneOrMoreIntParameters * Add validateOneLongParameter * Add validateExactlyOneLongParameter * Add validateOneOrMoreLongParameters * Modify existing validateIntParameter and validateLongParameter to throw IllegalArgumentException if given a null parameter map (#666) Closes #604 Closes #666 --- .../org/kiwiproject/jaxrs/KiwiResources.java | 214 +++++++- .../kiwiproject/jaxrs/KiwiResourcesTest.java | 511 +++++++++++++++++- 2 files changed, 714 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/kiwiproject/jaxrs/KiwiResources.java b/src/main/java/org/kiwiproject/jaxrs/KiwiResources.java index 1d09705a..d4d2b1ed 100644 --- a/src/main/java/org/kiwiproject/jaxrs/KiwiResources.java +++ b/src/main/java/org/kiwiproject/jaxrs/KiwiResources.java @@ -1,9 +1,16 @@ package org.kiwiproject.jaxrs; import static java.util.Objects.isNull; +import static java.util.Objects.nonNull; +import static java.util.stream.Collectors.toUnmodifiableList; +import static org.kiwiproject.base.KiwiPreconditions.checkArgumentNotBlank; +import static org.kiwiproject.base.KiwiPreconditions.checkArgumentNotNull; import static org.kiwiproject.base.KiwiStrings.f; +import static org.kiwiproject.collect.KiwiLists.first; +import static org.kiwiproject.collect.KiwiLists.isNullOrEmpty; import static org.kiwiproject.jaxrs.KiwiJaxrsValidations.assertNotNull; +import com.google.common.annotations.VisibleForTesting; import com.google.common.primitives.Ints; import com.google.common.primitives.Longs; import lombok.experimental.UtilityClass; @@ -15,6 +22,7 @@ import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import java.net.URI; +import java.util.List; import java.util.Map; import java.util.Optional; @@ -38,6 +46,8 @@ public class KiwiResources { private static final Map EMPTY_HEADERS = Map.of(); + private static final String PARAMETERS_MUST_NOT_BE_NULL = "parameters must not be null"; + private static final String PARAMETER_NAME_MUST_NOT_BE_BLANK = "parameterName must not be blank"; /** * Verifies that {@code resourceEntity} is not null, otherwise throws a {@link JaxrsNotFoundException}. @@ -380,16 +390,93 @@ public static Response.ResponseBuilder newResponseBuilderBufferingEntityFrom(Res * @throws JaxrsBadRequestException if the specified parameter is not present, or is not an integer */ public static int validateIntParameter(Map parameters, String parameterName) { - assertNotNull(parameterName, parameters); + checkArgumentNotNull(parameters, PARAMETERS_MUST_NOT_BE_NULL); + checkArgumentNotBlank(parameterName, PARAMETER_NAME_MUST_NOT_BE_BLANK); var value = parameters.get(parameterName); assertNotNull(parameterName, value); + return parseIntOrThrowBadRequest(value, parameterName); + } + + /** + * Checks whether {@code parameters} contains a parameter named {@code parameterName} that has at least one + * value that can be converted into an integer. If there is more than one value, then the first one returned + * by {@link MultivaluedMap#getFirst(Object)} is returned. + * + * @param parameters the multivalued parameters to check + * @param parameterName name of the parameter which should be present + * @param the type of values in the multivalued map + * @return the int value of the validated parameter + * @throws JaxrsBadRequestException if the specified parameter is not present with at least one value, or is + * not an integer + */ + public static int validateOneIntParameter(MultivaluedMap parameters, + String parameterName) { + checkArgumentNotNull(parameters, PARAMETERS_MUST_NOT_BE_NULL); + checkArgumentNotBlank(parameterName, PARAMETER_NAME_MUST_NOT_BE_BLANK); + + var value = parameters.getFirst(parameterName); + assertNotNull(parameterName, value); + + return parseIntOrThrowBadRequest(value, parameterName); + } + + /** + * Checks whether {@code parameters} contains a parameter named {@code parameterName} that has exactly one + * value that can be converted into an integer. If there is more than one value, this is considered a bad request + * and a {@link JaxrsBadRequestException} is thrown. + * + * @param parameters the multivalued parameters to check + * @param parameterName name of the parameter which should be present + * @param the type of values in the multivalued map + * @return the int value of the validated parameter + * @throws JaxrsBadRequestException if the specified parameter is not present with only one value, or is + * not an integer + */ + public static int validateExactlyOneIntParameter(MultivaluedMap parameters, + String parameterName) { + checkArgumentNotNull(parameters, PARAMETERS_MUST_NOT_BE_NULL); + checkArgumentNotBlank(parameterName, PARAMETER_NAME_MUST_NOT_BE_BLANK); + + var values = parameters.get(parameterName); + assertOneElementOrThrowBadRequest(values, parameterName); + + var value = first(values); + return parseIntOrThrowBadRequest(value, parameterName); + } + + /** + * Checks whether {@code parameters} contains a parameter named {@code parameterName} that has at least one + * value that can be converted into an integer. All the values must be convertible to integer, and they are + * all converted and returned in a List. + * + * @param parameters the multivalued parameters to check + * @param parameterName name of the parameter which should be present + * @param the type of values in the multivalued map + * @return an unmodifiable List containing the int values of the validated parameter + * @throws JaxrsBadRequestException if the specified parameter is not present with at least one value, or is + * not an integer + */ + public static List validateOneOrMoreIntParameters(MultivaluedMap parameters, + String parameterName) { + checkArgumentNotNull(parameters, PARAMETERS_MUST_NOT_BE_NULL); + checkArgumentNotBlank(parameterName, PARAMETER_NAME_MUST_NOT_BE_BLANK); + + var values = parameters.get(parameterName); + assertOneOrMoreElementsOrThrowBadRequest(values, parameterName); + + return values.stream() + .map(value -> parseIntOrThrowBadRequest(value, parameterName)) + .collect(toUnmodifiableList()); + } + + @VisibleForTesting + static int parseIntOrThrowBadRequest(Object value, String parameterName) { //noinspection UnstableApiUsage - var result = Optional.ofNullable(Ints.tryParse(value.toString())); + var result = Optional.ofNullable(value).map(Object::toString).map(Ints::tryParse); - return result.orElseThrow(() -> - new JaxrsBadRequestException(f("{} must be an integer", value), parameterName)); + return result.orElseThrow(() -> newJaxrsBadRequestException("'{}' is not an integer", value, parameterName)); } /** @@ -400,18 +487,127 @@ public static int validateIntParameter(Map parameters, String par * @param parameterName name of the parameter which should be present * @param the type of values in the map * @return the long value of the validated parameter - * @throws JaxrsBadRequestException if the specified parameter is not present, or is not long + * @throws JaxrsBadRequestException if the specified parameter is not present, or is not a long */ public static long validateLongParameter(Map parameters, String parameterName) { - assertNotNull(parameterName, parameters); + checkArgumentNotNull(parameters, PARAMETERS_MUST_NOT_BE_NULL); + checkArgumentNotBlank(parameterName, PARAMETER_NAME_MUST_NOT_BE_BLANK); var value = parameters.get(parameterName); assertNotNull(parameterName, value); + return parseLongOrThrowBadRequest(value, parameterName); + } + + /** + * Checks whether {@code parameters} contains a parameter named {@code parameterName} that has at least one + * value that can be converted into a long. If there is more than one value, then the first one returned + * by {@link MultivaluedMap#getFirst(Object)} is returned. + * + * @param parameters the multivalued parameters to check + * @param parameterName name of the parameter which should be present + * @param the type of values in the multivalued map + * @return the long value of the validated parameter + * @throws JaxrsBadRequestException if the specified parameter is not present with at least one value, or is + * not a long + */ + public static long validateOneLongParameter(MultivaluedMap parameters, + String parameterName) { + checkArgumentNotNull(parameters, PARAMETERS_MUST_NOT_BE_NULL); + checkArgumentNotBlank(parameterName, PARAMETER_NAME_MUST_NOT_BE_BLANK); + + var value = parameters.getFirst(parameterName); + assertNotNull(parameterName, value); + + return parseLongOrThrowBadRequest(value, parameterName); + } + + /** + * Checks whether {@code parameters} contains a parameter named {@code parameterName} that has exactly one + * value that can be converted into a long. If there is more than one value, this is considered a bad request + * and a {@link JaxrsBadRequestException} is thrown. + * + * @param parameters the multivalued parameters to check + * @param parameterName name of the parameter which should be present + * @param the type of values in the multivalued map + * @return the long value of the validated parameter + * @throws JaxrsBadRequestException if the specified parameter is not present with at least one value, or is + * not a long + */ + public static long validateExactlyOneLongParameter(MultivaluedMap parameters, + String parameterName) { + checkArgumentNotNull(parameters, PARAMETERS_MUST_NOT_BE_NULL); + checkArgumentNotBlank(parameterName, PARAMETER_NAME_MUST_NOT_BE_BLANK); + + var values = parameters.get(parameterName); + assertOneElementOrThrowBadRequest(values, parameterName); + + var value = first(values); + return parseLongOrThrowBadRequest(value, parameterName); + } + + /** + * Checks whether {@code parameters} contains a parameter named {@code parameterName} that has at least one + * value that can be converted into a long. All the values must be convertible to long, and they are + * all converted and returned in a List. + * + * @param parameters the multivalued parameters to check + * @param parameterName name of the parameter which should be present + * @param the type of values in the multivalued map + * @return an unmodifiable List containing the long values of the validated parameter + * @throws JaxrsBadRequestException if the specified parameter is not present with only one value, or is + * not a long + */ + public static List validateOneOrMoreLongParameters(MultivaluedMap parameters, + String parameterName) { + checkArgumentNotNull(parameters, PARAMETERS_MUST_NOT_BE_NULL); + checkArgumentNotBlank(parameterName, PARAMETER_NAME_MUST_NOT_BE_BLANK); + + var values = parameters.get(parameterName); + assertOneOrMoreElementsOrThrowBadRequest(values, parameterName); + + return values.stream() + .map(value -> parseLongOrThrowBadRequest(value, parameterName)) + .collect(toUnmodifiableList()); + } + + @VisibleForTesting + static long parseLongOrThrowBadRequest(Object value, String parameterName) { //noinspection UnstableApiUsage - var result = Optional.ofNullable(Longs.tryParse(value.toString())); + var result = Optional.ofNullable(value).map(Object::toString).map(Longs::tryParse); + + return result.orElseThrow(() -> newJaxrsBadRequestException("'{}' is not a long", value, parameterName)); + } + + @VisibleForTesting + static void assertOneElementOrThrowBadRequest(List values, String parameterName) { + String message = null; + + if (isNullOrEmpty(values)) { + message = parameterName + " has no values, but exactly one was expected"; + } else if (values.size() > 1) { + message = parameterName + " has " + values.size() + " values, but only one was expected"; + } + + if (nonNull(message)) { + throw new JaxrsBadRequestException(message, parameterName); + } + } + + @VisibleForTesting + static void assertOneOrMoreElementsOrThrowBadRequest(List values, String parameterName) { + if (isNullOrEmpty(values)) { + var message = parameterName + " has no values, but expected at least one"; + throw new JaxrsBadRequestException(message, parameterName); + } + } + + @VisibleForTesting + static JaxrsBadRequestException newJaxrsBadRequestException(String messageTemplate, + Object value, + String parameterName) { - return result.orElseThrow(() -> - new JaxrsBadRequestException(f("{} must be a long", value), parameterName)); + var message = f(messageTemplate, value); + return new JaxrsBadRequestException(message, parameterName); } } diff --git a/src/test/java/org/kiwiproject/jaxrs/KiwiResourcesTest.java b/src/test/java/org/kiwiproject/jaxrs/KiwiResourcesTest.java index 0c9cca41..375c266b 100644 --- a/src/test/java/org/kiwiproject/jaxrs/KiwiResourcesTest.java +++ b/src/test/java/org/kiwiproject/jaxrs/KiwiResourcesTest.java @@ -3,9 +3,12 @@ import static com.google.common.base.Verify.verify; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.catchThrowableOfType; import static org.kiwiproject.base.KiwiStrings.f; +import static org.kiwiproject.collect.KiwiLists.first; import static org.kiwiproject.jaxrs.JaxrsTestHelper.assertCreatedResponseWithLocation; import static org.kiwiproject.jaxrs.JaxrsTestHelper.assertCustomHeaderFirstValue; import static org.kiwiproject.jaxrs.JaxrsTestHelper.assertOkResponse; @@ -26,6 +29,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; import org.junit.jupiter.params.provider.ValueSource; import org.kiwiproject.jaxrs.exception.ErrorMessage; import org.kiwiproject.jaxrs.exception.JaxrsBadRequestException; @@ -41,12 +45,14 @@ import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import java.io.IOException; import java.io.InputStream; import java.math.BigInteger; import java.net.URI; import java.nio.charset.StandardCharsets; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Stream; @@ -506,6 +512,19 @@ void shouldThrow_WhenCannotBecomeAnInt(Map parameters) { assertThatThrownBy(() -> KiwiResources.validateIntParameter(parameters, "id")) .isExactlyInstanceOf(JaxrsBadRequestException.class); } + + @ParameterizedTest + @NullAndEmptySource + void shouldThrowIllegalArgument_GivenBlankParameterName(String parameterName) { + assertThatIllegalArgumentException().isThrownBy(() -> + KiwiResources.validateIntParameter(Map.of(), parameterName)); + } + + @Test + void shouldThrowIllegalArgument_GivenNullParameterMap() { + assertThatIllegalArgumentException().isThrownBy(() -> + KiwiResources.validateIntParameter(null, "id")); + } } private static Stream> validIntParameterMaps() { @@ -519,7 +538,6 @@ void shouldThrow_WhenCannotBecomeAnInt(Map parameters) { private static Stream> invalidIntParameterMaps() { return Stream.of( - null, Map.of(), Map.of("id", "asdf"), Map.of("id", "forty two"), @@ -547,6 +565,19 @@ void shouldThrow_WhenCannotBecomeLong(Map parameters) { assertThatThrownBy(() -> KiwiResources.validateLongParameter(parameters, "id")) .isExactlyInstanceOf(JaxrsBadRequestException.class); } + + @ParameterizedTest + @NullAndEmptySource + void shouldThrowIllegalArgument_GivenBlankParameterName(String parameterName) { + assertThatIllegalArgumentException().isThrownBy(() -> + KiwiResources.validateLongParameter(Map.of(), parameterName)); + } + + @Test + void shouldThrowIllegalArgument_GivenNullParameterMap() { + assertThatIllegalArgumentException().isThrownBy(() -> + KiwiResources.validateLongParameter(null, "id")); + } } private static Stream> validLongParameterMaps() { @@ -560,7 +591,6 @@ void shouldThrow_WhenCannotBecomeLong(Map parameters) { private static Stream> invalidLongParameterMaps() { return Stream.of( - null, Map.of(), Map.of("id", "asdf"), Map.of("id", "forty two"), @@ -571,6 +601,483 @@ void shouldThrow_WhenCannotBecomeLong(Map parameters) { ); } + @Nested + class MultivaluedMapValidation { + + private MultivaluedMap parameters; + + @BeforeEach + void setUp() { + parameters = new MultivaluedHashMap<>(); + } + + @Nested + class ValidateOneIntParameter { + + @ParameterizedTest + @NullAndEmptySource + void shouldThrowIllegalArgument_WhenGivenBlankParameterName(String parameterName) { + assertThatIllegalArgumentException() + .isThrownBy(() -> KiwiResources.validateOneIntParameter(parameters, parameterName)); + } + + @Test + void shouldThrowIllegalArgument_WhenGivenNullParameterMap() { + assertThatIllegalArgumentException().isThrownBy(() -> + KiwiResources.validateOneIntParameter(null, "name")); + } + + @Test + void shouldThrowBadRequest_WhenParameterMap_DoesNotContainParameter() { + assertThatThrownBy(() -> KiwiResources.validateOneIntParameter(parameters, "name")) + .isExactlyInstanceOf(JaxrsBadRequestException.class); + } + + @ParameterizedTest + @MethodSource("org.kiwiproject.jaxrs.KiwiResourcesTest#validOneIntParameterMultivaluedMaps") + void shouldReturnTheValue_WhenIsOrCanBecomeInt(MultivaluedMap parameters) { + assertThat(KiwiResources.validateOneIntParameter(parameters, "id")) + .isEqualTo(42); + } + + @Test + void shouldReturnTheFirstValue_WhenGivenMoreThanOneValue() { + parameters.put("ids", List.of("24", "42", "84")); + + assertThat(KiwiResources.validateOneIntParameter(parameters, "ids")) + .isEqualTo(24); + } + + @ParameterizedTest + @MethodSource("org.kiwiproject.jaxrs.KiwiResourcesTest#invalidOneIntParameterMultivaluedMaps") + void shouldThrow_WhenCannotBecomeInt(MultivaluedMap parameters) { + assertThatThrownBy(() -> KiwiResources.validateOneIntParameter(parameters, "id")) + .isExactlyInstanceOf(JaxrsBadRequestException.class); + } + } + + @Nested + class ValidateExactlyOneIntParameter { + + @ParameterizedTest + @NullAndEmptySource + void shouldThrowIllegalArgument_WhenGivenBlankParameterName(String parameterName) { + assertThatIllegalArgumentException() + .isThrownBy(() -> KiwiResources.validateExactlyOneIntParameter(parameters, parameterName)); + } + + @Test + void shouldThrowIllegalArgument_WhenGivenNullParameterMap() { + assertThatIllegalArgumentException().isThrownBy(() -> + KiwiResources.validateExactlyOneIntParameter(null, "name")); + } + + @Test + void shouldThrowBadRequest_WhenParameterMap_DoesNotContainParameter() { + assertThatThrownBy(() -> KiwiResources.validateExactlyOneIntParameter(parameters, "name")) + .isExactlyInstanceOf(JaxrsBadRequestException.class); + } + + @ParameterizedTest + @MethodSource("org.kiwiproject.jaxrs.KiwiResourcesTest#validOneIntParameterMultivaluedMaps") + void shouldReturnTheValue_WhenIsOrCanBecomeInt(MultivaluedMap parameters) { + assertThat(KiwiResources.validateExactlyOneIntParameter(parameters, "id")) + .isEqualTo(42); + } + + @Test + void shouldThrowBadRequest_WhenGivenMoreThanOneValue() { + parameters.put("ids", List.of("24", "42", "84")); + + assertThatThrownBy(() -> KiwiResources.validateExactlyOneIntParameter(parameters, "ids")) + .isExactlyInstanceOf(JaxrsBadRequestException.class); + } + + @ParameterizedTest + @MethodSource("org.kiwiproject.jaxrs.KiwiResourcesTest#invalidOneIntParameterMultivaluedMaps") + void shouldThrow_WhenCannotBecomeInt(MultivaluedMap parameters) { + assertThatThrownBy(() -> KiwiResources.validateExactlyOneIntParameter(parameters, "id")) + .isExactlyInstanceOf(JaxrsBadRequestException.class); + } + } + + @Nested + class ValidateOneOrMoreIntParameters { + + @ParameterizedTest + @NullAndEmptySource + void shouldThrowIllegalArgument_WhenGivenBlankParameterName(String parameterName) { + assertThatIllegalArgumentException() + .isThrownBy(() -> KiwiResources.validateOneOrMoreIntParameters(parameters, parameterName)); + } + + @Test + void shouldThrowIllegalArgument_WhenGivenNullParameterMap() { + assertThatIllegalArgumentException().isThrownBy(() -> + KiwiResources.validateOneOrMoreIntParameters(null, "name")); + } + + @Test + void shouldThrowBadRequest_WhenParameterMap_DoesNotContainParameter() { + assertThatThrownBy(() -> KiwiResources.validateOneOrMoreIntParameters(parameters, "name")) + .isExactlyInstanceOf(JaxrsBadRequestException.class); + } + + @ParameterizedTest + @MethodSource("org.kiwiproject.jaxrs.KiwiResourcesTest#validOneIntParameterMultivaluedMaps") + void shouldReturnTheValue_WhenIsOrCanBecomeInt(MultivaluedMap parameters) { + assertThat(KiwiResources.validateOneOrMoreIntParameters(parameters, "id")) + .isEqualTo(List.of(42)); + } + + @Test + void shouldReturnAllTheValues_WhenGivenMoreThanOneValue() { + parameters.put("ids", List.of("24", "42", "84")); + + assertThat(KiwiResources.validateOneOrMoreIntParameters(parameters, "ids")) + .isEqualTo(List.of(24, 42, 84)); + } + + @ParameterizedTest + @MethodSource("org.kiwiproject.jaxrs.KiwiResourcesTest#invalidOneIntParameterMultivaluedMaps") + void shouldThrow_WhenCannotBecomeInt(MultivaluedMap parameters) { + assertThatThrownBy(() -> KiwiResources.validateOneOrMoreIntParameters(parameters, "id")) + .isExactlyInstanceOf(JaxrsBadRequestException.class); + } + } + + @Nested + class ValidateOneLongParameter { + + @ParameterizedTest + @NullAndEmptySource + void shouldThrowIllegalArgument_WhenGivenBlankParameterName(String parameterName) { + assertThatIllegalArgumentException() + .isThrownBy(() -> KiwiResources.validateOneLongParameter(parameters, parameterName)); + } + + @Test + void shouldThrowIllegalArgument_WhenGivenNullParameterMap() { + assertThatIllegalArgumentException().isThrownBy(() -> + KiwiResources.validateOneLongParameter(null, "name")); + } + + @Test + void shouldThrowBadRequest_WhenParameterMap_DoesNotContainParameter() { + assertThatThrownBy(() -> KiwiResources.validateOneLongParameter(parameters, "name")) + .isExactlyInstanceOf(JaxrsBadRequestException.class); + } + + @ParameterizedTest + @MethodSource("org.kiwiproject.jaxrs.KiwiResourcesTest#validOneLongParameterMultivaluedMaps") + void shouldReturnTheValue_WhenIsOrCanBecomeLong(MultivaluedMap parameters) { + assertThat(KiwiResources.validateOneLongParameter(parameters, "id")) + .isEqualTo(42); + } + + @Test + void shouldReturnTheFirstValue_WhenGivenMoreThanOneValue() { + parameters.put("ids", List.of("24", "42", "84")); + + assertThat(KiwiResources.validateOneLongParameter(parameters, "ids")) + .isEqualTo(24); + } + + @ParameterizedTest + @MethodSource("org.kiwiproject.jaxrs.KiwiResourcesTest#invalidOneLongParameterMultivaluedMaps") + void shouldThrow_WhenCannotBecomeLong(MultivaluedMap parameters) { + assertThatThrownBy(() -> KiwiResources.validateOneLongParameter(parameters, "id")) + .isExactlyInstanceOf(JaxrsBadRequestException.class); + } + } + + @Nested + class ValidateExactlyOneLongParameter { + + @ParameterizedTest + @NullAndEmptySource + void shouldThrowIllegalArgument_WhenGivenBlankParameterName(String parameterName) { + assertThatIllegalArgumentException() + .isThrownBy(() -> KiwiResources.validateExactlyOneLongParameter(parameters, parameterName)); + } + + @Test + void shouldThrowIllegalArgument_WhenGivenNullParameterMap() { + assertThatIllegalArgumentException().isThrownBy(() -> + KiwiResources.validateExactlyOneLongParameter(null, "name")); + } + + @Test + void shouldThrowBadRequest_WhenParameterMap_DoesNotContainParameter() { + assertThatThrownBy(() -> KiwiResources.validateExactlyOneLongParameter(parameters, "name")) + .isExactlyInstanceOf(JaxrsBadRequestException.class); + } + + @ParameterizedTest + @MethodSource("org.kiwiproject.jaxrs.KiwiResourcesTest#validOneLongParameterMultivaluedMaps") + void shouldReturnTheValue_WhenIsOrCanBecomeLong(MultivaluedMap parameters) { + assertThat(KiwiResources.validateExactlyOneLongParameter(parameters, "id")) + .isEqualTo(42); + } + + @Test + void shouldThrowBadRequest_WhenGivenMoreThanOneValue() { + parameters.put("ids", List.of("24", "42", "84")); + + assertThatThrownBy(() -> KiwiResources.validateExactlyOneLongParameter(parameters, "ids")) + .isExactlyInstanceOf(JaxrsBadRequestException.class); + } + + @ParameterizedTest + @MethodSource("org.kiwiproject.jaxrs.KiwiResourcesTest#invalidOneLongParameterMultivaluedMaps") + void shouldThrow_WhenCannotBecomeLong(MultivaluedMap parameters) { + assertThatThrownBy(() -> KiwiResources.validateExactlyOneLongParameter(parameters, "id")) + .isExactlyInstanceOf(JaxrsBadRequestException.class); + } + } + + @Nested + class ValidateOneOrMoreLongParameters { + + @ParameterizedTest + @NullAndEmptySource + void shouldThrowIllegalArgument_WhenGivenBlankParameterName(String parameterName) { + assertThatIllegalArgumentException() + .isThrownBy(() -> KiwiResources.validateOneOrMoreLongParameters(parameters, parameterName)); + } + + @Test + void shouldThrowIllegalArgument_WhenGivenNullParameterMap() { + assertThatIllegalArgumentException().isThrownBy(() -> + KiwiResources.validateOneOrMoreLongParameters(null, "name")); + } + + @Test + void shouldThrowBadRequest_WhenParameterMap_DoesNotContainParameter() { + assertThatThrownBy(() -> KiwiResources.validateOneOrMoreLongParameters(parameters, "name")) + .isExactlyInstanceOf(JaxrsBadRequestException.class); + } + + @ParameterizedTest + @MethodSource("org.kiwiproject.jaxrs.KiwiResourcesTest#validOneLongParameterMultivaluedMaps") + void shouldReturnTheValue_WhenIsOrCanBecomeLong(MultivaluedMap parameters) { + assertThat(KiwiResources.validateOneOrMoreLongParameters(parameters, "id")) + .isEqualTo(List.of(42L)); + } + + @Test + void shouldReturnAllTheValues_WhenGivenMoreThanOneValue() { + parameters.put("ids", List.of("24", "42", "84")); + + assertThat(KiwiResources.validateOneOrMoreLongParameters(parameters, "ids")) + .isEqualTo(List.of(24L, 42L, 84L)); + } + + @ParameterizedTest + @MethodSource("org.kiwiproject.jaxrs.KiwiResourcesTest#invalidOneLongParameterMultivaluedMaps") + void shouldThrow_WhenCannotBecomeLong(MultivaluedMap parameters) { + assertThatThrownBy(() -> KiwiResources.validateOneOrMoreLongParameters(parameters, "id")) + .isExactlyInstanceOf(JaxrsBadRequestException.class); + } + } + } + + private static Stream> validOneIntParameterMultivaluedMaps() { + return Stream.of( + new MultivaluedHashMap<>(Map.of("id", 42)), + new MultivaluedHashMap<>(Map.of("id", 42L)), + new MultivaluedHashMap<>(Map.of("id", "42")), + new MultivaluedHashMap<>(Map.of("id", BigInteger.valueOf(42))) + ); + } + + private static Stream> invalidOneIntParameterMultivaluedMaps() { + return Stream.of( + new MultivaluedHashMap<>(Map.of()), + new MultivaluedHashMap<>(Map.of("id", "asdf")), + new MultivaluedHashMap<>(Map.of("id", "forty two")), + new MultivaluedHashMap<>(Map.of("id", 42.0F)), + new MultivaluedHashMap<>(Map.of("id", 42.2F)), + new MultivaluedHashMap<>(Map.of("id", 42.0)), + new MultivaluedHashMap<>(Map.of("id", 42.2)), + new MultivaluedHashMap<>(Map.of("id", Integer.MAX_VALUE + 1L)) + ); + } + + private static Stream> validOneLongParameterMultivaluedMaps() { + return Stream.of( + new MultivaluedHashMap<>(Map.of("id", 42)), + new MultivaluedHashMap<>(Map.of("id", 42L)), + new MultivaluedHashMap<>(Map.of("id", "42")), + new MultivaluedHashMap<>(Map.of("id", BigInteger.valueOf(42))) + ); + } + + private static Stream> invalidOneLongParameterMultivaluedMaps() { + return Stream.of( + new MultivaluedHashMap<>(), + new MultivaluedHashMap<>(Map.of("id", "asdf")), + new MultivaluedHashMap<>(Map.of("id", "forty two")), + new MultivaluedHashMap<>(Map.of("id", 42.0F)), + new MultivaluedHashMap<>(Map.of("id", 42.2F)), + new MultivaluedHashMap<>(Map.of("id", 42.0)), + new MultivaluedHashMap<>(Map.of("id", 42.2)) + ); + } + + @Nested + class ParseIntOrThrowBadRequest { + + @ParameterizedTest + @MethodSource("org.kiwiproject.jaxrs.KiwiResourcesTest#parseableAsInt") + void shouldReturnTheValue_WhenParseableAsInt(Object obj) { + assertThat(KiwiResources.parseIntOrThrowBadRequest(obj, "testParam")) + .isEqualTo(42); + } + + @ParameterizedTest + @MethodSource("org.kiwiproject.jaxrs.KiwiResourcesTest#notParseableAsInt") + void shouldThrowBadRequest_WhenNotParseableAsInt(Object obj) { + assertThatThrownBy(() -> KiwiResources.parseIntOrThrowBadRequest(obj, "testParam")) + .isExactlyInstanceOf(JaxrsBadRequestException.class) + .hasMessage("'%s' is not an integer", obj); + } + } + + private static Stream parseableAsInt() { + return Stream.of( + 42, + 42L, + "42", + BigInteger.valueOf(42) + ); + } + + private static Stream notParseableAsInt() { + return Stream.of( + null, + "", + "junk", + new Object(), + 42.0F, + 42.2F, + 42.0, + 42.24, + Integer.MAX_VALUE + 1L + ); + } + + @Nested + class ParseLongOrThrowBadRequest { + + @ParameterizedTest + @MethodSource("org.kiwiproject.jaxrs.KiwiResourcesTest#parseableAsLong") + void shouldReturnTheValue_WhenParseableAsInt(Object obj) { + assertThat(KiwiResources.parseLongOrThrowBadRequest(obj, "testParam")) + .isEqualTo(42L); + } + + @ParameterizedTest + @MethodSource("org.kiwiproject.jaxrs.KiwiResourcesTest#notParseableAsLong") + void shouldThrowBadRequest_WhenNotParseableAsInt(Object obj) { + assertThatThrownBy(() -> KiwiResources.parseLongOrThrowBadRequest(obj, "testParam")) + .isExactlyInstanceOf(JaxrsBadRequestException.class) + .hasMessage("'%s' is not a long", obj); + } + } + + private static Stream parseableAsLong() { + return Stream.of( + 42, + 42L, + "42", + BigInteger.valueOf(42) + ); + } + + private static Stream notParseableAsLong() { + return Stream.of( + null, + "", + "junk", + new Object(), + 42.0F, + 42.2F, + 42.0, + 42.24 + ); + } + + @Nested + class AssertOneElementOrThrowBadRequest { + + @Test + void shouldDoNothing_WhenListContainsOneElement() { + var values = List.of("42"); + assertThatCode(() -> KiwiResources.assertOneElementOrThrowBadRequest(values, "testParam")) + .doesNotThrowAnyException(); + } + + @ParameterizedTest + @NullAndEmptySource + void shouldThrowBadRequest_WhenListIsNullOrEmpty(List values) { + var thrown = catchThrowableOfType( + () -> KiwiResources.assertOneElementOrThrowBadRequest(values, "testParam"), + JaxrsBadRequestException.class); + + assertThat(thrown.getMessage()).isEqualTo("testParam has no values, but exactly one was expected"); + assertThat(thrown.getErrors()).hasSize(1); + } + + @Test + void shouldThrowBadRequest_WhenListHasMoreThanOneElement() { + var values = List.of("42", "84"); + + var thrown = catchThrowableOfType( + () -> KiwiResources.assertOneElementOrThrowBadRequest(values, "testParam"), + JaxrsBadRequestException.class); + + assertThat(thrown.getMessage()).isEqualTo("testParam has 2 values, but only one was expected"); + assertThat(thrown.getErrors()).hasSize(1); + } + } + + @Nested + class AssertOneOrMoreElementsOrThrowBadRequest { + + @Test + void shouldDoNothing_WhenListContainsOneElement() { + assertThatCode(() -> KiwiResources.assertOneOrMoreElementsOrThrowBadRequest(List.of("42"), "myParam")) + .doesNotThrowAnyException(); + } + + @Test + void shouldDoNothing_WhenListContainsMultipleElements() { + assertThatCode(() -> KiwiResources.assertOneOrMoreElementsOrThrowBadRequest(List.of("24", "42"), "myParam")) + .doesNotThrowAnyException(); + } + + @ParameterizedTest + @NullAndEmptySource + void shouldThrowBadRequest_WhenListIsNullOrEmpty(List values) { + assertThatThrownBy(() -> KiwiResources.assertOneOrMoreElementsOrThrowBadRequest(values, "myParam")) + .isExactlyInstanceOf(JaxrsBadRequestException.class); + } + } + + @Nested + class NewJaxrsBadRequestExceptionFactory { + + @Test + void shouldCreateJaxrsException_WithExpectedMessage() { + var ex = KiwiResources.newJaxrsBadRequestException("the value {} is not valid", 42, "testParam"); + assertThat(ex.getMessage()).isEqualTo("the value 42 is not valid"); + assertThat(ex.getErrors()).hasSize(1); + var errorMessage = first(ex.getErrors()); + assertThat(errorMessage.getFieldName()).isEqualTo("testParam"); + } + } + private static String defaultNotFoundMessage() { return new ErrorMessage(null).getMessage(); }