From 3b24eca9a540c1f292247af796650ae0d83b2942 Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Mon, 9 Sep 2024 12:10:14 -0700 Subject: [PATCH 01/42] Migrate backstopper-core from javax to jakarta --- backstopper-core/README.md | 27 ++++++--- backstopper-core/build.gradle | 11 +--- .../exception/ClientDataValidationError.java | 6 +- .../exception/ServersideValidationError.java | 2 +- .../handler/ApiExceptionHandlerUtils.java | 16 +++--- ...entDataValidationErrorHandlerListener.java | 8 +-- ...streamNetworkExceptionHandlerListener.java | 6 +- .../GenericApiExceptionHandlerListener.java | 4 +- ...versideValidationErrorHandlerListener.java | 8 +-- .../service/ClientDataValidationService.java | 18 +++--- .../FailFastServersideValidationService.java | 10 ++-- .../service/NoOpJsr303Validator.java | 46 +++++++++++++-- .../ServersideValidationErrorTest.java | 2 +- .../handler/ApiExceptionHandlerUtilsTest.java | 6 +- ...ataValidationErrorHandlerListenerTest.java | 12 ++-- ...ideValidationErrorHandlerListenerTest.java | 10 ++-- .../ClientDataValidationServiceTest.java | 6 +- ...ilFastServersideValidationServiceTest.java | 4 +- .../service/NoOpJsr303ValidatorTest.java | 6 +- build.gradle | 12 ++-- settings.gradle | 56 +++++++++---------- 21 files changed, 159 insertions(+), 117 deletions(-) diff --git a/backstopper-core/README.md b/backstopper-core/README.md index 035b3c1..ba78c33 100644 --- a/backstopper-core/README.md +++ b/backstopper-core/README.md @@ -1,17 +1,28 @@ # Backstopper - core -Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 7 and greater. +Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 11 and greater. -This `backstopper-core` library contains the key core components necessary for a Backstopper system to work. The [base project README.md](../README.md) and [User Guide](../USER_GUIDE.md) contain the main bulk of information regarding Backstopper, but in particular: - -* [Backstopper key components](../USER_GUIDE.md#key_components) - This describes the main classes contained in this core library and what they are for. See the source code and javadocs on classes for further information. -* [Framework-specific modules](../README.md#framework_modules) - The list of specific framework plugin libraries Backstopper currently has support for. -* [Sample applications](../README.md#samples) - The list of sample applications demonstrating how to integrate and use Backstopper in the various supported frameworks. -* [Creating new framework integrations](../USER_GUIDE.md#new_framework_integrations) - Information on how to create new framework integrations. +(NOTE: The [Backstopper 1.x branch](https://github.com/Nike-Inc/backstopper/tree/v1.x) contains a version of +Backstopper for Java 7+, and for the `javax` ecosystem. The current Backstopper supports Java 11+ and the `jakarta` +ecosystem.) + +This `backstopper-core` library contains the key core components necessary for a Backstopper system to work. +The [base project README.md](../README.md) and [User Guide](../USER_GUIDE.md) contain the main bulk of information +regarding Backstopper, but in particular: + +* [Backstopper key components](../USER_GUIDE.md#key_components) - This describes the main classes contained in this core + library and what they are for. See the source code and javadocs on classes for further information. +* [Framework-specific modules](../README.md#framework_modules) - The list of specific framework plugin libraries + Backstopper currently has support for. +* [Sample applications](../README.md#samples) - The list of sample applications demonstrating how to integrate and use + Backstopper in the various supported frameworks. +* [Creating new framework integrations](../USER_GUIDE.md#new_framework_integrations) - Information on how to create new + framework integrations. ## More Info -See the [base project README.md](../README.md), [User Guide](../USER_GUIDE.md), and Backstopper repository source code and javadocs for all further information. +See the [base project README.md](../README.md), [User Guide](../USER_GUIDE.md), and Backstopper repository source code +and javadocs for all further information. ## License diff --git a/backstopper-core/build.gradle b/backstopper-core/build.gradle index 52c89fd..59e25dc 100644 --- a/backstopper-core/build.gradle +++ b/backstopper-core/build.gradle @@ -1,16 +1,11 @@ evaluationDependsOn(':') -compileTestJava { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - dependencies { api( project(":nike-internal-util"), "org.slf4j:slf4j-api:$slf4jVersion", - "javax.inject:javax.inject:$javaxInjectVersion", - "javax.validation:validation-api:$javaxValidationVersion" + "jakarta.inject:jakarta.inject-api:$jakartaInjectVersion", + "jakarta.validation:jakarta.validation-api:$jakartaValidationVersion" ) compileOnly( "org.jetbrains:annotations:$jetbrainsAnnotationsVersion" @@ -24,6 +19,6 @@ dependencies { "org.assertj:assertj-core:$assertJVersion", "com.tngtech.java:junit-dataprovider:$junitDataproviderVersion", "org.hamcrest:hamcrest-all:$hamcrestVersion", - "org.hibernate:hibernate-validator:$hibernateValidatorVersion" + "org.hibernate.validator:hibernate-validator:$hibernateValidatorVersion" ) } diff --git a/backstopper-core/src/main/java/com/nike/backstopper/exception/ClientDataValidationError.java b/backstopper-core/src/main/java/com/nike/backstopper/exception/ClientDataValidationError.java index c6d0084..99055ad 100644 --- a/backstopper-core/src/main/java/com/nike/backstopper/exception/ClientDataValidationError.java +++ b/backstopper-core/src/main/java/com/nike/backstopper/exception/ClientDataValidationError.java @@ -2,7 +2,7 @@ import java.util.List; -import javax.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolation; /** * A runtime exception representing a CLIENT DATA JSR 303 validation failure (i.e. a validation error with data @@ -51,8 +51,8 @@ public List> getViolations() { /** * @return The validation groups that were used to do the validation, if any. This may be null or empty; if this is - * null/empty it means the {@link javax.validation.groups.Default} validation group was used as per - * {@link javax.validation.Validator#validate(Object, Class[])} (i.e. no groups were passed in to that + * null/empty it means the {@link jakarta.validation.groups.Default} validation group was used as per + * {@link jakarta.validation.Validator#validate(Object, Class[])} (i.e. no groups were passed in to that * method). */ public Class[] getValidationGroups() { diff --git a/backstopper-core/src/main/java/com/nike/backstopper/exception/ServersideValidationError.java b/backstopper-core/src/main/java/com/nike/backstopper/exception/ServersideValidationError.java index 4c6feff..c5d056d 100644 --- a/backstopper-core/src/main/java/com/nike/backstopper/exception/ServersideValidationError.java +++ b/backstopper-core/src/main/java/com/nike/backstopper/exception/ServersideValidationError.java @@ -4,7 +4,7 @@ import java.util.Set; -import javax.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolation; /** * A runtime exception representing a SERVERSIDE JSR 303 validation failure (i.e. a validation error with diff --git a/backstopper-core/src/main/java/com/nike/backstopper/handler/ApiExceptionHandlerUtils.java b/backstopper-core/src/main/java/com/nike/backstopper/handler/ApiExceptionHandlerUtils.java index baa30c8..15f8f96 100644 --- a/backstopper-core/src/main/java/com/nike/backstopper/handler/ApiExceptionHandlerUtils.java +++ b/backstopper-core/src/main/java/com/nike/backstopper/handler/ApiExceptionHandlerUtils.java @@ -15,8 +15,8 @@ import java.util.Set; import java.util.UUID; -import javax.inject.Named; -import javax.inject.Singleton; +import jakarta.inject.Named; +import jakarta.inject.Singleton; /** * Set of reusable utility methods used by the API exception handling chain @@ -211,21 +211,21 @@ public String buildErrorMessageForLogs(StringBuilder sb, RequestInfoForLogging r } protected @Nullable Object extractOrigErrorRequestUriAttr(@NotNull RequestInfoForLogging request) { - // Corresponds to javax.servlet.RequestDispatcher.ERROR_REQUEST_URI. - return request.getAttribute("javax.servlet.error.request_uri"); + // Corresponds to jakarta.servlet.RequestDispatcher.ERROR_REQUEST_URI. + return request.getAttribute("jakarta.servlet.error.request_uri"); } protected @Nullable Object extractOrigForwardedRequestUriAttr(@NotNull RequestInfoForLogging request) { - // Corresponds to javax.servlet.RequestDispatcher.FORWARD_REQUEST_URI. - Object forwardedRequestUriAttr = request.getAttribute("javax.servlet.forward.request_uri"); + // Corresponds to jakarta.servlet.RequestDispatcher.FORWARD_REQUEST_URI. + Object forwardedRequestUriAttr = request.getAttribute("jakarta.servlet.forward.request_uri"); if (forwardedRequestUriAttr != null) { return forwardedRequestUriAttr; } // The forwarded request URI attr was null. Try the path info attr as a last resort. - // Corresponds to javax.servlet.RequestDispatcher.FORWARD_PATH_INFO. - return request.getAttribute("javax.servlet.forward.path_info"); + // Corresponds to jakarta.servlet.RequestDispatcher.FORWARD_PATH_INFO. + return request.getAttribute("jakarta.servlet.forward.path_info"); } /** diff --git a/backstopper-core/src/main/java/com/nike/backstopper/handler/listener/impl/ClientDataValidationErrorHandlerListener.java b/backstopper-core/src/main/java/com/nike/backstopper/handler/listener/impl/ClientDataValidationErrorHandlerListener.java index 6462540..43d060b 100644 --- a/backstopper-core/src/main/java/com/nike/backstopper/handler/listener/impl/ClientDataValidationErrorHandlerListener.java +++ b/backstopper-core/src/main/java/com/nike/backstopper/handler/listener/impl/ClientDataValidationErrorHandlerListener.java @@ -15,10 +15,10 @@ import java.util.List; import java.util.Map; -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; -import javax.validation.ConstraintViolation; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Singleton; +import jakarta.validation.ConstraintViolation; import static com.nike.backstopper.apierror.SortedApiErrorSet.singletonSortedSetOf; diff --git a/backstopper-core/src/main/java/com/nike/backstopper/handler/listener/impl/DownstreamNetworkExceptionHandlerListener.java b/backstopper-core/src/main/java/com/nike/backstopper/handler/listener/impl/DownstreamNetworkExceptionHandlerListener.java index 42e5018..3460dcb 100644 --- a/backstopper-core/src/main/java/com/nike/backstopper/handler/listener/impl/DownstreamNetworkExceptionHandlerListener.java +++ b/backstopper-core/src/main/java/com/nike/backstopper/handler/listener/impl/DownstreamNetworkExceptionHandlerListener.java @@ -16,9 +16,9 @@ import java.util.List; import java.util.concurrent.TimeoutException; -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Singleton; import static com.nike.backstopper.apierror.SortedApiErrorSet.singletonSortedSetOf; diff --git a/backstopper-core/src/main/java/com/nike/backstopper/handler/listener/impl/GenericApiExceptionHandlerListener.java b/backstopper-core/src/main/java/com/nike/backstopper/handler/listener/impl/GenericApiExceptionHandlerListener.java index 9deb5e5..74e5af6 100644 --- a/backstopper-core/src/main/java/com/nike/backstopper/handler/listener/impl/GenericApiExceptionHandlerListener.java +++ b/backstopper-core/src/main/java/com/nike/backstopper/handler/listener/impl/GenericApiExceptionHandlerListener.java @@ -10,8 +10,8 @@ import java.util.ArrayList; import java.util.List; -import javax.inject.Named; -import javax.inject.Singleton; +import jakarta.inject.Named; +import jakarta.inject.Singleton; /** * Handles generic {@link ApiException} errors by simply setting {@link ApiExceptionHandlerListenerResult#errors} to diff --git a/backstopper-core/src/main/java/com/nike/backstopper/handler/listener/impl/ServersideValidationErrorHandlerListener.java b/backstopper-core/src/main/java/com/nike/backstopper/handler/listener/impl/ServersideValidationErrorHandlerListener.java index 411ef8d..558fe6b 100644 --- a/backstopper-core/src/main/java/com/nike/backstopper/handler/listener/impl/ServersideValidationErrorHandlerListener.java +++ b/backstopper-core/src/main/java/com/nike/backstopper/handler/listener/impl/ServersideValidationErrorHandlerListener.java @@ -13,10 +13,10 @@ import java.util.ArrayList; import java.util.List; -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; -import javax.validation.ConstraintViolation; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Singleton; +import jakarta.validation.ConstraintViolation; import static com.nike.backstopper.apierror.SortedApiErrorSet.singletonSortedSetOf; diff --git a/backstopper-core/src/main/java/com/nike/backstopper/service/ClientDataValidationService.java b/backstopper-core/src/main/java/com/nike/backstopper/service/ClientDataValidationService.java index e82a19a..96ab140 100644 --- a/backstopper-core/src/main/java/com/nike/backstopper/service/ClientDataValidationService.java +++ b/backstopper-core/src/main/java/com/nike/backstopper/service/ClientDataValidationService.java @@ -7,11 +7,11 @@ import java.util.List; import java.util.Set; -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; -import javax.validation.ConstraintViolation; -import javax.validation.Validator; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Singleton; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validator; /** * Provides methods for performing JSR 303 validation on objects that will throw a {@link ClientDataValidationError} if @@ -62,9 +62,9 @@ public void validateObjectsFailFast(Object... validateTheseObjects) { * identical behavior). * *

NOTE: When asking for a specific group to be validated the Default group will not be validated - to validate - * constraints which are members of the Default group you must pass in {@link javax.validation.groups.Default} (in + * constraints which are members of the Default group you must pass in {@link jakarta.validation.groups.Default} (in * which case you could just call the simpler {@link #validateObjectsFailFast(Object...)} method), or you must pass - * in a class that extends {@link javax.validation.groups.Default}. + * in a class that extends {@link jakarta.validation.groups.Default}. */ public void validateObjectsWithGroupFailFast(Class group, Object... validateTheseObjects) { validateObjectsWithGroupsFailFast(new Class[]{group}, validateTheseObjects); @@ -80,7 +80,7 @@ public void validateObjectsWithGroupFailFast(Class group, Object... validateT * *

NOTE: When asking for specific groups to be validated the Default group will not be validated unless it is * included - to validate constraints which are members of the Default group one of the groups you pass in must be - * {@link javax.validation.groups.Default} or must extend it. + * {@link jakarta.validation.groups.Default} or must extend it. */ public void validateObjectsWithGroupsFailFast(Collection> groups, Object... validateTheseObjects) { Class[] groupsArray = @@ -99,7 +99,7 @@ public void validateObjectsWithGroupsFailFast(Collection> groups, Objec * *

NOTE: When asking for specific groups to be validated the Default group will not be validated unless it is * included - to validate constraints which are members of the Default group one of the groups you pass in must be - * {@link javax.validation.groups.Default} or must extend it. + * {@link jakarta.validation.groups.Default} or must extend it. */ public void validateObjectsWithGroupsFailFast(Class[] groups, Object... validateTheseObjects) { if (validateTheseObjects == null || validateTheseObjects.length == 0) { diff --git a/backstopper-core/src/main/java/com/nike/backstopper/service/FailFastServersideValidationService.java b/backstopper-core/src/main/java/com/nike/backstopper/service/FailFastServersideValidationService.java index 31010e3..5ed77d9 100644 --- a/backstopper-core/src/main/java/com/nike/backstopper/service/FailFastServersideValidationService.java +++ b/backstopper-core/src/main/java/com/nike/backstopper/service/FailFastServersideValidationService.java @@ -4,11 +4,11 @@ import java.util.Set; -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; -import javax.validation.ConstraintViolation; -import javax.validation.Validator; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Singleton; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validator; /** * Helper service that provides a method for fail-fast JSR 303 validation of serverside objects (e.g. objects received diff --git a/backstopper-core/src/main/java/com/nike/backstopper/service/NoOpJsr303Validator.java b/backstopper-core/src/main/java/com/nike/backstopper/service/NoOpJsr303Validator.java index 2c54928..df25dcb 100644 --- a/backstopper-core/src/main/java/com/nike/backstopper/service/NoOpJsr303Validator.java +++ b/backstopper-core/src/main/java/com/nike/backstopper/service/NoOpJsr303Validator.java @@ -1,11 +1,14 @@ package com.nike.backstopper.service; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; import java.util.Set; -import javax.validation.ConstraintViolation; -import javax.validation.ValidationException; -import javax.validation.Validator; -import javax.validation.metadata.BeanDescriptor; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ValidationException; +import jakarta.validation.Validator; +import jakarta.validation.executable.ExecutableValidator; +import jakarta.validation.metadata.BeanDescriptor; import static java.util.Collections.emptySet; @@ -17,7 +20,7 @@ * @author Nic Munroe */ @SuppressWarnings("WeakerAccess") -public class NoOpJsr303Validator implements Validator { +public class NoOpJsr303Validator implements Validator, ExecutableValidator { public static final NoOpJsr303Validator SINGLETON_IMPL = new NoOpJsr303Validator(); @@ -46,4 +49,37 @@ public BeanDescriptor getConstraintsForClass(Class clazz) { public T unwrap(Class type) { throw new ValidationException(this.getClass().getName() + " does not implement unwrap()"); } + + @Override + public ExecutableValidator forExecutables() { + return this; + } + + @Override + public Set> validateParameters( + T object, Method method, Object[] parameterValues, Class... groups + ) { + return emptySet(); + } + + @Override + public Set> validateReturnValue( + T object, Method method, Object returnValue, Class... groups + ) { + return emptySet(); + } + + @Override + public Set> validateConstructorParameters( + Constructor constructor, Object[] parameterValues, Class... groups + ) { + return emptySet(); + } + + @Override + public Set> validateConstructorReturnValue( + Constructor constructor, T createdObject, Class... groups + ) { + return emptySet(); + } } diff --git a/backstopper-core/src/test/java/com/nike/backstopper/exception/ServersideValidationErrorTest.java b/backstopper-core/src/test/java/com/nike/backstopper/exception/ServersideValidationErrorTest.java index 77c404b..50ca128 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/exception/ServersideValidationErrorTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/exception/ServersideValidationErrorTest.java @@ -5,7 +5,7 @@ import java.util.HashSet; import java.util.Set; -import javax.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolation; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; diff --git a/backstopper-core/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerUtilsTest.java b/backstopper-core/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerUtilsTest.java index 3530b81..e92e222 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerUtilsTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerUtilsTest.java @@ -252,7 +252,7 @@ public void buildErrorMessageForLogs_includes_orig_error_request_uri_when_availa String origErrorRequestUriValue = UUID.randomUUID().toString(); if (isErrorRequestUriValueInAttrs) { - doReturn(origErrorRequestUriValue).when(request).getAttribute("javax.servlet.error.request_uri"); + doReturn(origErrorRequestUriValue).when(request).getAttribute("jakarta.servlet.error.request_uri"); } // when @@ -291,8 +291,8 @@ public void buildErrorMessageForLogs_includes_orig_forwarded_request_uri_when_av StringBuilder sb = new StringBuilder(); RequestInfoForLogging request = mock(RequestInfoForLogging.class); - doReturn(forwardedRequestUriAttrValue).when(request).getAttribute("javax.servlet.forward.request_uri"); - doReturn(forwardedPathInfoAttrValue).when(request).getAttribute("javax.servlet.forward.path_info"); + doReturn(forwardedRequestUriAttrValue).when(request).getAttribute("jakarta.servlet.forward.request_uri"); + doReturn(forwardedPathInfoAttrValue).when(request).getAttribute("jakarta.servlet.forward.path_info"); // when impl.buildErrorMessageForLogs( diff --git a/backstopper-core/src/test/java/com/nike/backstopper/handler/listener/impl/ClientDataValidationErrorHandlerListenerTest.java b/backstopper-core/src/test/java/com/nike/backstopper/handler/listener/impl/ClientDataValidationErrorHandlerListenerTest.java index a816efe..2488efa 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/handler/listener/impl/ClientDataValidationErrorHandlerListenerTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/handler/listener/impl/ClientDataValidationErrorHandlerListenerTest.java @@ -22,11 +22,11 @@ import java.util.Collections; import java.util.List; -import javax.validation.ConstraintViolation; -import javax.validation.Path; -import javax.validation.constraints.NotNull; -import javax.validation.groups.Default; -import javax.validation.metadata.ConstraintDescriptor; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Path; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.groups.Default; +import jakarta.validation.metadata.ConstraintDescriptor; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; @@ -206,7 +206,7 @@ public void shouldAddExtraLoggingDetailsForClientDataValidationError() { Pair.of("client_data_validation_failed_objects", SomeValidatableObject.class.getName() + "," + Object.class.getName()), Pair.of("validation_groups_considered", Default.class.getName() + "," + SomeValidationGroup.class.getName()), Pair.of("constraint_violation_details", - "SomeValidatableObject.path.to.violation1|javax.validation.constraints.NotNull|MISSING_EXPECTED_CONTENT,Object.path.to.violation2|org.hibernate.validator.constraints" + + "SomeValidatableObject.path.to.violation1|jakarta.validation.constraints.NotNull|MISSING_EXPECTED_CONTENT,Object.path.to.violation2|org.hibernate.validator.constraints" + ".NotEmpty|TYPE_CONVERSION_ERROR")) ); } diff --git a/backstopper-core/src/test/java/com/nike/backstopper/handler/listener/impl/ServersideValidationErrorHandlerListenerTest.java b/backstopper-core/src/test/java/com/nike/backstopper/handler/listener/impl/ServersideValidationErrorHandlerListenerTest.java index cb84993..d8d712c 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/handler/listener/impl/ServersideValidationErrorHandlerListenerTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/handler/listener/impl/ServersideValidationErrorHandlerListenerTest.java @@ -21,10 +21,10 @@ import java.util.LinkedHashSet; import java.util.List; -import javax.validation.ConstraintViolation; -import javax.validation.Path; -import javax.validation.constraints.NotNull; -import javax.validation.metadata.ConstraintDescriptor; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Path; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.metadata.ConstraintDescriptor; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.containsInAnyOrder; @@ -142,7 +142,7 @@ public void shouldAddExtraLoggingDetailsForServersideValidationError() { extraLoggingDetails.toString(); assertThat(extraLoggingDetails, containsInAnyOrder(Pair.of("serverside_validation_object", SomeValidatableObject.class.getName()), Pair.of("serverside_validation_errors", - "path.to.violation1|javax.validation.constraints.NotNull|Violation_1_Message, path.to.violation2|org.hibernate.validator.constraints" + + "path.to.violation1|jakarta.validation.constraints.NotNull|Violation_1_Message, path.to.violation2|org.hibernate.validator.constraints" + ".NotEmpty|Violation_2_Message"))); } diff --git a/backstopper-core/src/test/java/com/nike/backstopper/service/ClientDataValidationServiceTest.java b/backstopper-core/src/test/java/com/nike/backstopper/service/ClientDataValidationServiceTest.java index c8c4162..393fd28 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/service/ClientDataValidationServiceTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/service/ClientDataValidationServiceTest.java @@ -12,9 +12,9 @@ import java.util.HashSet; import java.util.List; -import javax.validation.ConstraintViolation; -import javax.validation.Validator; -import javax.validation.groups.Default; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validator; +import jakarta.validation.groups.Default; import static junit.framework.TestCase.fail; import static org.hamcrest.CoreMatchers.is; diff --git a/backstopper-core/src/test/java/com/nike/backstopper/service/FailFastServersideValidationServiceTest.java b/backstopper-core/src/test/java/com/nike/backstopper/service/FailFastServersideValidationServiceTest.java index 97dcb8b..b7fc288 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/service/FailFastServersideValidationServiceTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/service/FailFastServersideValidationServiceTest.java @@ -11,8 +11,8 @@ import java.util.Collections; import java.util.HashSet; -import javax.validation.ConstraintViolation; -import javax.validation.Validator; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validator; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; diff --git a/backstopper-core/src/test/java/com/nike/backstopper/service/NoOpJsr303ValidatorTest.java b/backstopper-core/src/test/java/com/nike/backstopper/service/NoOpJsr303ValidatorTest.java index 6a01875..c54cdf0 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/service/NoOpJsr303ValidatorTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/service/NoOpJsr303ValidatorTest.java @@ -3,9 +3,9 @@ import org.assertj.core.api.ThrowableAssert; import org.junit.Test; -import javax.validation.ValidationException; -import javax.validation.Validator; -import javax.validation.constraints.NotNull; +import jakarta.validation.ValidationException; +import jakarta.validation.Validator; +import jakarta.validation.constraints.NotNull; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; diff --git a/build.gradle b/build.gradle index 34148a9..226b753 100644 --- a/build.gradle +++ b/build.gradle @@ -60,8 +60,8 @@ allprojects { } // http://www.gradle.org/docs/current/userguide/java_plugin.html - sourceCompatibility = JavaVersion.VERSION_1_7 - targetCompatibility = JavaVersion.VERSION_1_7 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } // Disable jar, sourcesJar, and javadoc tasks for the root project - we only want them to run for submodules @@ -78,8 +78,8 @@ configure(subprojects.findAll { it.name.startsWith("sample") || it.name.startsWi // ========== PROPERTIES FOR GRADLE BUILD - DEPENDENCY VERSIONS / ETC ext { slf4jVersion = '1.7.36' - javaxInjectVersion = '1' - javaxValidationVersion = '1.0.0.GA' + jakartaInjectVersion = '2.0.1' + jakartaValidationVersion = '3.0.2' servletApiVersion = '3.0.1' spring4Version = '4.3.2.RELEASE' spring5Version = '5.1.8.RELEASE' @@ -98,8 +98,8 @@ ext { assertJVersion = '3.23.1' junitDataproviderVersion = '1.13.1' hamcrestVersion = '1.3' - hibernateValidatorVersion = '4.3.0.Final' - javaxValidationVersionForNewerSpring = '2.0.1.Final' + hibernateValidatorVersion = '8.0.1.Final' + jakartaValidationVersionForNewerSpring = '2.0.1.Final' hibernateValidatorVersionForNewerSpring = '6.2.2.Final' elApiVersion = '3.0.1-b06' elImplVersion = '3.0.1-b12' diff --git a/settings.gradle b/settings.gradle index dd970d2..1663b78 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,31 +2,31 @@ rootProject.name = 'backstopper' // Published-artifact modules include "nike-internal-util", - "backstopper-custom-validators", - "backstopper-core", - "backstopper-reusable-tests", - "backstopper-reusable-tests-junit5", - "backstopper-jackson", - "backstopper-servlet-api", - "backstopper-spring-web", - "backstopper-spring-web-mvc", - "backstopper-spring-web-flux", - "backstopper-spring-boot1", - "backstopper-spring-boot2-webmvc", - "backstopper-jaxrs", - "backstopper-jersey1", - "backstopper-jersey2", - // Test-only modules (not published) - "testonly:testonly-spring-reusable-test-support", - "testonly:testonly-spring4-webmvc", - "testonly:testonly-spring5-webmvc", - "testonly:testonly-springboot1", - "testonly:testonly-springboot2-webmvc", - "testonly:testonly-springboot2-webflux", - // Sample modules (not published) - "samples:sample-spring-web-mvc", - "samples:sample-spring-boot1", - "samples:sample-spring-boot2-webmvc", - "samples:sample-spring-boot2-webflux", - "samples:sample-jersey1", - "samples:sample-jersey2" \ No newline at end of file + "backstopper-core" +// "backstopper-custom-validators", +// "backstopper-reusable-tests", +// "backstopper-reusable-tests-junit5", +// "backstopper-jackson", +// "backstopper-servlet-api", +// "backstopper-spring-web", +// "backstopper-spring-web-mvc", +// "backstopper-spring-web-flux", +// "backstopper-spring-boot1", +// "backstopper-spring-boot2-webmvc", +// "backstopper-jaxrs", +// "backstopper-jersey1", +// "backstopper-jersey2", +// // Test-only modules (not published) +// "testonly:testonly-spring-reusable-test-support", +// "testonly:testonly-spring4-webmvc", +// "testonly:testonly-spring5-webmvc", +// "testonly:testonly-springboot1", +// "testonly:testonly-springboot2-webmvc", +// "testonly:testonly-springboot2-webflux", +// // Sample modules (not published) +// "samples:sample-spring-web-mvc", +// "samples:sample-spring-boot1", +// "samples:sample-spring-boot2-webmvc", +// "samples:sample-spring-boot2-webflux", +// "samples:sample-jersey1", +// "samples:sample-jersey2" \ No newline at end of file From ad6842da4a8cd357e40f78981e752936ef39c344 Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Mon, 9 Sep 2024 13:34:44 -0700 Subject: [PATCH 02/42] Migrate backstopper-custom-validators from javax to jakarta --- backstopper-custom-validators/README.md | 36 +++++++++++++------ backstopper-custom-validators/build.gradle | 12 ++----- .../StringConvertsToClassType.java | 6 ++-- .../StringConvertsToClassTypeValidator.java | 4 +-- ...tringConvertsToClassTypeValidatorTest.java | 6 ++-- build.gradle | 7 ++-- settings.gradle | 4 +-- 7 files changed, 40 insertions(+), 35 deletions(-) diff --git a/backstopper-custom-validators/README.md b/backstopper-custom-validators/README.md index b848360..61f65d4 100644 --- a/backstopper-custom-validators/README.md +++ b/backstopper-custom-validators/README.md @@ -1,19 +1,32 @@ # Backstopper - custom-validators -Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 7 and greater. +Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 11 and greater. + +(NOTE: The [Backstopper 1.x branch](https://github.com/Nike-Inc/backstopper/tree/v1.x) contains a version of +Backstopper for Java 7+, and for the `javax` ecosystem. The current Backstopper supports Java 11+ and the `jakarta` +ecosystem.) + +This library contains JSR 303 Bean Validation annotations that have proven to be useful and reusable. These are entirely +optional. They are also largely dependency free so this library is usable in non-Backstopper projects that utilize JSR +303 validations. -This library contains JSR 303 Bean Validation annotations that have proven to be useful and reusable. These are entirely optional. They are also largely dependency free so this library is usable in non-Backstopper projects that utilize JSR 303 validations. - ## Custom JSR 303 Validation Constraints - -* **`StringConvertsToClassType`** - Validates that the annotated element (of type String) can be converted to the desired `classType`. The `classType` can be any of the following: + +* **`StringConvertsToClassType`** - Validates that the annotated element (of type String) can be converted to the + desired `classType`. The `classType` can be any of the following: * Any boxed primitive class type (e.g. `Integer.class`). * Any raw primitive class type (e.g. `int.class`). - * `String.class` - a String can always be converted to a String, so this validator will always return true in this case. - * Any enum class type - validation is done by comparing the string value to `Enum.name()`. The value of the `allowCaseInsensitiveEnumMatch()` constraint property determines if the validation is done in a case sensitive or case insensitive manner. - * `null` is always considered valid - if you need to enforce non-null then you should place an additional `@NotNull` constraint on the field as well. - * More information and usage instructions can be found in the javadocs for `StringConvertsToClassType`, but here's an example showing how you would mark a model field that you wanted to guarantee was convertible to a `RgbColor` enum after passing JSR 303 validation: - + * `String.class` - a String can always be converted to a String, so this validator will always return true in this + case. + * Any enum class type - validation is done by comparing the string value to `Enum.name()`. The value of the + `allowCaseInsensitiveEnumMatch()` constraint property determines if the validation is done in a case sensitive or + case insensitive manner. + * `null` is always considered valid - if you need to enforce non-null then you should place an additional `@NotNull` + constraint on the field as well. + * More information and usage instructions can be found in the javadocs for `StringConvertsToClassType`, but here's + an example showing how you would mark a model field that you wanted to guarantee was convertible to a `RgbColor` + enum after passing JSR 303 validation: + ``` java @StringConvertsToClassType( message = "NOT_RGB_COLOR_ENUM", classType = RgbColor.class, allowCaseInsensitiveEnumMatch = true @@ -23,7 +36,8 @@ public final String rgb_color; ## More Info -See the [base project README.md](../README.md), [User Guide](../USER_GUIDE.md), and Backstopper repository source code and javadocs for all further information. +See the [base project README.md](../README.md), [User Guide](../USER_GUIDE.md), and Backstopper repository source code +and javadocs for all further information. ## License diff --git a/backstopper-custom-validators/build.gradle b/backstopper-custom-validators/build.gradle index 349ec0a..b31f43f 100644 --- a/backstopper-custom-validators/build.gradle +++ b/backstopper-custom-validators/build.gradle @@ -1,14 +1,9 @@ evaluationDependsOn(':') -compileTestJava { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - dependencies { api( "org.slf4j:slf4j-api:$slf4jVersion", - "javax.validation:validation-api:$javaxValidationVersion" + "jakarta.validation:jakarta.validation-api:$jakartaValidationVersion" ) testImplementation( project(":nike-internal-util"), @@ -17,8 +12,7 @@ dependencies { "ch.qos.logback:logback-classic:$logbackVersion", "org.assertj:assertj-core:$assertJVersion", "com.tngtech.java:junit-dataprovider:$junitDataproviderVersion", - "org.hibernate:hibernate-validator:$hibernateValidatorVersion", - // The jaxb-api is needed for building on the java 11 JDK as these classes were moved out of the Java SE libs. - "javax.xml.bind:jaxb-api:$jaxbApiVersion", + "org.hibernate.validator:hibernate-validator:$hibernateValidatorVersion", + "org.glassfish.expressly:expressly:$glassfishExpresslyVersion", ) } diff --git a/backstopper-custom-validators/src/main/java/com/nike/backstopper/validation/constraints/StringConvertsToClassType.java b/backstopper-custom-validators/src/main/java/com/nike/backstopper/validation/constraints/StringConvertsToClassType.java index 3799a05..5efbcdb 100644 --- a/backstopper-custom-validators/src/main/java/com/nike/backstopper/validation/constraints/StringConvertsToClassType.java +++ b/backstopper-custom-validators/src/main/java/com/nike/backstopper/validation/constraints/StringConvertsToClassType.java @@ -6,8 +6,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; -import javax.validation.Constraint; -import javax.validation.Payload; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; import static java.lang.annotation.ElementType.ANNOTATION_TYPE; import static java.lang.annotation.ElementType.CONSTRUCTOR; @@ -63,7 +63,7 @@ * * *

{@code null} is always considered valid - if you need to enforce non-null then you should place an additional - * {@link javax.validation.constraints.NotNull} constraint on the field as well. + * {@link jakarta.validation.constraints.NotNull} constraint on the field as well. * *

Note that Floats and Doubles will fail validation if the number parses to {@link Float#isInfinite()}, * {@link Float#isNaN()}, {@link Double#isInfinite()}, or {@link Double#isNaN()}. diff --git a/backstopper-custom-validators/src/main/java/com/nike/backstopper/validation/constraints/impl/StringConvertsToClassTypeValidator.java b/backstopper-custom-validators/src/main/java/com/nike/backstopper/validation/constraints/impl/StringConvertsToClassTypeValidator.java index ee73732..448f325 100644 --- a/backstopper-custom-validators/src/main/java/com/nike/backstopper/validation/constraints/impl/StringConvertsToClassTypeValidator.java +++ b/backstopper-custom-validators/src/main/java/com/nike/backstopper/validation/constraints/impl/StringConvertsToClassTypeValidator.java @@ -5,8 +5,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; /** * Implementation of the validation logic for {@link StringConvertsToClassType}. See that annotation's javadocs for more diff --git a/backstopper-custom-validators/src/test/java/com/nike/backstopper/validation/constraints/impl/StringConvertsToClassTypeValidatorTest.java b/backstopper-custom-validators/src/test/java/com/nike/backstopper/validation/constraints/impl/StringConvertsToClassTypeValidatorTest.java index 5bdbd15..276d824 100644 --- a/backstopper-custom-validators/src/test/java/com/nike/backstopper/validation/constraints/impl/StringConvertsToClassTypeValidatorTest.java +++ b/backstopper-custom-validators/src/test/java/com/nike/backstopper/validation/constraints/impl/StringConvertsToClassTypeValidatorTest.java @@ -12,9 +12,9 @@ import java.util.Set; -import javax.validation.ConstraintViolation; -import javax.validation.Validation; -import javax.validation.Validator; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; diff --git a/build.gradle b/build.gradle index 226b753..0c681ee 100644 --- a/build.gradle +++ b/build.gradle @@ -98,11 +98,8 @@ ext { assertJVersion = '3.23.1' junitDataproviderVersion = '1.13.1' hamcrestVersion = '1.3' - hibernateValidatorVersion = '8.0.1.Final' - jakartaValidationVersionForNewerSpring = '2.0.1.Final' - hibernateValidatorVersionForNewerSpring = '6.2.2.Final' - elApiVersion = '3.0.1-b06' - elImplVersion = '3.0.1-b12' + hibernateValidatorVersion = '8.0.1.Final' // Compatible with Jakarta EE 10 + glassfishExpresslyVersion = '5.0.0' // Provides EL impl support for hibernate validator, compatible with Jakarta EE 10 jetbrainsAnnotationsVersion = '16.0.3' diff --git a/settings.gradle b/settings.gradle index 1663b78..727d418 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,8 +2,8 @@ rootProject.name = 'backstopper' // Published-artifact modules include "nike-internal-util", - "backstopper-core" -// "backstopper-custom-validators", + "backstopper-core", + "backstopper-custom-validators" // "backstopper-reusable-tests", // "backstopper-reusable-tests-junit5", // "backstopper-jackson", From 7141e0b62ada3e0e19a9d3d424e4092f4f760c32 Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Mon, 9 Sep 2024 13:40:26 -0700 Subject: [PATCH 03/42] Migrate backstopper-reusable-tests-junit5 from javax to jakarta --- backstopper-reusable-tests-junit5/README.md | 22 +++++++++++-------- .../build.gradle | 3 --- ...ctionBasedJsr303AnnotationTrollerBase.java | 4 ++-- .../ReflectionMagicWorksTest.java | 18 +++++++-------- ...ationMessagesPointToApiErrorsTestTest.java | 4 ++-- settings.gradle | 5 ++--- 6 files changed, 28 insertions(+), 28 deletions(-) diff --git a/backstopper-reusable-tests-junit5/README.md b/backstopper-reusable-tests-junit5/README.md index ddbbaba..38c66cd 100644 --- a/backstopper-reusable-tests-junit5/README.md +++ b/backstopper-reusable-tests-junit5/README.md @@ -1,19 +1,23 @@ # Backstopper - reusable-tests -Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 7 and greater. +Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 11 and greater. -This library contains some reusable unit test classes that should be integrated into every Backstopper-enabled -project to guarantee that the conventions and rules that Backstopper requires are followed. This is a fairly easy -process and is described in detail in the Backstopper User Guide in the [Reusable Unit Tests for Enforcing +(NOTE: The [Backstopper 1.x branch](https://github.com/Nike-Inc/backstopper/tree/v1.x) contains a version of +Backstopper for Java 7+, and for the `javax` ecosystem. The current Backstopper supports Java 11+ and the `jakarta` +ecosystem.) + +This library contains some reusable unit test classes that should be integrated into every Backstopper-enabled +project to guarantee that the conventions and rules that Backstopper requires are followed. This is a fairly easy +process and is described in detail in the Backstopper User Guide in the [Reusable Unit Tests for Enforcing Backstopper Rules and Conventions](../USER_GUIDE.md#reusable_tests) section. -Beyond that, the classes in this reusable-tests library are heavily documented with extensive javadocs, and the -[sample applications](../README.md#samples) show concrete usage of these tests to enforce the rules and conventions. -Please explore the source code and samples to learn more. - +Beyond that, the classes in this reusable-tests library are heavily documented with extensive javadocs, and the +[sample applications](../README.md#samples) show concrete usage of these tests to enforce the rules and conventions. +Please explore the source code and samples to learn more. + ## More Info -See the [base project README.md](../README.md), [User Guide](../USER_GUIDE.md), and Backstopper repository source +See the [base project README.md](../README.md), [User Guide](../USER_GUIDE.md), and Backstopper repository source code and javadocs for all further information. ## License diff --git a/backstopper-reusable-tests-junit5/build.gradle b/backstopper-reusable-tests-junit5/build.gradle index 7891a6c..ec68749 100644 --- a/backstopper-reusable-tests-junit5/build.gradle +++ b/backstopper-reusable-tests-junit5/build.gradle @@ -1,8 +1,5 @@ evaluationDependsOn(':') -sourceCompatibility = JavaVersion.VERSION_1_8 -targetCompatibility = JavaVersion.VERSION_1_8 - ext { mockito3Version = '3.12.4' javassistJava8Version = '3.29.0-GA' diff --git a/backstopper-reusable-tests-junit5/src/main/java/com/nike/backstopper/apierror/contract/jsr303convention/ReflectionBasedJsr303AnnotationTrollerBase.java b/backstopper-reusable-tests-junit5/src/main/java/com/nike/backstopper/apierror/contract/jsr303convention/ReflectionBasedJsr303AnnotationTrollerBase.java index ce62174..35498c2 100644 --- a/backstopper-reusable-tests-junit5/src/main/java/com/nike/backstopper/apierror/contract/jsr303convention/ReflectionBasedJsr303AnnotationTrollerBase.java +++ b/backstopper-reusable-tests-junit5/src/main/java/com/nike/backstopper/apierror/contract/jsr303convention/ReflectionBasedJsr303AnnotationTrollerBase.java @@ -27,7 +27,7 @@ import java.util.Set; import java.util.function.Predicate; -import javax.validation.Constraint; +import jakarta.validation.Constraint; /** * Base class for tests that need to troll through the JSR 303 annotations in the project in order to do some checking @@ -167,7 +167,7 @@ public abstract class ReflectionBasedJsr303AnnotationTrollerBase { private final Set DEFAULT_CONSTRAINT_SEARCH_PACKAGES = new LinkedHashSet<>(Arrays.asList( "com.nike", "org.hibernate.validator.constraints", - "javax.validation.constraints" + "jakarta.validation.constraints" )); /** diff --git a/backstopper-reusable-tests-junit5/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/ReflectionMagicWorksTest.java b/backstopper-reusable-tests-junit5/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/ReflectionMagicWorksTest.java index 0cdf400..26c909d 100644 --- a/backstopper-reusable-tests-junit5/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/ReflectionMagicWorksTest.java +++ b/backstopper-reusable-tests-junit5/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/ReflectionMagicWorksTest.java @@ -14,15 +14,15 @@ import java.util.List; import java.util.function.Predicate; -import javax.validation.Constraint; -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; -import javax.validation.Payload; -import javax.validation.constraints.AssertFalse; -import javax.validation.constraints.AssertTrue; -import javax.validation.constraints.Max; -import javax.validation.constraints.Min; -import javax.validation.constraints.NotNull; +import jakarta.validation.Constraint; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import jakarta.validation.Payload; +import jakarta.validation.constraints.AssertFalse; +import jakarta.validation.constraints.AssertTrue; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; import static com.nike.backstopper.apierror.contract.jsr303convention.ReflectionBasedJsr303AnnotationTrollerBase.extractMessageFromAnnotation; import static com.nike.backstopper.apierror.contract.jsr303convention.ReflectionBasedJsr303AnnotationTrollerBase.generateExclusionForAnnotatedElementAndAnnotationClass; diff --git a/backstopper-reusable-tests-junit5/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyJsr303ValidationMessagesPointToApiErrorsTestTest.java b/backstopper-reusable-tests-junit5/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyJsr303ValidationMessagesPointToApiErrorsTestTest.java index 3f05535..58f73b6 100644 --- a/backstopper-reusable-tests-junit5/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyJsr303ValidationMessagesPointToApiErrorsTestTest.java +++ b/backstopper-reusable-tests-junit5/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyJsr303ValidationMessagesPointToApiErrorsTestTest.java @@ -12,8 +12,8 @@ import java.util.List; import java.util.function.Predicate; -import javax.validation.constraints.Min; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; diff --git a/settings.gradle b/settings.gradle index 727d418..11b266a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,9 +3,8 @@ rootProject.name = 'backstopper' // Published-artifact modules include "nike-internal-util", "backstopper-core", - "backstopper-custom-validators" -// "backstopper-reusable-tests", -// "backstopper-reusable-tests-junit5", + "backstopper-custom-validators", + "backstopper-reusable-tests-junit5" // "backstopper-jackson", // "backstopper-servlet-api", // "backstopper-spring-web", From 0535b5356320374e830574d86af9fd571192509d Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Mon, 9 Sep 2024 13:52:30 -0700 Subject: [PATCH 04/42] Migrate backstopper-jackson from javax to jakarta --- backstopper-jackson/README.md | 22 +++++++++++++++------- backstopper-jackson/build.gradle | 5 ----- build.gradle | 4 ++-- settings.gradle | 4 ++-- 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/backstopper-jackson/README.md b/backstopper-jackson/README.md index 9923439..9d59e66 100644 --- a/backstopper-jackson/README.md +++ b/backstopper-jackson/README.md @@ -1,19 +1,27 @@ # Backstopper - jackson -Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 7 and greater. +Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 11 and greater. + +(NOTE: The [Backstopper 1.x branch](https://github.com/Nike-Inc/backstopper/tree/v1.x) contains a version of +Backstopper for Java 7+, and for the `javax` ecosystem. The current Backstopper supports Java 11+ and the `jakarta` +ecosystem.) This library contains some helper classes for working in an environment that uses Jackson with Backstopper. - + ## Helper classes - -* **`JsonUtilWithDefaultErrorContractDTOSupport`** - A general-purpose JSON serializer that has built-in support for the default Backstopper error contract DTO. In particular: - * If an error code is parseable to an integer then the JSON field for that error code will be serialized as a number rather than a string. + +* **`JsonUtilWithDefaultErrorContractDTOSupport`** - A general-purpose JSON serializer that has built-in support for the + default Backstopper error contract DTO. In particular: + * If an error code is parseable to an integer then the JSON field for that error code will be serialized as a number + rather than a string. * If an error has an empty metadata section then it will be omitted from the serialized JSON. - * You can create a Jackson `ObjectMapper` with any combination of the above rules turned on or off by using the `generateErrorContractObjectMapper(...)` static factory method. + * You can create a Jackson `ObjectMapper` with any combination of the above rules turned on or off by using the + `generateErrorContractObjectMapper(...)` static factory method. ## More Info -See the [base project README.md](../README.md), [User Guide](../USER_GUIDE.md), and Backstopper repository source code and javadocs for all further information. +See the [base project README.md](../README.md), [User Guide](../USER_GUIDE.md), and Backstopper repository source code +and javadocs for all further information. ## License diff --git a/backstopper-jackson/build.gradle b/backstopper-jackson/build.gradle index f067e30..1eef27a 100644 --- a/backstopper-jackson/build.gradle +++ b/backstopper-jackson/build.gradle @@ -1,10 +1,5 @@ evaluationDependsOn(':') -compileTestJava { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - dependencies { api( project(":backstopper-core"), diff --git a/build.gradle b/build.gradle index 0c681ee..4158a95 100644 --- a/build.gradle +++ b/build.gradle @@ -93,8 +93,8 @@ ext { junitVersion = '4.13.2' junit5Version = '5.8.2' mockitoVersion = '1.9.5' - logbackVersion = '1.2.3' - jacksonVersion = '2.12.7' + logbackVersion = '1.5.8' + jacksonVersion = '2.17.2' assertJVersion = '3.23.1' junitDataproviderVersion = '1.13.1' hamcrestVersion = '1.3' diff --git a/settings.gradle b/settings.gradle index 11b266a..677fe4d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -4,8 +4,8 @@ rootProject.name = 'backstopper' include "nike-internal-util", "backstopper-core", "backstopper-custom-validators", - "backstopper-reusable-tests-junit5" -// "backstopper-jackson", + "backstopper-reusable-tests-junit5", + "backstopper-jackson" // "backstopper-servlet-api", // "backstopper-spring-web", // "backstopper-spring-web-mvc", From 569ad3086e5f5d3b3848cd4f3833c4c208e6a1d8 Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Mon, 9 Sep 2024 14:31:00 -0700 Subject: [PATCH 05/42] Migrate backstopper-servlet-api from javax to jakarta --- backstopper-servlet-api/README.md | 30 +++++++++++------ backstopper-servlet-api/build.gradle | 9 ++---- .../ApiExceptionHandlerServletApiBase.java | 8 ++--- ...handledExceptionHandlerServletApiBase.java | 8 ++--- ...equestInfoForLoggingServletApiAdapter.java | 4 +-- .../UnhandledServletContainerErrorHelper.java | 15 +++++---- ...ApiExceptionHandlerServletApiBaseTest.java | 4 +-- ...ledExceptionHandlerServletApiBaseTest.java | 4 +-- ...stInfoForLoggingServletApiAdapterTest.java | 32 +++++++++++++++++-- ...andledServletContainerErrorHelperTest.java | 8 ++--- build.gradle | 2 +- settings.gradle | 4 +-- 12 files changed, 82 insertions(+), 46 deletions(-) diff --git a/backstopper-servlet-api/README.md b/backstopper-servlet-api/README.md index cb4f77c..59b8a7d 100644 --- a/backstopper-servlet-api/README.md +++ b/backstopper-servlet-api/README.md @@ -1,14 +1,25 @@ # Backstopper - servlet-api -Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 7 and greater. +Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 11 and greater. -This library is intended to be used as a base for creating framework-specific integrations with other Servlet-based frameworks that Backstopper doesn't [already have support for](../README.md#framework_modules). +(NOTE: The [Backstopper 1.x branch](https://github.com/Nike-Inc/backstopper/tree/v1.x) contains a version of +Backstopper for Java 7+, and for the `javax` ecosystem. The current Backstopper supports Java 11+ and the `jakarta` +ecosystem.) + +This library is intended to be used as a base for creating framework-specific integrations with other Servlet-based +frameworks that Backstopper doesn't [already have support for](../README.md#framework_modules). It contains the following classes: -* **`ApiExceptionHandlerServletApiBase`** - An extension of the core `ApiExceptionHandlerBase` that takes in a `HttpServletRequest` and `HttpServletResponse` and does the necessary adaptation for calling the `ApiExceptionHandlerBase` `super` methods. -* **`UnhandledExceptionHandlerServletApiBase`** - An extension of the core `UnhandledExceptionHandlerBase` that takes in a `HttpServletRequest` and `HttpServletResponse` and does the necessary adaptation for calling the `UnhandledExceptionHandlerBase` `super` methods. -* **`RequestInfoForLoggingServletApiAdapter`** - The adapter used by `ApiExceptionHandlerServletApiBase` and `UnhandledExceptionHandlerServletApiBase` for exposing `HttpServletRequest` as the `RequestInfoForLogging` needed by the core Backstopper components. +* **`ApiExceptionHandlerServletApiBase`** - An extension of the core `ApiExceptionHandlerBase` that takes in a + `HttpServletRequest` and `HttpServletResponse` and does the necessary adaptation for calling the + `ApiExceptionHandlerBase` `super` methods. +* **`UnhandledExceptionHandlerServletApiBase`** - An extension of the core `UnhandledExceptionHandlerBase` that + takes in a `HttpServletRequest` and `HttpServletResponse` and does the necessary adaptation for calling the + `UnhandledExceptionHandlerBase` `super` methods. +* **`RequestInfoForLoggingServletApiAdapter`** - The adapter used by `ApiExceptionHandlerServletApiBase` and + `UnhandledExceptionHandlerServletApiBase` for exposing `HttpServletRequest` as the `RequestInfoForLogging` needed + by the core Backstopper components. ## NOTE - Servlet API dependency required at runtime @@ -19,14 +30,15 @@ This should not affect most users since this library is likely to be used in a S required dependencies are already on the classpath at runtime, however if you receive class-not-found errors related to Servlet API classes then you'll need to pull the necessary dependency into your project. -The dependency you may need to pull in (choose one of the following, depending on your environment needs): +The dependency you may need to pull in: -* Servlet 3+ API: [javax.servlet:javax.servlet-api:\[servlet-api-version\]](https://search.maven.org/search?q=g:javax.servlet%20AND%20a:javax.servlet-api) -* Servlet 2 API: [javax.servlet:servlet-api:\[servlet-2-api-version\]](https://search.maven.org/search?q=g:javax.servlet%20AND%20a:servlet-api) +* Jakarta Servlet API: + [jakarta.servlet:jakarta.servlet-api:\[servlet-api-version\]](https://search.maven.org/search?q=g:jakarta.servlet%20AND%20a:jakarta.servlet-api) ## More Info -See the [base project README.md](../README.md), [User Guide](../USER_GUIDE.md), and Backstopper repository source code and javadocs for all further information. +See the [base project README.md](../README.md), [User Guide](../USER_GUIDE.md), and Backstopper repository source +code and javadocs for all further information. ## License diff --git a/backstopper-servlet-api/build.gradle b/backstopper-servlet-api/build.gradle index 6be9d9d..c95f0ea 100644 --- a/backstopper-servlet-api/build.gradle +++ b/backstopper-servlet-api/build.gradle @@ -1,17 +1,12 @@ evaluationDependsOn(':') -compileTestJava { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - dependencies { api( project(":backstopper-core"), ) compileOnly( "org.jetbrains:annotations:$jetbrainsAnnotationsVersion", - "javax.servlet:javax.servlet-api:$servletApiVersion", + "jakarta.servlet:jakarta.servlet-api:$servletApiVersion", ) testImplementation( "org.jetbrains:annotations:$jetbrainsAnnotationsVersion", @@ -21,6 +16,6 @@ dependencies { "org.assertj:assertj-core:$assertJVersion", "com.tngtech.java:junit-dataprovider:$junitDataproviderVersion", "org.hamcrest:hamcrest-all:$hamcrestVersion", - "javax.servlet:javax.servlet-api:$servletApiVersion", + "jakarta.servlet:jakarta.servlet-api:$servletApiVersion", ) } diff --git a/backstopper-servlet-api/src/main/java/com/nike/backstopper/handler/ApiExceptionHandlerServletApiBase.java b/backstopper-servlet-api/src/main/java/com/nike/backstopper/handler/ApiExceptionHandlerServletApiBase.java index c3f27d5..2756d29 100644 --- a/backstopper-servlet-api/src/main/java/com/nike/backstopper/handler/ApiExceptionHandlerServletApiBase.java +++ b/backstopper-servlet-api/src/main/java/com/nike/backstopper/handler/ApiExceptionHandlerServletApiBase.java @@ -7,13 +7,13 @@ import java.util.List; import java.util.Map; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; /** * Simple extension of {@link com.nike.backstopper.handler.ApiExceptionHandlerBase} that provides some convenience when * working in a Servlet API based framework. Implementors can call {@link #maybeHandleException(Throwable, - * javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)} instead of {@link + * jakarta.servlet.http.HttpServletRequest, jakarta.servlet.http.HttpServletResponse)} instead of {@link * #maybeHandleException(Throwable, RequestInfoForLogging)} to populate the servlet response's headers and status code * automatically. * @@ -47,7 +47,7 @@ public ApiExceptionHandlerServletApiBase(ProjectApiErrors projectApiErrors, * request and servlet response. The request will be wrapped in a {@link RequestInfoForLoggingServletApiAdapter} so * that it can be passed along to the method that does the work. If there are any headers in the returned {@link * ErrorResponseInfo#headersToAddToResponse} then they will be automatically added to the given servlet response, - * and {@link javax.servlet.http.HttpServletResponse#setStatus(int)} will be automatically set with {@link + * and {@link jakarta.servlet.http.HttpServletResponse#setStatus(int)} will be automatically set with {@link * ErrorResponseInfo#httpStatusCode} as well. */ public ErrorResponseInfo maybeHandleException( diff --git a/backstopper-servlet-api/src/main/java/com/nike/backstopper/handler/UnhandledExceptionHandlerServletApiBase.java b/backstopper-servlet-api/src/main/java/com/nike/backstopper/handler/UnhandledExceptionHandlerServletApiBase.java index 67bdddc..ccac32f 100644 --- a/backstopper-servlet-api/src/main/java/com/nike/backstopper/handler/UnhandledExceptionHandlerServletApiBase.java +++ b/backstopper-servlet-api/src/main/java/com/nike/backstopper/handler/UnhandledExceptionHandlerServletApiBase.java @@ -6,13 +6,13 @@ import java.util.List; import java.util.Map; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; /** * Simple extension of {@link com.nike.backstopper.handler.UnhandledExceptionHandlerBase} that provides some convenience * when working in a Servlet API based framework. Implementors can call {@link #handleException(Throwable, - * javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)} instead of {@link + * jakarta.servlet.http.HttpServletRequest, jakarta.servlet.http.HttpServletResponse)} instead of {@link * #handleException(Throwable, RequestInfoForLogging)} to populate the servlet response's headers and status code * automatically. * @@ -36,7 +36,7 @@ public UnhandledExceptionHandlerServletApiBase(ProjectApiErrors projectApiErrors * and servlet response. The request will be wrapped in a {@link RequestInfoForLoggingServletApiAdapter} so that it * can be passed along to the method that does the work. If there are any headers in the returned {@link * ErrorResponseInfo#headersToAddToResponse} then they will be automatically added to the given servlet response, - * and {@link javax.servlet.http.HttpServletResponse#setStatus(int)} will be automatically set with {@link + * and {@link jakarta.servlet.http.HttpServletResponse#setStatus(int)} will be automatically set with {@link * ErrorResponseInfo#httpStatusCode} as well. */ public ErrorResponseInfo handleException(Throwable ex, HttpServletRequest servletRequest, diff --git a/backstopper-servlet-api/src/main/java/com/nike/backstopper/handler/adapter/RequestInfoForLoggingServletApiAdapter.java b/backstopper-servlet-api/src/main/java/com/nike/backstopper/handler/adapter/RequestInfoForLoggingServletApiAdapter.java index d1ef08e..04966aa 100644 --- a/backstopper-servlet-api/src/main/java/com/nike/backstopper/handler/adapter/RequestInfoForLoggingServletApiAdapter.java +++ b/backstopper-servlet-api/src/main/java/com/nike/backstopper/handler/adapter/RequestInfoForLoggingServletApiAdapter.java @@ -16,8 +16,8 @@ import java.util.List; import java.util.Map; -import javax.servlet.ServletInputStream; -import javax.servlet.http.HttpServletRequest; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.http.HttpServletRequest; /** * Adapter that allows {@link HttpServletRequest} to be used as a {@link RequestInfoForLogging}. diff --git a/backstopper-servlet-api/src/main/java/com/nike/backstopper/servletapi/UnhandledServletContainerErrorHelper.java b/backstopper-servlet-api/src/main/java/com/nike/backstopper/servletapi/UnhandledServletContainerErrorHelper.java index f572af8..e671626 100644 --- a/backstopper-servlet-api/src/main/java/com/nike/backstopper/servletapi/UnhandledServletContainerErrorHelper.java +++ b/backstopper-servlet-api/src/main/java/com/nike/backstopper/servletapi/UnhandledServletContainerErrorHelper.java @@ -12,9 +12,9 @@ import java.util.Collections; import java.util.List; -import javax.inject.Named; -import javax.inject.Singleton; -import javax.servlet.ServletRequest; +import jakarta.inject.Named; +import jakarta.inject.Singleton; +import jakarta.servlet.ServletRequest; /** * This class is intended to help with integrating Backstopper with Servlet containers for handling otherwise-unhandled @@ -45,6 +45,7 @@ @SuppressWarnings("WeakerAccess") public class UnhandledServletContainerErrorHelper { + // TODO javax-to-jakarta: Test these things in with the new spring/springboot libs/frameworks. protected static final List DEFAULT_THROWABLE_REQUEST_ATTR_NAMES = Arrays.asList( // Try the Springboot 2 attrs first. // Corresponds to org.springframework.boot.web.reactive.error.DefaultErrorAttributes.ERROR_ATTRIBUTE. @@ -57,14 +58,14 @@ public class UnhandledServletContainerErrorHelper { "org.springframework.boot.autoconfigure.web.DefaultErrorAttributes.ERROR", // Fall back to the Servlet API value last. - // Corresponds to javax.servlet.RequestDispatcher.ERROR_EXCEPTION. - "javax.servlet.error.exception" + // Corresponds to jakarta.servlet.RequestDispatcher.ERROR_EXCEPTION. + "jakarta.servlet.error.exception" ); protected static final List DEFAULT_ERROR_STATUS_CODE_REQUEST_ATTR_NAMES = Collections.singletonList( // Servlet API value. - // Corresponds to javax.servlet.RequestDispatcher.ERROR_STATUS_CODE. - "javax.servlet.error.status_code" + // Corresponds to jakarta.servlet.RequestDispatcher.ERROR_STATUS_CODE. + "jakarta.servlet.error.status_code" ); public @NotNull Throwable extractOrGenerateErrorForRequest( diff --git a/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerServletApiBaseTest.java b/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerServletApiBaseTest.java index 4b3e71e..47e7447 100644 --- a/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerServletApiBaseTest.java +++ b/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerServletApiBaseTest.java @@ -15,8 +15,8 @@ import java.util.List; import java.util.Map; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; diff --git a/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/UnhandledExceptionHandlerServletApiBaseTest.java b/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/UnhandledExceptionHandlerServletApiBaseTest.java index f192fd9..e2af49e 100644 --- a/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/UnhandledExceptionHandlerServletApiBaseTest.java +++ b/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/UnhandledExceptionHandlerServletApiBaseTest.java @@ -13,8 +13,8 @@ import java.util.List; import java.util.Map; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; diff --git a/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/adapter/RequestInfoForLoggingServletApiAdapterTest.java b/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/adapter/RequestInfoForLoggingServletApiAdapterTest.java index 0ad1e93..5a0e310 100644 --- a/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/adapter/RequestInfoForLoggingServletApiAdapterTest.java +++ b/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/adapter/RequestInfoForLoggingServletApiAdapterTest.java @@ -7,6 +7,7 @@ import org.hamcrest.core.Is; import org.junit.Before; import org.junit.Test; +import org.slf4j.LoggerFactory; import java.io.BufferedReader; import java.io.ByteArrayInputStream; @@ -19,8 +20,9 @@ import java.util.TreeMap; import java.util.UUID; -import javax.servlet.ServletInputStream; -import javax.servlet.http.HttpServletRequest; +import jakarta.servlet.ReadListener; +import jakarta.servlet.ServletInputStream; +import jakarta.servlet.http.HttpServletRequest; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.notNullValue; @@ -219,5 +221,31 @@ public void close() throws IOException { this.sourceStream.close(); } + @Override + public boolean isFinished() { + try { + return this.sourceStream.available() <= 0; + } + catch (IOException e) { + LoggerFactory.getLogger(this.getClass()).error("An error occurred asking for available bytes from the underlying stream.", e); + return true; + } + } + + @Override + public boolean isReady() { + try { + return this.sourceStream.available() > 0; + } + catch (IOException e) { + LoggerFactory.getLogger(this.getClass()).error("An error occurred asking for available bytes from the underlying stream.", e); + return false; + } + } + + @Override + public void setReadListener(ReadListener readListener) { + // Do nothing. + } } } \ No newline at end of file diff --git a/backstopper-servlet-api/src/test/java/com/nike/backstopper/servletapi/UnhandledServletContainerErrorHelperTest.java b/backstopper-servlet-api/src/test/java/com/nike/backstopper/servletapi/UnhandledServletContainerErrorHelperTest.java index c76f346..70fb1a8 100644 --- a/backstopper-servlet-api/src/test/java/com/nike/backstopper/servletapi/UnhandledServletContainerErrorHelperTest.java +++ b/backstopper-servlet-api/src/test/java/com/nike/backstopper/servletapi/UnhandledServletContainerErrorHelperTest.java @@ -22,8 +22,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import javax.servlet.RequestDispatcher; -import javax.servlet.ServletRequest; +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.ServletRequest; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; @@ -160,7 +160,7 @@ public void extractOrGenerateErrorForRequest_generates_synthetic_ApiException_fo Synthetic404Scenario scenario ) { // given - doReturn(scenario.statusCodeAttr).when(requestMock).getAttribute("javax.servlet.error.status_code"); + doReturn(scenario.statusCodeAttr).when(requestMock).getAttribute("jakarta.servlet.error.status_code"); // when Throwable result = helper.extractOrGenerateErrorForRequest(requestMock, projectApiErrors); @@ -208,7 +208,7 @@ public void extractOrGenerateErrorForRequest_generates_synthetic_ApiException_fo Synthetic500Scenario scenario ) { // given - doReturn(scenario.statusCodeAttr).when(requestMock).getAttribute("javax.servlet.error.status_code"); + doReturn(scenario.statusCodeAttr).when(requestMock).getAttribute("jakarta.servlet.error.status_code"); // when Throwable result = helper.extractOrGenerateErrorForRequest(requestMock, projectApiErrors); diff --git a/build.gradle b/build.gradle index 4158a95..c353137 100644 --- a/build.gradle +++ b/build.gradle @@ -80,7 +80,7 @@ ext { slf4jVersion = '1.7.36' jakartaInjectVersion = '2.0.1' jakartaValidationVersion = '3.0.2' - servletApiVersion = '3.0.1' + servletApiVersion = '6.0.0' // Compatible with Jakarta EE 10 spring4Version = '4.3.2.RELEASE' spring5Version = '5.1.8.RELEASE' springboot1Version = '1.5.2.RELEASE' diff --git a/settings.gradle b/settings.gradle index 677fe4d..4edb20d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,8 +5,8 @@ include "nike-internal-util", "backstopper-core", "backstopper-custom-validators", "backstopper-reusable-tests-junit5", - "backstopper-jackson" -// "backstopper-servlet-api", + "backstopper-jackson", + "backstopper-servlet-api" // "backstopper-spring-web", // "backstopper-spring-web-mvc", // "backstopper-spring-web-flux", From 7ae8f411aa6b92d9f708c101b6cf387de10cd532 Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Mon, 9 Sep 2024 15:23:10 -0700 Subject: [PATCH 06/42] Make changes to support running builds on java 17 --- .../com/nike/backstopper/exception/ApiException.java | 2 +- .../handler/ApiExceptionHandlerBaseTest.java | 12 ++++++------ .../handler/UnhandledExceptionHandlerBaseTest.java | 8 ++++---- .../service/ClientDataValidationServiceTest.java | 8 ++++---- .../ApiExceptionHandlerServletApiBaseTest.java | 2 +- .../UnhandledExceptionHandlerServletApiBaseTest.java | 2 +- .../RequestInfoForLoggingServletApiAdapterTest.java | 2 +- build.gradle | 4 ++-- nike-internal-util/build.gradle | 5 ----- 9 files changed, 20 insertions(+), 25 deletions(-) diff --git a/backstopper-core/src/main/java/com/nike/backstopper/exception/ApiException.java b/backstopper-core/src/main/java/com/nike/backstopper/exception/ApiException.java index 118e7b9..6c5109a 100644 --- a/backstopper-core/src/main/java/com/nike/backstopper/exception/ApiException.java +++ b/backstopper-core/src/main/java/com/nike/backstopper/exception/ApiException.java @@ -215,7 +215,7 @@ protected static String extractMessage(ApiError error) { } /** - * Extracts and joins all messages from the input List<{@link ApiError}> if the desired message is null. + * Extracts and joins all messages from the input List<{@link ApiError}> if the desired message is null. * * Will return null if the input error List is null */ diff --git a/backstopper-core/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerBaseTest.java b/backstopper-core/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerBaseTest.java index 8b1bf3a..9db4930 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerBaseTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerBaseTest.java @@ -50,14 +50,14 @@ import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyCollection; -import static org.mockito.Matchers.anyInt; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyCollection; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; /** @@ -497,7 +497,7 @@ public void shouldLogStackTrace_has_expected_default_behavior(int statusCode, bo // then Assertions.assertThat(result).isEqualTo(expectedResult); - verifyZeroInteractions(errorsCollectionMock, originalExceptionMock, coreExceptionMock, reqMock); + verifyNoMoreInteractions(errorsCollectionMock, originalExceptionMock, coreExceptionMock, reqMock); } @DataProvider(value = { @@ -542,7 +542,7 @@ public void shouldLogStackTrace_honors_ApiException_with_StackTraceLoggingBehavi // then Assertions.assertThat(result).isEqualTo(expectedResult); - verifyZeroInteractions(errorsCollectionMock, originalExceptionMock, reqMock); + verifyNoMoreInteractions(errorsCollectionMock, originalExceptionMock, reqMock); } // DEFAULT_WRAPPER_EXCEPTION_CLASS_NAMES should contain at least WrapperException, ExecutionException, CompletionException diff --git a/backstopper-core/src/test/java/com/nike/backstopper/handler/UnhandledExceptionHandlerBaseTest.java b/backstopper-core/src/test/java/com/nike/backstopper/handler/UnhandledExceptionHandlerBaseTest.java index 04d7570..91d4523 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/handler/UnhandledExceptionHandlerBaseTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/handler/UnhandledExceptionHandlerBaseTest.java @@ -33,10 +33,10 @@ import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyInt; -import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; diff --git a/backstopper-core/src/test/java/com/nike/backstopper/service/ClientDataValidationServiceTest.java b/backstopper-core/src/test/java/com/nike/backstopper/service/ClientDataValidationServiceTest.java index 393fd28..a84cf11 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/service/ClientDataValidationServiceTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/service/ClientDataValidationServiceTest.java @@ -21,12 +21,12 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder; import static org.mockito.BDDMockito.given; -import static org.mockito.Matchers.any; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; /** * Verifies the functionality of {@link com.nike.backstopper.service.ClientDataValidationService} @@ -90,13 +90,13 @@ public void shouldDelegateValidateObjectsWithGroupsFailFastCollectionMethodWithE @Test public void validateObjectsWithGroupsFailFastShouldDoNothingIfObjectsArrayIsNull() { validationServiceSpy.validateObjectsWithGroupsFailFast((Class[])null, (Object[])null); - verifyZeroInteractions(validatorMock); + verifyNoMoreInteractions(validatorMock); } @Test public void validateObjectsWithGroupsFailFastShouldDoNothingIfObjectsArrayIsEmpty() { validationServiceSpy.validateObjectsWithGroupsFailFast((Class[])null, new Object[0]); - verifyZeroInteractions(validatorMock); + verifyNoMoreInteractions(validatorMock); } @Test diff --git a/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerServletApiBaseTest.java b/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerServletApiBaseTest.java index 47e7447..80c030a 100644 --- a/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerServletApiBaseTest.java +++ b/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerServletApiBaseTest.java @@ -21,7 +21,7 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.sameInstance; -import static org.mockito.Matchers.any; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; diff --git a/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/UnhandledExceptionHandlerServletApiBaseTest.java b/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/UnhandledExceptionHandlerServletApiBaseTest.java index e2af49e..6966029 100644 --- a/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/UnhandledExceptionHandlerServletApiBaseTest.java +++ b/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/UnhandledExceptionHandlerServletApiBaseTest.java @@ -19,7 +19,7 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.sameInstance; -import static org.mockito.Matchers.any; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; diff --git a/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/adapter/RequestInfoForLoggingServletApiAdapterTest.java b/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/adapter/RequestInfoForLoggingServletApiAdapterTest.java index 5a0e310..63654c0 100644 --- a/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/adapter/RequestInfoForLoggingServletApiAdapterTest.java +++ b/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/adapter/RequestInfoForLoggingServletApiAdapterTest.java @@ -28,7 +28,7 @@ import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.sameInstance; import static org.hamcrest.core.Is.is; -import static org.mockito.Matchers.any; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; diff --git a/build.gradle b/build.gradle index c353137..e74af62 100644 --- a/build.gradle +++ b/build.gradle @@ -92,7 +92,7 @@ ext { junitVersion = '4.13.2' junit5Version = '5.8.2' - mockitoVersion = '1.9.5' + mockitoVersion = '5.13.0' logbackVersion = '1.5.8' jacksonVersion = '2.17.2' assertJVersion = '3.23.1' @@ -110,7 +110,7 @@ ext { restAssuredVersion = '3.0.1' // JACOCO PROPERTIES - jacocoToolVersion = '0.8.4' + jacocoToolVersion = '0.8.12' // Anything in this jacocoExclusions list will be excluded from coverage reports. The format is paths to class // files, with wildcards allowed. e.g.: jacocoExclusions = [ "com/nike/Foo.class", "**/Bar*.*" ] jacocoExclusions = [] diff --git a/nike-internal-util/build.gradle b/nike-internal-util/build.gradle index 0d70bd5..c7f993f 100644 --- a/nike-internal-util/build.gradle +++ b/nike-internal-util/build.gradle @@ -4,11 +4,6 @@ version=nikeInternalUtilVersion groupId = nikeInternalUtilGroupId group = nikeInternalUtilGroupId -compileTestJava { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - dependencies { testImplementation( "junit:junit:$junitVersion", From c696a5d4f3ebd56a5bf7fa83d2e5089897c4626c Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Mon, 9 Sep 2024 15:31:24 -0700 Subject: [PATCH 07/42] Switch to java 17 --- backstopper-core/README.md | 4 ++-- backstopper-custom-validators/README.md | 4 ++-- backstopper-jackson/README.md | 4 ++-- backstopper-reusable-tests-junit5/README.md | 4 ++-- backstopper-servlet-api/README.md | 4 ++-- build.gradle | 9 +++++++-- 6 files changed, 17 insertions(+), 12 deletions(-) diff --git a/backstopper-core/README.md b/backstopper-core/README.md index ba78c33..b79a0ab 100644 --- a/backstopper-core/README.md +++ b/backstopper-core/README.md @@ -1,9 +1,9 @@ # Backstopper - core -Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 11 and greater. +Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 17 and greater. (NOTE: The [Backstopper 1.x branch](https://github.com/Nike-Inc/backstopper/tree/v1.x) contains a version of -Backstopper for Java 7+, and for the `javax` ecosystem. The current Backstopper supports Java 11+ and the `jakarta` +Backstopper for Java 7+, and for the `javax` ecosystem. The current Backstopper supports Java 17+ and the `jakarta` ecosystem.) This `backstopper-core` library contains the key core components necessary for a Backstopper system to work. diff --git a/backstopper-custom-validators/README.md b/backstopper-custom-validators/README.md index 61f65d4..1523f82 100644 --- a/backstopper-custom-validators/README.md +++ b/backstopper-custom-validators/README.md @@ -1,9 +1,9 @@ # Backstopper - custom-validators -Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 11 and greater. +Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 17 and greater. (NOTE: The [Backstopper 1.x branch](https://github.com/Nike-Inc/backstopper/tree/v1.x) contains a version of -Backstopper for Java 7+, and for the `javax` ecosystem. The current Backstopper supports Java 11+ and the `jakarta` +Backstopper for Java 7+, and for the `javax` ecosystem. The current Backstopper supports Java 17+ and the `jakarta` ecosystem.) This library contains JSR 303 Bean Validation annotations that have proven to be useful and reusable. These are entirely diff --git a/backstopper-jackson/README.md b/backstopper-jackson/README.md index 9d59e66..63c23bb 100644 --- a/backstopper-jackson/README.md +++ b/backstopper-jackson/README.md @@ -1,9 +1,9 @@ # Backstopper - jackson -Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 11 and greater. +Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 17 and greater. (NOTE: The [Backstopper 1.x branch](https://github.com/Nike-Inc/backstopper/tree/v1.x) contains a version of -Backstopper for Java 7+, and for the `javax` ecosystem. The current Backstopper supports Java 11+ and the `jakarta` +Backstopper for Java 7+, and for the `javax` ecosystem. The current Backstopper supports Java 17+ and the `jakarta` ecosystem.) This library contains some helper classes for working in an environment that uses Jackson with Backstopper. diff --git a/backstopper-reusable-tests-junit5/README.md b/backstopper-reusable-tests-junit5/README.md index 38c66cd..4cbd7eb 100644 --- a/backstopper-reusable-tests-junit5/README.md +++ b/backstopper-reusable-tests-junit5/README.md @@ -1,9 +1,9 @@ # Backstopper - reusable-tests -Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 11 and greater. +Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 17 and greater. (NOTE: The [Backstopper 1.x branch](https://github.com/Nike-Inc/backstopper/tree/v1.x) contains a version of -Backstopper for Java 7+, and for the `javax` ecosystem. The current Backstopper supports Java 11+ and the `jakarta` +Backstopper for Java 7+, and for the `javax` ecosystem. The current Backstopper supports Java 17+ and the `jakarta` ecosystem.) This library contains some reusable unit test classes that should be integrated into every Backstopper-enabled diff --git a/backstopper-servlet-api/README.md b/backstopper-servlet-api/README.md index 59b8a7d..82c65fa 100644 --- a/backstopper-servlet-api/README.md +++ b/backstopper-servlet-api/README.md @@ -1,9 +1,9 @@ # Backstopper - servlet-api -Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 11 and greater. +Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 17 and greater. (NOTE: The [Backstopper 1.x branch](https://github.com/Nike-Inc/backstopper/tree/v1.x) contains a version of -Backstopper for Java 7+, and for the `javax` ecosystem. The current Backstopper supports Java 11+ and the `jakarta` +Backstopper for Java 7+, and for the `javax` ecosystem. The current Backstopper supports Java 17+ and the `jakarta` ecosystem.) This library is intended to be used as a base for creating framework-specific integrations with other Servlet-based diff --git a/build.gradle b/build.gradle index e74af62..257a33a 100644 --- a/build.gradle +++ b/build.gradle @@ -60,8 +60,13 @@ allprojects { } // http://www.gradle.org/docs/current/userguide/java_plugin.html - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + } } // Disable jar, sourcesJar, and javadoc tasks for the root project - we only want them to run for submodules From 5871421df68be66d80cb4fa057de597e9d0220d0 Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Mon, 9 Sep 2024 17:43:05 -0700 Subject: [PATCH 08/42] Migrate backstopper-spring-web from javax to jakarta --- backstopper-spring-web/README.md | 8 +- backstopper-spring-web/build.gradle | 21 ++-- ...idationErrorToApiErrorHandlerListener.java | 98 ++--------------- ...mmonFrameworkExceptionHandlerListener.java | 8 +- ...ionErrorToApiErrorHandlerListenerTest.java | 103 ------------------ ...FrameworkExceptionHandlerListenerTest.java | 20 +--- .../async/AsyncRequestTimeoutException.java | 11 -- .../web/servlet/NoHandlerFoundException.java | 10 -- .../NoSuchRequestHandlingMethodException.java | 10 -- build.gradle | 4 +- settings.gradle | 4 +- 11 files changed, 34 insertions(+), 263 deletions(-) delete mode 100644 backstopper-spring-web/src/test/java/org/springframework/web/context/request/async/AsyncRequestTimeoutException.java delete mode 100644 backstopper-spring-web/src/test/java/org/springframework/web/servlet/NoHandlerFoundException.java delete mode 100644 backstopper-spring-web/src/test/java/org/springframework/web/servlet/mvc/multiaction/NoSuchRequestHandlingMethodException.java diff --git a/backstopper-spring-web/README.md b/backstopper-spring-web/README.md index bf09541..5006e96 100644 --- a/backstopper-spring-web/README.md +++ b/backstopper-spring-web/README.md @@ -1,6 +1,10 @@ # Backstopper - spring-web -Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 7 and greater. +Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 17 and greater. + +(NOTE: The [Backstopper 1.x branch](https://github.com/Nike-Inc/backstopper/tree/v1.x) contains a version of +Backstopper for Java 7+, and for the `javax` ecosystem. The current Backstopper supports Java 17+ and the `jakarta` +ecosystem. It also contains support for Spring 4 and 5, and Springboot 1 and 2.) This `backstopper-spring-web` module is not meant to be used standalone. It is here to provide common code for any `spring-web*` based application, including both Spring Web MVC and Spring WebFlux applications. But this module @@ -9,6 +13,8 @@ does not provide Spring+Backstopper integration by itself. To integrate Backstopper with your Spring application, please choose the correct concrete integration library, depending on which Spring environment your application is running in: +// TODO javax-to-jakarta: Fix these links to other libraries after the refactor is complete. + ### Spring WebFlux based applications * [backstopper-spring-web-flux](../backstopper-spring-web-flux) - For Spring WebFlux applications. diff --git a/backstopper-spring-web/build.gradle b/backstopper-spring-web/build.gradle index 25dcae7..bea7da1 100644 --- a/backstopper-spring-web/build.gradle +++ b/backstopper-spring-web/build.gradle @@ -1,21 +1,13 @@ evaluationDependsOn(':') -compileTestJava { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - -ext { - springSecurityVersionForTesting = '5.1.6.RELEASE' -} - dependencies { api( project(":backstopper-core"), ) compileOnly( "org.jetbrains:annotations:$jetbrainsAnnotationsVersion", - "org.springframework:spring-web:$spring4Version", + "org.springframework:spring-web:$spring6Version", + "org.springframework:spring-context:$spring6Version", ) testImplementation( project(":backstopper-core").sourceSets.test.output, @@ -27,8 +19,11 @@ dependencies { "com.tngtech.java:junit-dataprovider:$junitDataproviderVersion", "com.fasterxml.jackson.core:jackson-core:$jacksonVersion", "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion", - "org.hibernate:hibernate-validator:$hibernateValidatorVersion", - "org.springframework:spring-web:$spring5Version", - "org.springframework.security:spring-security-core:$springSecurityVersionForTesting", + "org.hibernate.validator:hibernate-validator:$hibernateValidatorVersion", + "org.springframework:spring-web:$spring6Version", + "org.springframework:spring-context:$spring6Version", + "org.springframework.security:spring-security-core:$springSecurityVersion", + "org.springframework:spring-webmvc:$spring6Version", + "jakarta.servlet:jakarta.servlet-api:$servletApiVersion", ) } diff --git a/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/ConventionBasedSpringValidationErrorToApiErrorHandlerListener.java b/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/ConventionBasedSpringValidationErrorToApiErrorHandlerListener.java index 8cee337..cb71071 100644 --- a/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/ConventionBasedSpringValidationErrorToApiErrorHandlerListener.java +++ b/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/ConventionBasedSpringValidationErrorToApiErrorHandlerListener.java @@ -8,23 +8,19 @@ import com.nike.backstopper.handler.listener.ApiExceptionHandlerListenerResult; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.validation.BindException; +import org.springframework.validation.Errors; import org.springframework.validation.FieldError; import org.springframework.validation.ObjectError; -import org.springframework.web.bind.MethodArgumentNotValidException; -import java.lang.reflect.Method; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Objects; -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Singleton; /** * Handles exceptions from the Spring framework that represent JSR 303 validation errors. This handler assumes you're @@ -41,44 +37,8 @@ public class ConventionBasedSpringValidationErrorToApiErrorHandlerListener imple private static final Logger logger = LoggerFactory.getLogger(ConventionBasedSpringValidationErrorToApiErrorHandlerListener.class); - // WebExchangeBindException is Spring 5+, but we might be running in Spring 4. - // So we have to access WebExchangeBindException.getAllErrors() by reflection. :( - protected static final String WEB_EXCHANGE_BIND_EXCEPTION_CLASSNAME = - "org.springframework.web.bind.support.WebExchangeBindException"; - - protected static final @Nullable Method webExchangeBindExGetAllErrorsMethod; - protected final ProjectApiErrors projectApiErrors; - static { - webExchangeBindExGetAllErrorsMethod = extractGetAllErrorsMethod(WEB_EXCHANGE_BIND_EXCEPTION_CLASSNAME); - } - - static @Nullable Method extractGetAllErrorsMethod(String classname) { - try { - Class webExchangeBindExClass = Class.forName(classname); - Method methodToUse = webExchangeBindExClass.getDeclaredMethod("getAllErrors"); - methodToUse.setAccessible(true); - return methodToUse; - } - catch (ClassNotFoundException e) { - // Do nothing - this is expected when running in Spring 4. - } - catch (Exception e) { - // This should hopefully never happen - but if it does, at least log the error - // so the user knows what happened. - logger.error( - "Unable to retrieve the getAllErrors() method from the class {}. Backstopper will be unable to " - + "provide full error details to the user when encountering this exception.", - classname, - e - ); - } - - // We were unable to successfully pull the desired method (for whatever reason), so return null. - return null; - } - /** * @param projectApiErrors The {@link ProjectApiErrors} that should be used by this instance when finding {@link * ApiError}s. Cannot be null. @@ -98,26 +58,13 @@ public ConventionBasedSpringValidationErrorToApiErrorHandlerListener( @Override public ApiExceptionHandlerListenerResult shouldHandleException(Throwable ex) { - if (ex instanceof MethodArgumentNotValidException) { - return ApiExceptionHandlerListenerResult.handleResponse( - convertSpringErrorsToApiErrors( - ((MethodArgumentNotValidException) ex).getBindingResult().getAllErrors() - ) - ); - } - - if (ex instanceof BindException) { - return ApiExceptionHandlerListenerResult.handleResponse( - convertSpringErrorsToApiErrors(((BindException) ex).getAllErrors()) - ); - } - - String exClassname = (ex == null) ? null : ex.getClass().getName(); - if (Objects.equals(exClassname, WEB_EXCHANGE_BIND_EXCEPTION_CLASSNAME)) { - List objectErrors = extractAllErrorsFromWebExchangeBindException(ex); - if (objectErrors != null && !objectErrors.isEmpty()) { + if (ex instanceof Errors) { + Errors errEx = (Errors) ex; + List errList = errEx.getAllErrors(); + //noinspection ConstantValue + if (errList != null && !errList.isEmpty()) { return ApiExceptionHandlerListenerResult.handleResponse( - convertSpringErrorsToApiErrors(objectErrors) + convertSpringErrorsToApiErrors(errList) ); } } @@ -126,31 +73,6 @@ public ApiExceptionHandlerListenerResult shouldHandleException(Throwable ex) { return ApiExceptionHandlerListenerResult.ignoreResponse(); } - protected @Nullable List extractAllErrorsFromWebExchangeBindException( - @NotNull Throwable ex - ) { - Method getAllErrorsMethod = getWebExchangeBindExGetAllErrorsMethod(); - if (getAllErrorsMethod == null) { - return null; - } - - try { - return (List) getAllErrorsMethod.invoke(ex); - } - catch (Exception e) { - logger.warn( - "Unexpected error occurred while trying to access WebExchangeBindException.getAllErrors(). " - + "Backstopper will be unable to provide full error details to the user for this exception.", - e - ); - return null; - } - } - - protected @Nullable Method getWebExchangeBindExGetAllErrorsMethod() { - return webExchangeBindExGetAllErrorsMethod; - } - /** * Helper method for translating the given springErrors set into a set of {@link ApiError} objects by calling {@link * #convertSpringErrorToApiError(ObjectError)} on each one. diff --git a/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListener.java b/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListener.java index a0dfbea..153d25f 100644 --- a/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListener.java +++ b/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListener.java @@ -55,10 +55,7 @@ public abstract class OneOffSpringCommonFrameworkExceptionHandlerListener implem // Support all the various 404 cases from competing dependencies using classname matching. protected final Set DEFAULT_TO_404_CLASSNAMES = new LinkedHashSet<>(Arrays.asList( // NoHandlerFoundException is found in the spring-webmvc dependency, not spring-web. - "org.springframework.web.servlet.NoHandlerFoundException", - // NoSuchRequestHandlingMethodException is a deprecated Spring 4.x exception that doesn't appear in - // Spring 5 but should be translated to a 404. - "org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException" + "org.springframework.web.servlet.NoHandlerFoundException" )); // Support Spring Security exceptions that should map to a 403. @@ -75,8 +72,7 @@ public abstract class OneOffSpringCommonFrameworkExceptionHandlerListener implem "org.springframework.security.authentication.DisabledException", "org.springframework.security.authentication.CredentialsExpiredException", "org.springframework.security.authentication.AccountExpiredException", - "org.springframework.security.core.userdetails.UsernameNotFoundException", - "org.springframework.security.authentication.rcp.RemoteAuthenticationException" + "org.springframework.security.core.userdetails.UsernameNotFoundException" )); // Support 503 cases from competing dependencies using classname matching. diff --git a/backstopper-spring-web/src/test/java/com/nike/backstopper/handler/spring/listener/impl/ConventionBasedSpringValidationErrorToApiErrorHandlerListenerTest.java b/backstopper-spring-web/src/test/java/com/nike/backstopper/handler/spring/listener/impl/ConventionBasedSpringValidationErrorToApiErrorHandlerListenerTest.java index 44fe4bb..4ce16a5 100644 --- a/backstopper-spring-web/src/test/java/com/nike/backstopper/handler/spring/listener/impl/ConventionBasedSpringValidationErrorToApiErrorHandlerListenerTest.java +++ b/backstopper-spring-web/src/test/java/com/nike/backstopper/handler/spring/listener/impl/ConventionBasedSpringValidationErrorToApiErrorHandlerListenerTest.java @@ -25,18 +25,13 @@ import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.support.WebExchangeBindException; -import java.lang.reflect.Method; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.UUID; -import static com.nike.backstopper.handler.spring.listener.impl.ConventionBasedSpringValidationErrorToApiErrorHandlerListener.WEB_EXCHANGE_BIND_EXCEPTION_CLASSNAME; -import static com.nike.backstopper.handler.spring.listener.impl.ConventionBasedSpringValidationErrorToApiErrorHandlerListener.extractGetAllErrorsMethod; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -80,35 +75,6 @@ public void constructor_throws_IllegalArgumentException_if_passed_null() { assertThat(ex).isInstanceOf(IllegalArgumentException.class); } - @Test - public void const_WEB_EXCHANGE_BIND_EXCEPTION_CLASSNAME_equals_WebExchangeBindException() { - // expect - assertThat(WEB_EXCHANGE_BIND_EXCEPTION_CLASSNAME).isEqualTo(WebExchangeBindException.class.getName()); - } - - @Test - public void extractGetAllErrorsMethod_works_as_expected_for_WebExchangeBindException() - throws NoSuchMethodException { - - // when - Method result = extractGetAllErrorsMethod(WEB_EXCHANGE_BIND_EXCEPTION_CLASSNAME); - - // then - assertThat(result).isEqualTo(WebExchangeBindException.class.getDeclaredMethod("getAllErrors")); - } - - @Test - public void extractGetAllErrorsMethod_returns_null_if_class_not_found() { - // expect - assertThat(extractGetAllErrorsMethod("does.not.exist.Foo")).isNull(); - } - - @Test - public void extractGetAllErrorsMethod_returns_null_for_unexpected_exception() { - // expect - assertThat(extractGetAllErrorsMethod(this.getClass().getName())).isNull(); - } - @Test public void shouldHandleException_gracefully_ignores_when_the_exception_is_null() { // when @@ -197,36 +163,6 @@ public void shouldHandleException_handles_WebExchangeBindException_as_expected() verify(bindingResult).getAllErrors(); } - @Test - public void shouldHandleException_delegates_to_extractAllErrorsFromWebExchangeBindException_for_WebExchangeBindException_error_retrieval() { - // given - ConventionBasedSpringValidationErrorToApiErrorHandlerListener listenerSpy = spy(listener); - - WebExchangeBindException ex = new WebExchangeBindException(null, mock(BindingResult.class)); - - ApiError someFieldError = testProjectApiErrors.getMissingExpectedContentApiError(); - ApiError otherFieldError = testProjectApiErrors.getTypeConversionApiError(); - ApiError notAFieldError = testProjectApiErrors.getGenericBadRequestApiError(); - List errorsList = Arrays.asList( - new FieldError("someObj", "someField", someFieldError.getName()), - new FieldError("otherObj", "otherField", otherFieldError.getName()), - new ObjectError("notAFieldObject", notAFieldError.getName()) - ); - - doReturn(errorsList).when(listenerSpy).extractAllErrorsFromWebExchangeBindException(ex); - - // when - ApiExceptionHandlerListenerResult result = listenerSpy.shouldHandleException(ex); - - // then - validateResponse(result, true, Arrays.asList( - new ApiErrorWithMetadata(someFieldError, Pair.of("field", "someField")), - new ApiErrorWithMetadata(otherFieldError, Pair.of("field", "otherField")), - notAFieldError - )); - verify(listenerSpy).extractAllErrorsFromWebExchangeBindException(ex); - } - @DataProvider(value = { "true", "false" @@ -251,45 +187,6 @@ public void shouldHandleException_ignores_WebExchangeBindException_that_has_null verify(bindingResult).getAllErrors(); } - @Test - public void extractAllErrorsFromWebExchangeBindException_works_as_expected_for_WebExchangeBindException() { - // given - WebExchangeBindException exMock = mock(WebExchangeBindException.class); - List expectedErrorsList = mock(List.class); - - doReturn(expectedErrorsList).when(exMock).getAllErrors(); - - // when - List result = listener.extractAllErrorsFromWebExchangeBindException(exMock); - - // then - assertThat(result).isSameAs(expectedErrorsList); - } - - @Test - public void extractAllErrorsFromWebExchangeBindException_returns_null_if_extraction_method_is_null() { - // given - ConventionBasedSpringValidationErrorToApiErrorHandlerListener listenerSpy = spy(listener); - doReturn(null).when(listenerSpy).getWebExchangeBindExGetAllErrorsMethod(); - - // when - List result = listenerSpy.extractAllErrorsFromWebExchangeBindException( - mock(WebExchangeBindException.class) - ); - - // then - assertThat(result).isNull(); - verify(listenerSpy).getWebExchangeBindExGetAllErrorsMethod(); - } - @Test - public void extractAllErrorsFromWebExchangeBindException_returns_null_if_unexpected_exception_occurs() { - // when - List result = listener.extractAllErrorsFromWebExchangeBindException(new RuntimeException()); - - // then - assertThat(result).isNull(); - } - @Test public void shouldDefaultToGENERIC_SERVICE_ERRORIfMessageIsNotValidApiErrorEnumName() { BindingResult bindingResult = mock(BindingResult.class); diff --git a/backstopper-spring-web/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListenerTest.java b/backstopper-spring-web/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListenerTest.java index 6410914..6d25a8a 100644 --- a/backstopper-spring-web/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListenerTest.java +++ b/backstopper-spring-web/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListenerTest.java @@ -24,6 +24,7 @@ import org.springframework.beans.ConversionNotSupportedException; import org.springframework.beans.TypeMismatchException; import org.springframework.core.MethodParameter; +import org.springframework.http.HttpHeaders; import org.springframework.http.converter.HttpMessageConversionException; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; @@ -35,13 +36,11 @@ import org.springframework.security.authentication.DisabledException; import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.authentication.LockedException; -import org.springframework.security.authentication.rcp.RemoteAuthenticationException; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.web.context.request.async.AsyncRequestTimeoutException; import org.springframework.web.method.annotation.MethodArgumentConversionNotSupportedException; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import org.springframework.web.servlet.NoHandlerFoundException; -import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException; import java.util.ArrayList; import java.util.Arrays; @@ -435,19 +434,7 @@ public void shouldHandleException_returns_TEMPORARY_SERVICE_PROBLEM_for_AsyncReq @Test public void shouldHandleException_should_return_not_found_error_when_passed_NoHandlerFoundException() { // given - NoHandlerFoundException ex = new NoHandlerFoundException(); - - // when - ApiExceptionHandlerListenerResult result = listener.shouldHandleException(ex); - - // then - validateResponse(result, true, singletonList(testProjectApiErrors.getNotFoundApiError())); - } - - @Test - public void shouldHandleException_should_return_not_found_error_when_passed_NoSuchRequestHandlingMethodException() { - // given - NoSuchRequestHandlingMethodException ex = new NoSuchRequestHandlingMethodException(); + NoHandlerFoundException ex = new NoHandlerFoundException("GET", "/foo", new HttpHeaders()); // when ApiExceptionHandlerListenerResult result = listener.shouldHandleException(ex); @@ -466,8 +453,7 @@ public static List> unauthorized401ExceptionsDataProvider() { new DisabledException("foo"), new CredentialsExpiredException("foo"), new AccountExpiredException("foo"), - new UsernameNotFoundException("foo"), - new RemoteAuthenticationException("foo") + new UsernameNotFoundException("foo") ).map(Collections::singletonList) .collect(Collectors.toList()); } diff --git a/backstopper-spring-web/src/test/java/org/springframework/web/context/request/async/AsyncRequestTimeoutException.java b/backstopper-spring-web/src/test/java/org/springframework/web/context/request/async/AsyncRequestTimeoutException.java deleted file mode 100644 index 6c57aaa..0000000 --- a/backstopper-spring-web/src/test/java/org/springframework/web/context/request/async/AsyncRequestTimeoutException.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.springframework.web.context.request.async; - -/** - * A copy of Spring's {@code AsyncRequestTimeoutException} from the same package. Used during testing to trigger - * code branches that require an exception with this fully qualified classname. - * - * @author Nic Munroe - */ -public class AsyncRequestTimeoutException extends RuntimeException { - -} diff --git a/backstopper-spring-web/src/test/java/org/springframework/web/servlet/NoHandlerFoundException.java b/backstopper-spring-web/src/test/java/org/springframework/web/servlet/NoHandlerFoundException.java deleted file mode 100644 index 26a10ec..0000000 --- a/backstopper-spring-web/src/test/java/org/springframework/web/servlet/NoHandlerFoundException.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.springframework.web.servlet; - -/** - * A placeholder for Spring's {@code NoHandlerFoundException} from the same package. Used during testing - * to trigger code branches that require an exception with this fully qualified classname. - * - * @author Nic Munroe - */ -public class NoHandlerFoundException extends RuntimeException { -} diff --git a/backstopper-spring-web/src/test/java/org/springframework/web/servlet/mvc/multiaction/NoSuchRequestHandlingMethodException.java b/backstopper-spring-web/src/test/java/org/springframework/web/servlet/mvc/multiaction/NoSuchRequestHandlingMethodException.java deleted file mode 100644 index 7dda9e3..0000000 --- a/backstopper-spring-web/src/test/java/org/springframework/web/servlet/mvc/multiaction/NoSuchRequestHandlingMethodException.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.springframework.web.servlet.mvc.multiaction; - -/** - * A placeholder for Spring's {@code NoSuchRequestHandlingMethodException} from the same package. Used during testing - * to trigger code branches that require an exception with this fully qualified classname. - * - * @author Nic Munroe - */ -public class NoSuchRequestHandlingMethodException extends RuntimeException { -} diff --git a/build.gradle b/build.gradle index 257a33a..381fc25 100644 --- a/build.gradle +++ b/build.gradle @@ -86,8 +86,8 @@ ext { jakartaInjectVersion = '2.0.1' jakartaValidationVersion = '3.0.2' servletApiVersion = '6.0.0' // Compatible with Jakarta EE 10 - spring4Version = '4.3.2.RELEASE' - spring5Version = '5.1.8.RELEASE' + spring6Version = '6.0.23' // Compatible with Jakarta EE 9/10 + springSecurityVersion = '6.1.9' // Closest spring secrity version to our pinned spring6Version, but without going over to avoid versions being bumped above what we want for testing. springboot1Version = '1.5.2.RELEASE' springboot2Version = '2.6.3' jersey1Version = '1.19.2' diff --git a/settings.gradle b/settings.gradle index 4edb20d..d3d94a6 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,8 +6,8 @@ include "nike-internal-util", "backstopper-custom-validators", "backstopper-reusable-tests-junit5", "backstopper-jackson", - "backstopper-servlet-api" -// "backstopper-spring-web", + "backstopper-servlet-api", + "backstopper-spring-web" // "backstopper-spring-web-mvc", // "backstopper-spring-web-flux", // "backstopper-spring-boot1", From 12552db393efc265cd995695b67a36191f422962 Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Mon, 9 Sep 2024 17:43:45 -0700 Subject: [PATCH 09/42] Remove the junit4 based backstopper-reusable-tests library --- backstopper-reusable-tests/README.md | 18 - backstopper-reusable-tests/build.gradle | 36 - ...ctionBasedJsr303AnnotationTrollerBase.java | 703 ------------------ ...otationsAreJacksonCaseInsensitiveTest.java | 115 --- ...alidationMessagesPointToApiErrorsTest.java | 141 ---- .../ProjectApiErrorsTestBase.java | 416 ----------- .../BarebonesCoreApiErrorForTesting.java | 87 --- .../testutil/ProjectApiErrorsForTesting.java | 122 --- ...nBasedJsr303AnnotationTrollerBaseTest.java | 187 ----- .../ReflectionMagicWorksTest.java | 273 ------- ...ionsAreJacksonCaseInsensitiveTestTest.java | 128 ---- ...ationMessagesPointToApiErrorsTestTest.java | 89 --- .../ProjectApiErrorsTestBaseTest.java | 226 ------ 13 files changed, 2541 deletions(-) delete mode 100644 backstopper-reusable-tests/README.md delete mode 100644 backstopper-reusable-tests/build.gradle delete mode 100644 backstopper-reusable-tests/src/main/java/com/nike/backstopper/apierror/contract/jsr303convention/ReflectionBasedJsr303AnnotationTrollerBase.java delete mode 100644 backstopper-reusable-tests/src/main/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyEnumsReferencedByStringConvertsToClassTypeJsr303AnnotationsAreJacksonCaseInsensitiveTest.java delete mode 100644 backstopper-reusable-tests/src/main/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyJsr303ValidationMessagesPointToApiErrorsTest.java delete mode 100644 backstopper-reusable-tests/src/main/java/com/nike/backstopper/apierror/projectspecificinfo/ProjectApiErrorsTestBase.java delete mode 100644 backstopper-reusable-tests/src/main/java/com/nike/backstopper/apierror/testutil/BarebonesCoreApiErrorForTesting.java delete mode 100644 backstopper-reusable-tests/src/main/java/com/nike/backstopper/apierror/testutil/ProjectApiErrorsForTesting.java delete mode 100644 backstopper-reusable-tests/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/ReflectionBasedJsr303AnnotationTrollerBaseTest.java delete mode 100644 backstopper-reusable-tests/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/ReflectionMagicWorksTest.java delete mode 100644 backstopper-reusable-tests/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyEnumsReferencedByStringConvertsToClassTypeJsr303AnnotationsAreJacksonCaseInsensitiveTestTest.java delete mode 100644 backstopper-reusable-tests/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyJsr303ValidationMessagesPointToApiErrorsTestTest.java delete mode 100644 backstopper-reusable-tests/src/test/java/com/nike/backstopper/apierror/projectspecificinfo/ProjectApiErrorsTestBaseTest.java diff --git a/backstopper-reusable-tests/README.md b/backstopper-reusable-tests/README.md deleted file mode 100644 index e0448f3..0000000 --- a/backstopper-reusable-tests/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# Backstopper - reusable-tests - -**DEPRECATED - This module is based on JUnit 4, and should be considered deprecated. For the JUnit 5 version of this -module see [here](../backstopper-reusable-tests-junit5).** - -Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 7 and greater. - -This library contains some reusable unit test classes that should be integrated into every Backstopper-enabled project to guarantee that the conventions and rules that Backstopper requires are followed. This is a fairly easy process and is described in detail in the Backstopper User Guide in the [Reusable Unit Tests for Enforcing Backstopper Rules and Conventions](../USER_GUIDE.md#reusable_tests) section. - -Beyond that, the classes in this reusable-tests library are heavily documented with extensive javadocs, and the [sample applications](../README.md#samples) show concrete usage of these tests to enforce the rules and conventions. Please explore the source code and samples to learn more. - -## More Info - -See the [base project README.md](../README.md), [User Guide](../USER_GUIDE.md), and Backstopper repository source code and javadocs for all further information. - -## License - -Backstopper is released under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/backstopper-reusable-tests/build.gradle b/backstopper-reusable-tests/build.gradle deleted file mode 100644 index 42061ae..0000000 --- a/backstopper-reusable-tests/build.gradle +++ /dev/null @@ -1,36 +0,0 @@ -evaluationDependsOn(':') - -compileTestJava { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - -ext { - // This module should use AssertJ 2.x by default so it's compatible with projects that are still on Java 7. - assertJ2Version = '2.9.1' - // Get mockito up as high as possible for transitive dependency export purposes (we can't go higher due to - // Java 7 support). - mockito2Version = '2.28.2' -} - -dependencies { - api( - project(":backstopper-core"), - project(":backstopper-custom-validators"), - "com.fasterxml.jackson.core:jackson-core:$jacksonVersion", - "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion", - "org.slf4j:slf4j-api:$slf4jVersion", - "javax.inject:javax.inject:$javaxInjectVersion", - "javax.validation:validation-api:$javaxValidationVersion", - "junit:junit:$junitVersion", - "org.mockito:mockito-core:$mockito2Version", - "org.assertj:assertj-core:$assertJ2Version", - "com.tngtech.java:junit-dataprovider:$junitDataproviderVersion", - "org.hamcrest:hamcrest-all:$hamcrestVersion", - "org.reflections:reflections:$orgReflectionsVersion", - "org.javassist:javassist:$javassistVersion" - ) - testImplementation( - "ch.qos.logback:logback-classic:$logbackVersion", - ) -} diff --git a/backstopper-reusable-tests/src/main/java/com/nike/backstopper/apierror/contract/jsr303convention/ReflectionBasedJsr303AnnotationTrollerBase.java b/backstopper-reusable-tests/src/main/java/com/nike/backstopper/apierror/contract/jsr303convention/ReflectionBasedJsr303AnnotationTrollerBase.java deleted file mode 100644 index a3d02eb..0000000 --- a/backstopper-reusable-tests/src/main/java/com/nike/backstopper/apierror/contract/jsr303convention/ReflectionBasedJsr303AnnotationTrollerBase.java +++ /dev/null @@ -1,703 +0,0 @@ -package com.nike.backstopper.apierror.contract.jsr303convention; - -import com.nike.backstopper.validation.constraints.StringConvertsToClassType; -import com.nike.internal.util.Pair; - -import com.google.common.base.Predicate; - -import org.reflections.Reflections; -import org.reflections.scanners.FieldAnnotationsScanner; -import org.reflections.scanners.MethodAnnotationsScanner; -import org.reflections.scanners.MethodParameterScanner; -import org.reflections.scanners.SubTypesScanner; -import org.reflections.scanners.TypeAnnotationsScanner; -import org.reflections.util.ClasspathHelper; -import org.reflections.util.ConfigurationBuilder; - -import java.lang.annotation.Annotation; -import java.lang.reflect.AnnotatedElement; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Member; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; - -import javax.validation.Constraint; - -/** - * Base class for tests that need to troll through the JSR 303 annotations in the project in order to do some checking - * on them. We have this base class because the reflection magic necessary to populate the various data is both - * complicated and time consuming, so we only want to do it once per project and/or test suite. Also provides several - * reusable helper methods that will come in handy for any test class that needs to process those JSR 303 annotations. - *

- * NOTE: For unit tests based on this class you're probably going to be most interested in the - * {@link #projectRelevantConstraintAnnotationsExcludingUnitTestsList} field and the various helper methods. - *

- * QUICK START: - *

    - *
  1. Create an extension of this class and fill in the required abstract methods.
  2. - *
  3. - * If you're following the "JSR 303 messages must correspond to a {@link - * com.nike.backstopper.apierror.ApiError}" convention then create an extension of {@link - * VerifyJsr303ValidationMessagesPointToApiErrorsTest}, fill in the required abstract method, and then make sure - * it is actually getting run by inspecting your unit test report (if it's not then you'll need to manually call - * the verification method as part of a test that gets picked up by your system). - *
  4. - *
  5. - * If you're using the {@link StringConvertsToClassType} JSR 303 annotation then create an extension of {@link - * VerifyEnumsReferencedByStringConvertsToClassTypeJsr303AnnotationsAreJacksonCaseInsensitiveTest}, fill in the - * required abstract method, and then make sure it is actually getting run by inspecting your unit test report - * (if it's not then you'll need to manually call the verification method as part of a test that gets picked up - * by your system). - *
  6. - *
  7. - * Add any other tests you need that require JSR 303 annotation trolling (if any). Use the logic in {@link - * VerifyJsr303ValidationMessagesPointToApiErrorsTest} and {@link - * VerifyEnumsReferencedByStringConvertsToClassTypeJsr303AnnotationsAreJacksonCaseInsensitiveTest} as examples - * of how to use this base class to perform useful tests. - *
  8. - *
- * See the USAGE section below for more information on how to implement these items (including example classes). - *

- * USAGE: - *

- * - * @deprecated This is the JUnit 4 version and will not be maintained long term. Please migrate to the JUnit 5 module: backstopper-reusable-tests-junit5 - * @author Nic Munroe - */ -@Deprecated -@SuppressWarnings("WeakerAccess") -public abstract class ReflectionBasedJsr303AnnotationTrollerBase { - - /** - * The default set of packages to include when {@link #getDefaultPackagesToSearchForConstraintAnnotations()} is - * called. - */ - private Set DEFAULT_CONSTRAINT_SEARCH_PACKAGES = new LinkedHashSet<>(Arrays.asList( - "com.nike", - "org.hibernate.validator.constraints", - "javax.validation.constraints" - )); - - /** - * Utility for trolling through the project looking for declarations that match specific requirements - this will be - * configured to pick up all JSR 303 annotation classes and declarations in the project that we care about (i.e. - * classes that we use and the declarations in *our* code). - */ - @SuppressWarnings("FieldCanBeLocal") - private final Reflections reflections; - /** - * The list of annotation classes used in our project. NOTE: This includes annotations on test classes! - */ - public final List> constraintAnnotationClasses; - /** - * The FULL list of "annotation type to annotation declaration" pairs in our project, INCLUDING annotations placed - * on unit test classes. Each annotation declaration in our project will have one of these pairs. NOTE: This - * INCLUDES annotations on test classes! If you just want the annotations relevant to the project (excluding unit - * tests) you should use {@link #projectRelevantConstraintAnnotationsExcludingUnitTestsList} instead. - */ - public final List> allConstraintAnnotationsMasterList; - /** - * The list of "annotation type to annotation declaration" pairs in our project, EXCLUDING irrelevant unit test - * annotation declarations. Each annotation declaration in our project will have one of these pairs. NOTE: This - * EXCLUDES annotations on test classes! If you want ALL the annotations, including annotations placed on unit test - * classes, you should use {@link #allConstraintAnnotationsMasterList} instead. - */ - public final List> projectRelevantConstraintAnnotationsExcludingUnitTestsList; - - /** - * Classes added to this list via {@link #ignoreAllAnnotationsAssociatedWithTheseProjectClasses()} will be excluded - * from the {@link #projectRelevantConstraintAnnotationsExcludingUnitTestsList} list. *All* annotations on this - * class or its fields/methods/constructors/etc will be ignored. - *

- * If you have a single, *specific* annotation declaration that you want ignored you should use {@link - * #specificAnnotationDeclarationsExcludedFromStrictMessageRequirement} instead. - */ - private final List> ignoreAllAnnotationsAssociatedWithTheseClasses; - /** - * Each item in this list represents a single, *specific* annotation declaration that should be excluded from the - * {@link #projectRelevantConstraintAnnotationsExcludingUnitTestsList} list. If a predicate in this list returns - * true then that specific annotation declaration (represented by the pair of the annotation declaration and the - * element it is annotated on) will be excluded. - *

- * There is a helper method at {@link #generateExclusionForAnnotatedElementAndAnnotationClass(java.lang.reflect.AnnotatedElement, - * Class)} that should build Predicates to cover most cases you might care about, however if you need more specific - * logic you can build your own Predicates. - *

- * If you need a blanket "ignore all annotations on this class" type of exclusion, use {@link - * #ignoreAllAnnotationsAssociatedWithTheseClasses} instead. - */ - private final List>> - specificAnnotationDeclarationsExcludedFromStrictMessageRequirement; - - /** - * The list returned by this method is used to help populate {@link #ignoreAllAnnotationsAssociatedWithTheseClasses} - * (see that field's javadocs for more information). Concrete extensions of this class should implement this to - * include any classes where they want *all* JSR 303 annotations in the class excluded from the "JSR 303 annotation - * messages must point to ApiErrors" requirement. Note that if you have a class with several inner classes defined - * and you want everything ignored (common for unit test classes) then you can get all those inner classes at once - * with the convenient {@link Class#getDeclaredClasses()} method (e.g. {@code SomeWidgetTest.class.getDeclaredClasses()}). - * You can safely return null for this method and it will be treated the same as an empty list. - */ - protected abstract List> ignoreAllAnnotationsAssociatedWithTheseProjectClasses(); - - /** - * The list returned by this method is used to help populate {@link #specificAnnotationDeclarationsExcludedFromStrictMessageRequirement}. - * Concrete extensions of this class should implement this to include any specific annotation declarations they want - * excluded from the "JSR 303 annotation messages must point to ApiErrors" requirement. See {@link - * #specificAnnotationDeclarationsExcludedFromStrictMessageRequirement} for more details, and make your life easier - * by using {@link #generateExclusionForAnnotatedElementAndAnnotationClass(java.lang.reflect.AnnotatedElement, - * Class)}. For example, to exclude a {@code @NotNull} annotation on {@code SomeWidget.someField} you would call - * {@code generateExclusionForAnnotatedElementAndAnnotationClass(SomeWidget.class.getDeclaredField("someField"), - * NotNull.class)} and add that to the returned list. You can safely return null for this method and it will be - * treated the same as an empty list. - * - * @see #specificAnnotationDeclarationsExcludedFromStrictMessageRequirement - * @see #generateExclusionForAnnotatedElementAndAnnotationClass(java.lang.reflect.AnnotatedElement, Class) - */ - protected abstract List>> specificAnnotationDeclarationExclusionsForProject() - throws Exception; - - /** - * Helper constructor that calls {@link #ReflectionBasedJsr303AnnotationTrollerBase(Set)} passing in null in order - * to use only the default packages when searching for constraint annotations. - */ - public ReflectionBasedJsr303AnnotationTrollerBase() { - //noinspection unchecked - this((Set) null); - } - - /** - * Helper constructor that calls {@link #ReflectionBasedJsr303AnnotationTrollerBase(Set)} passing in the given - * varargs as a set for the extra packages to use when searching for constraint annotations. - */ - @SuppressWarnings("unused") - public ReflectionBasedJsr303AnnotationTrollerBase(String... extraPackagesForConstraintAnnotationSearch) { - this(new LinkedHashSet<>(Arrays.asList(extraPackagesForConstraintAnnotationSearch))); - } - - /** - * Initializes the instance based on what is returned by {@link #ignoreAllAnnotationsAssociatedWithTheseProjectClasses()} - * and {@link #specificAnnotationDeclarationExclusionsForProject()}. This is time consuming and should only be done - * once per project if possible - see the usage info in the {@link ReflectionBasedJsr303AnnotationTrollerBase} - * class-level javadocs. - * - *

The given set of extra packages for constraint annotation searching will be passed into {@link - * #getFinalPackagesToSearchForConstraintAnnotations(Set)} to generate the final set of packages that are searched. - * If you don't want the {@link #DEFAULT_CONSTRAINT_SEARCH_PACKAGES} default packages to be searched you can - * override {@link #getDefaultPackagesToSearchForConstraintAnnotations()}. - */ - public ReflectionBasedJsr303AnnotationTrollerBase(Set extraPackagesForConstraintAnnotationSearch) { - - /* - * Set up the {@link #ignoreAllAnnotationsAssociatedWithTheseClasses} and - * {@link #specificAnnotationDeclarationsExcludedFromStrictMessageRequirement} fields so we know which - * annotations are project-relevant vs. unit-test-only. - */ - ignoreAllAnnotationsAssociatedWithTheseClasses = - new ArrayList<>(setupIgnoreAllAnnotationsAssociatedWithTheseClasses()); - specificAnnotationDeclarationsExcludedFromStrictMessageRequirement = - new ArrayList<>(setupSpecificAnnotationDeclarationExclusions()); - - /* - * Set up the {@link #reflections}, {@link #constraintAnnotationClasses}, and - * {@link #allConstraintAnnotationsMasterList} fields. This is where the crazy reflection magic happens to troll - * the project for the JSR 303 annotation declarations. - */ - // Create the ConfigurationBuilder to search the relevant set of packages. - ConfigurationBuilder configurationBuilder = new ConfigurationBuilder(); - for (String packageToAdd : getFinalPackagesToSearchForConstraintAnnotations( - extraPackagesForConstraintAnnotationSearch)) { - configurationBuilder.addUrls(ClasspathHelper.forPackage(packageToAdd)); - } - - // Create the Reflections object so it scans for all validation annotations we care about and all project - // classes that might have annotations on them. - reflections = new Reflections(configurationBuilder.setScanners( - new SubTypesScanner(), new MethodParameterScanner(), new TypeAnnotationsScanner(), - new MethodAnnotationsScanner(), new FieldAnnotationsScanner() - )); - - // Gather the list of all JSR 303 validation annotations in the project. Per the JSR 303 spec this is any - // annotation class type that is marked with @Constraint. - constraintAnnotationClasses = new ArrayList<>(); - for (Class constraintAnnotatedType : reflections.getTypesAnnotatedWith(Constraint.class, true)) { - if (constraintAnnotatedType.isAnnotation()) { - //noinspection unchecked - constraintAnnotationClasses.add((Class) constraintAnnotatedType); - } - } - - // We're not done gathering validation annotations though, unfortunately. JSR 303 also says that *any* - // annotation (whether it is a Constraint or not) that has a value field that returns an array of actual - // Constraints is treated as a "multi-value constraint", and the validation processor will run each - // of the Constraints in the array as if they were declared separately on the annotated element. Therefore, - // we have to dig through all the annotations in the project, find any that fall into this "multi-value - // constraint" category, and include them in our calculations. - for (Class annotationClass : reflections.getSubTypesOf(Annotation.class)) { - if (isMultiValueConstraintClass(annotationClass)) - constraintAnnotationClasses.add(annotationClass); - } - - // Setup the master constraint list - allConstraintAnnotationsMasterList = - new ArrayList<>(setupAllConstraintAnnotationsMasterList(reflections, constraintAnnotationClasses)); - - /* - * Finally use the info we've gathered/constructed previously to populate the - * {@link #projectRelevantConstraintAnnotationsExcludingUnitTestsList} field, which is the main chunk of data - * that extensions of this class will care about. - */ - projectRelevantConstraintAnnotationsExcludingUnitTestsList = Collections.unmodifiableList( - getSubAnnotationListUsingExclusionFilters(allConstraintAnnotationsMasterList, - ignoreAllAnnotationsAssociatedWithTheseClasses, - specificAnnotationDeclarationsExcludedFromStrictMessageRequirement)); - } - - /** - * @param extraPackagesForConstraintAnnotationSearch Extra project-specific packages that should be included in the - * constraint annotation searching. - * @return The final set of packages that should be used when doing constraint annotation searching. The given - * {@code extraPackagesForConstraintAnnotationSearch} will be added to {@link #getDefaultPackagesToSearchForConstraintAnnotations()} - * and returned. If you want a different set of default packages then you should override that method. - */ - protected Set getFinalPackagesToSearchForConstraintAnnotations( - Set extraPackagesForConstraintAnnotationSearch) { - Set finalPackages = new LinkedHashSet<>(getDefaultPackagesToSearchForConstraintAnnotations()); - if (extraPackagesForConstraintAnnotationSearch != null) - finalPackages.addAll(extraPackagesForConstraintAnnotationSearch); - return finalPackages; - } - - /** - * @return {@link #DEFAULT_CONSTRAINT_SEARCH_PACKAGES}. If you need different behavior then override this method. - */ - protected Set getDefaultPackagesToSearchForConstraintAnnotations() { - return DEFAULT_CONSTRAINT_SEARCH_PACKAGES; - } - - /** - * @return {@link #ignoreAllAnnotationsAssociatedWithTheseProjectClasses()}, or an empty list if it is null. - */ - private List> setupIgnoreAllAnnotationsAssociatedWithTheseClasses() { - List> ignoreList = ignoreAllAnnotationsAssociatedWithTheseProjectClasses(); - if (ignoreList == null) - ignoreList = new ArrayList<>(); - - return ignoreList; - } - - /** - * @return {@link #specificAnnotationDeclarationExclusionsForProject()}, or an empty list if it is null. - */ - private List>> setupSpecificAnnotationDeclarationExclusions() { - List>> specificDeclarationExclusionsList; - - try { - specificDeclarationExclusionsList = specificAnnotationDeclarationExclusionsForProject(); - if (specificDeclarationExclusionsList == null) - specificDeclarationExclusionsList = new ArrayList<>(); - } - catch (Exception e) { - throw new RuntimeException(e); - } - - return specificDeclarationExclusionsList; - } - - /** - * @return The master list of constraint annotations appropriate for populating {@link - * #allConstraintAnnotationsMasterList} - see that field's javadocs for more info. - */ - private List> setupAllConstraintAnnotationsMasterList( - Reflections reflectionsArg, List> constraintAnnotationClassesArg - ) { - List> masterList = new ArrayList<>(); - for (Class constraintAnnotationClass : constraintAnnotationClassesArg) { - // We will need to treat multi-value and single Constraints differently - boolean isMultiValueConstraint = isMultiValueConstraintClass(constraintAnnotationClass); - - // Grab the easy-to-handle elements annotated with this class. - List elementsAnnotatedWithThisClass = new ArrayList<>(); - elementsAnnotatedWithThisClass - .addAll(reflectionsArg.getConstructorsAnnotatedWith(constraintAnnotationClass)); - elementsAnnotatedWithThisClass.addAll(reflectionsArg.getMethodsAnnotatedWith(constraintAnnotationClass)); - elementsAnnotatedWithThisClass.addAll(reflectionsArg.getFieldsAnnotatedWith(constraintAnnotationClass)); - - // Register the easy-to-handle element annotations into our master list. - for (AnnotatedElement annotatedElement : elementsAnnotatedWithThisClass) { - List annotationsToRegister = explodeAnnotationToManyConstraintsIfMultiValue( - annotatedElement.getAnnotation(constraintAnnotationClass), isMultiValueConstraint); - for (Annotation annotationToRegister : annotationsToRegister) { - masterList.add(Pair.of(annotationToRegister, annotatedElement)); - } - } - - // Grab the class types annotated with this class. - List> typesAnnotatedWithThisClass = new ArrayList<>(); - typesAnnotatedWithThisClass.addAll(reflectionsArg.getTypesAnnotatedWith(constraintAnnotationClass)); - - // Register the class type annotations into our master list. - for (Class annotatedClassType : typesAnnotatedWithThisClass) { - // We don't want to include annotations on this class type if it is itself an annotation class since - // that is how validation annotations do "inheritance", "composition", or "is-a" marking. - // e.g. @NotBlank is itself annotated with @NotNull to indicate that it is an extension of - // NotNull's logic, and it shouldn't be part of the strict message checking. - if (!annotatedClassType.isAnnotation()) { - List annotationsToRegister = explodeAnnotationToManyConstraintsIfMultiValue( - annotatedClassType.getAnnotation(constraintAnnotationClass), isMultiValueConstraint); - for (Annotation annotationToRegister : annotationsToRegister) { - masterList.add(Pair.of(annotationToRegister, annotatedClassType)); - } - } - } - - // Grab the method params annotated with this class. - List methodParamsAnnotatedWithThisClass = new ArrayList<>(); - methodParamsAnnotatedWithThisClass - .addAll(reflectionsArg.getMethodsWithAnyParamAnnotated(constraintAnnotationClass)); - - // Register the method param annotations into our master list. - for (Method methodWithAnnotatedParam : methodParamsAnnotatedWithThisClass) { - Annotation[][] paramAnnotations = methodWithAnnotatedParam.getParameterAnnotations(); - masterList.addAll( - extractAnnotationsFrom2dArray(paramAnnotations, constraintAnnotationClass, isMultiValueConstraint, - methodWithAnnotatedParam)); - } - - // Grab the constructor params annotated with this class. - List constructorParamsAnnotatedWithThisClass = new ArrayList<>(); - constructorParamsAnnotatedWithThisClass - .addAll(reflectionsArg.getConstructorsWithAnyParamAnnotated(constraintAnnotationClass)); - - // Register the constructor param annotations into our master list. - for (Constructor constructorWithAnnotatedParam : constructorParamsAnnotatedWithThisClass) { - Annotation[][] paramAnnotations = constructorWithAnnotatedParam.getParameterAnnotations(); - masterList.addAll( - extractAnnotationsFrom2dArray(paramAnnotations, constraintAnnotationClass, isMultiValueConstraint, - constructorWithAnnotatedParam)); - } - } - - return masterList; - } - - /** - * @return true if this is a multi-value constraint class as per JSR 303 requirements (contains a value() method - * which returns an array of Constraints), false otherwise. - */ - private static boolean isMultiValueConstraintClass(Class annotationClass) { - // It must have a value() method. - Method valueMethod; - try { - valueMethod = annotationClass.getDeclaredMethod("value"); - } - catch (NoSuchMethodException e) { - return false; - } - - // That value field must return a type of "array of Constraint" - //noinspection RedundantIfStatement - if (valueMethod.getReturnType().isArray() - && valueMethod.getReturnType().getComponentType().getAnnotation(Constraint.class) != null) { - return true; - } - - return false; - } - - - /** - * @return The list of Constraint annotations retrieved from annotation.value() if it is a multi-value constraint - * annotation, or a singleton list containing only the given annotation if it is not a multi-value constraint. - */ - private static List explodeAnnotationToManyConstraintsIfMultiValue(Annotation annotation, - boolean isMultiValueConstraint) { - if (!isMultiValueConstraint) - return Collections.singletonList(annotation); - - try { - Method valueMethod = annotation.getClass().getMethod("value"); - Object[] subAnnotations = (Object[]) valueMethod.invoke(annotation); - // We know that each object in the array is a Constraint, so we can safely cast it to an annotation. - List returnList = new ArrayList<>(); - for (Object subAnnotation : subAnnotations) { - returnList.add((Annotation) subAnnotation); - } - return returnList; - } - catch (NoSuchMethodException e) { - throw new IllegalStateException("Expected multi-value constraint annotation to have a 'value' method.", e); - } - catch (InvocationTargetException | IllegalAccessException e) { - throw new RuntimeException(e); - } - } - - /** - * @return The list of annotation->owningElement pairs from the given 2-dimensional array that match the given - * desiredAnnotationClass - note that if desiredAnnotationClassIsMultiValueConstraint is true then each matching - * annotation will be exploded via {@link #explodeAnnotationToManyConstraintsIfMultiValue(java.lang.annotation.Annotation, - * boolean)} before being added to the return list. - */ - private static List> extractAnnotationsFrom2dArray( - Annotation[][] annotations2dArray, Class desiredAnnotationClass, - boolean desiredAnnotationClassIsMultiValueConstraint, AnnotatedElement owningElement) { - List> returnList = new ArrayList<>(); - for (Annotation[] innerArray : annotations2dArray) { - for (Annotation annotation : innerArray) { - if (annotation.annotationType().equals(desiredAnnotationClass)) { - List annotationsToRegister = explodeAnnotationToManyConstraintsIfMultiValue( - annotation, desiredAnnotationClassIsMultiValueConstraint - ); - for (Annotation annotationToRegister : annotationsToRegister) { - returnList.add(Pair.of(annotationToRegister, owningElement)); - } - } - } - } - - return returnList; - } - - // ==================== REUSABLE HELPER METHODS FOR SUBCLASSES ========================= - - /** - * @return A Predicate that will exclude the given annotation declaration (represented by annotated element and - * annotation class), ready for insertion into {@link #specificAnnotationDeclarationsExcludedFromStrictMessageRequirement} - */ - public static Predicate> generateExclusionForAnnotatedElementAndAnnotationClass( - final AnnotatedElement annotatedElement, final Class annotationClass) { - return new Predicate>() { - @Override - public boolean apply(Pair input) { - //noinspection RedundantIfStatement - if (annotatedElement.equals(input.getRight()) && annotationClass - .equals(input.getLeft().annotationType())) - return true; - - return false; - } - }; - } - - /** - * @return Helper method to extract the annotation.message() string. - */ - public static String extractMessageFromAnnotation(Annotation annotation) { - try { - Method messageMethod = annotation.annotationType().getDeclaredMethod("message"); - return (String) messageMethod.invoke(annotation); - } - catch (Exception e) { - throw new RuntimeException(e); - } - } - - /** - * @return Helper method that extracts the "owner class" from the given AnnotatedElement. In the context of this - * unit test, this method expects annotatedElement to be either a {@link java.lang.reflect.Member} (in which case - * the owning class is {@link java.lang.reflect.Member#getDeclaringClass()}), or annotatedElement should be a Class - * (in which case there is no owning class since the annotation is *on* the class, and this method just returns the - * annotatedElement cast to a Class). - */ - public static Class getOwnerClass(AnnotatedElement annotatedElement) { - if (annotatedElement instanceof Member) - return ((Member) annotatedElement).getDeclaringClass(); - else if (annotatedElement instanceof Class) - return (Class) annotatedElement; - - throw new IllegalArgumentException( - "Expected annotatedElement to be of type Member or Class, but instead received: " + annotatedElement - .getClass().getName()); - } - - /** - * @return Helper method that returns the given listToFilter after it has been filtered down based on the given - * keepTheseItemsFilter predicate. - */ - public static List> getSubAnnotationList( - List> listToFilter, - Predicate> keepTheseItemsFilter) { - List> returnList = new ArrayList<>(); - for (Pair pair : listToFilter) { - if (keepTheseItemsFilter.apply(pair)) - returnList.add(pair); - } - - return returnList; - } - - /** - * @return Helper method that filters the listToFilter down to only the items where {@link - * #getOwnerClass(java.lang.reflect.AnnotatedElement)} matches the given ownerClass. - */ - public static List> getSubAnnotationListForElementsOfOwnerClass( - List> listToFilter, final Class ownerClass) { - return getSubAnnotationList(listToFilter, new Predicate>() { - public boolean apply(Pair input) { - return getOwnerClass(input.getRight()).equals(ownerClass); - } - }); - } - - /** - * @return Helper method that filters the listToFilter down to only the items where the annotation.annotationType() - * matches the given desiredAnnotationClass. - */ - public static List> getSubAnnotationListForAnnotationsOfClassType( - List> listToFilter, - final Class desiredAnnotationClass) { - return getSubAnnotationList(listToFilter, new Predicate>() { - public boolean apply(Pair input) { - return input.getLeft().annotationType().equals(desiredAnnotationClass); - } - }); - } - - /** - * @return Helper method that returns the given listToFilter after any pairs have been removed where the pair's - * AnnotatedElement's owning class is in annotatedElementOwnerClassesToExclude OR where the pair matches any matcher - * in specificAnnotationDeclarationExclusionMatchers. - */ - public static List> getSubAnnotationListUsingExclusionFilters( - List> listToFilter, - final List> annotatedElementOwnerClassesToExclude, - final List>> specificAnnotationDeclarationExclusionMatchers) { - return getSubAnnotationList(listToFilter, new Predicate>() { - public boolean apply(Pair input) { - AnnotatedElement annotatedElement = input.getRight(); - - if (annotatedElementOwnerClassesToExclude != null && annotatedElementOwnerClassesToExclude - .contains(getOwnerClass(annotatedElement))) - return false; - - if (specificAnnotationDeclarationExclusionMatchers != null) { - for (Predicate> exclusionMatcher : specificAnnotationDeclarationExclusionMatchers) { - if (exclusionMatcher.apply(input)) - return false; - } - } - - return true; - } - }); - } - - /** - * @return A String identifying where the given AnnotatedElement lives in the project - for example an annotation - * placed on a field would return a string like {@code "com.nike.somepackage.SomeClass.someField[FIELD]"}. This is - * handy for helping a dev track down a specific annotation declaration causing a unit test to fail. - */ - public static String getAnnotatedElementLocationAsString(AnnotatedElement annotatedElement) { - StringBuilder sb = new StringBuilder(); - - sb.append(getOwnerClass(annotatedElement).getName()); - if (annotatedElement instanceof Constructor) - sb.append("[CONSTRUCTOR]"); - else if (annotatedElement instanceof Class) - sb.append("[CLASS]"); - else if (annotatedElement instanceof Method) - sb.append(".").append(((Method) annotatedElement).getName()).append("[METHOD]"); - else if (annotatedElement instanceof Field) - sb.append(".").append(((Field) annotatedElement).getName()).append("[FIELD]"); - else - sb.append(".").append(annotatedElement.toString()).append("[???]"); - - return sb.toString(); - } -} diff --git a/backstopper-reusable-tests/src/main/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyEnumsReferencedByStringConvertsToClassTypeJsr303AnnotationsAreJacksonCaseInsensitiveTest.java b/backstopper-reusable-tests/src/main/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyEnumsReferencedByStringConvertsToClassTypeJsr303AnnotationsAreJacksonCaseInsensitiveTest.java deleted file mode 100644 index e0c09c5..0000000 --- a/backstopper-reusable-tests/src/main/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyEnumsReferencedByStringConvertsToClassTypeJsr303AnnotationsAreJacksonCaseInsensitiveTest.java +++ /dev/null @@ -1,115 +0,0 @@ -package com.nike.backstopper.apierror.contract.jsr303convention; - -import com.nike.backstopper.validation.constraints.StringConvertsToClassType; -import com.nike.internal.util.Pair; - -import com.fasterxml.jackson.databind.ObjectMapper; - -import org.junit.Test; - -import java.io.IOException; -import java.lang.annotation.Annotation; -import java.lang.reflect.AnnotatedElement; -import java.util.List; - -import static org.hamcrest.CoreMatchers.not; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.MatcherAssert.assertThat; - -/** - * Makes sure that any Enums referenced by {@link StringConvertsToClassType} JSR 303 annotations are case insensitive - * if {@link StringConvertsToClassType#allowCaseInsensitiveEnumMatch()} is true when being deserialized - * (e.g. by Jackson). - * - *

This test is only necessary if you are using {@link StringConvertsToClassType} annotations for validation, *and* - * you want to support case-insensitive enum values during deserialization. - * - *

You can exclude annotation declarations by making sure that the {@link #getAnnotationTroller()} you use has - * populated its {@link ReflectionBasedJsr303AnnotationTrollerBase#ignoreAllAnnotationsAssociatedWithTheseClasses} and - * {@link ReflectionBasedJsr303AnnotationTrollerBase#specificAnnotationDeclarationsExcludedFromStrictMessageRequirement} - * lists appropriately. - * - * @deprecated This is the JUnit 4 version and will not be maintained long term. Please migrate to the JUnit 5 module: backstopper-reusable-tests-junit5 - * @author Nic Munroe - * @see com.nike.backstopper.apierror.contract.jsr303convention.ReflectionBasedJsr303AnnotationTrollerBase - */ -@Deprecated -public abstract class VerifyEnumsReferencedByStringConvertsToClassTypeJsr303AnnotationsAreJacksonCaseInsensitiveTest { - - private ObjectMapper objectMapper = new ObjectMapper(); - - /** - * @return The annotation troller to use for your project. This should likely be accessed as a singleton - see the - * javadocs for {@link ReflectionBasedJsr303AnnotationTrollerBase} for more info on why and example code on how to - * do it. - */ - protected abstract ReflectionBasedJsr303AnnotationTrollerBase getAnnotationTroller(); - - /** - * Makes sure that any enums referenced by {@link StringConvertsToClassType} annotations in your project where - * {@link StringConvertsToClassType#allowCaseInsensitiveEnumMatch()} is true (and that aren't explicitly excluded) - * support case insensitive deserialization when being deserialized by Jackson. See the javadocs for - * {@link StringConvertsToClassType} for more info on why this is required and how to do it. - */ - @Test - public void verifyEnumsReferencedByStringConvertsToClassTypeJsr303AnnotationsAreCaseInsensitive() - throws IOException { - ReflectionBasedJsr303AnnotationTrollerBase troller = getAnnotationTroller(); - List> allStringConvertsToClassTypeAnnotations = - ReflectionBasedJsr303AnnotationTrollerBase.getSubAnnotationListForAnnotationsOfClassType( - troller.projectRelevantConstraintAnnotationsExcludingUnitTestsList, StringConvertsToClassType.class); - - for (Pair annotationPair : allStringConvertsToClassTypeAnnotations) { - StringConvertsToClassType sctctAnnotation = (StringConvertsToClassType) annotationPair.getLeft(); - if (sctctAnnotation.classType().isEnum() && sctctAnnotation.allowCaseInsensitiveEnumMatch()) { - // This field is supposed to be able to deserialize to the desired enum in a case insensitive way. - // Make sure the enum supports case insensitive deserialization. - @SuppressWarnings("unchecked") - Class enumClass = (Class) sctctAnnotation.classType(); - - // Grab all the enum values for the enum referenced by this @StringConvertsToClassType declaration. - Object[] enumValuesArray = sctctAnnotation.classType().getEnumConstants(); - assertThat(enumValuesArray, notNullValue()); - - // For each enum value, verify that we can use Jackson to serialize it, convert it to an alternate case, - // and then deserialize the alternate version successfully. - for (Object enumValue : enumValuesArray) { - String enumAsJsonString = objectMapper.writeValueAsString(enumValue); - String lowercaseEnumAsJsonString = enumAsJsonString.toLowerCase(); - String uppercaseEnumAsJsonString = enumAsJsonString.toUpperCase(); - - // Verify that the lowercase version really is different - if it's the same then do uppercase - // instead. - String alternateCaseEnumAsJsonString = enumAsJsonString.equals(lowercaseEnumAsJsonString) - ? uppercaseEnumAsJsonString - : lowercaseEnumAsJsonString; - // Sanity check that the alternate case really is different than the original enum's name. - assertThat(alternateCaseEnumAsJsonString, not(enumAsJsonString)); - - boolean deserializationSucceeded; - try { - Enum deserializedEnum = objectMapper.readValue(alternateCaseEnumAsJsonString, enumClass); - deserializationSucceeded = deserializedEnum.equals(enumValue); - } - catch (Throwable ex) { - deserializationSucceeded = false; - } - - if (!deserializationSucceeded) { - @SuppressWarnings("StringBufferReplaceableByString") - StringBuilder sb = new StringBuilder(); - sb.append("Found a @").append(StringConvertsToClassType.class.getSimpleName()) - .append(" annotation that references an enum class type that is not case insensitive. ") - .append( "This most likely means you need to add a @JsonCreator method in the enum that knows " - + "how to deserialize the enum in a case insensitive manner. ") - .append("Offending enum class: ").append(enumClass.getName()) - .append(", offending element containing the annotation: ") - .append(ReflectionBasedJsr303AnnotationTrollerBase - .getAnnotatedElementLocationAsString(annotationPair.getRight())); - throw new AssertionError(sb.toString()); - } - } - } - } - } -} diff --git a/backstopper-reusable-tests/src/main/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyJsr303ValidationMessagesPointToApiErrorsTest.java b/backstopper-reusable-tests/src/main/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyJsr303ValidationMessagesPointToApiErrorsTest.java deleted file mode 100644 index f20f7ef..0000000 --- a/backstopper-reusable-tests/src/main/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyJsr303ValidationMessagesPointToApiErrorsTest.java +++ /dev/null @@ -1,141 +0,0 @@ -package com.nike.backstopper.apierror.contract.jsr303convention; - -import com.nike.backstopper.apierror.ApiError; -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.internal.util.Pair; - -import org.junit.Test; - -import java.lang.annotation.Annotation; -import java.lang.reflect.AnnotatedElement; -import java.lang.reflect.InvocationTargetException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.notNullValue; - -/** - * Verifies that *ALL* non-excluded JSR 303 validation annotations in this project have a message defined that maps to a - * {@link ApiError#getName()} for one of the errors found in the project's {@link #getProjectApiErrors()}. - * You can exclude annotation declarations by making sure that the {@link #getAnnotationTroller()} you use has populated - * its {@link ReflectionBasedJsr303AnnotationTrollerBase#ignoreAllAnnotationsAssociatedWithTheseClasses} and {@link - * ReflectionBasedJsr303AnnotationTrollerBase#specificAnnotationDeclarationsExcludedFromStrictMessageRequirement} lists - * appropriately. - * - * @deprecated This is the JUnit 4 version and will not be maintained long term. Please migrate to the JUnit 5 module: backstopper-reusable-tests-junit5 - * @author Nic Munroe - * @see com.nike.backstopper.apierror.contract.jsr303convention.ReflectionBasedJsr303AnnotationTrollerBase - */ -@Deprecated -public abstract class VerifyJsr303ValidationMessagesPointToApiErrorsTest { - - /** - * @return The annotation troller to use for your project. This should likely be accessed as a singleton - see the - * javadocs for {@link ReflectionBasedJsr303AnnotationTrollerBase} for more info on why and example code on how to - * do it. - */ - protected abstract ReflectionBasedJsr303AnnotationTrollerBase getAnnotationTroller(); - - /** - * @return The {@link ProjectApiErrors} for your project. - */ - protected abstract ProjectApiErrors getProjectApiErrors(); - - /** - * Makes sure that any constraint annotation messages that aren't explicitly excluded point to an {@link - * com.nike.backstopper.apierror.ApiError} name from your project's {@link #getProjectApiErrors()}. - */ - @Test - public void verifyThatAllValidationAnnotationsReferToApiErrors() - throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { - final ReflectionBasedJsr303AnnotationTrollerBase troller = getAnnotationTroller(); - List> relevantAnnotations = - troller.projectRelevantConstraintAnnotationsExcludingUnitTestsList; - - List invalidAnnotations = new ArrayList<>(); - for (Pair pair : relevantAnnotations) { - Annotation annotation = pair.getLeft(); - AnnotatedElement annotatedElement = pair.getRight(); - String message = ReflectionBasedJsr303AnnotationTrollerBase.extractMessageFromAnnotation(annotation); - ApiError apiError; - try { - apiError = getProjectApiErrors().convertToApiError(message); - assertThat(apiError, notNullValue()); - } - catch (Throwable ex) { - // This constraint annotation has an invalid message value. Keep track of it for later so we can spit - // all the invalid ones out at once. - invalidAnnotations.add(new InvalidAnnotationDescription(annotation, annotatedElement, message)); - } - } - - if (invalidAnnotations.size() > 0) { - // We have at least one invalid annotation, so this unit test will need to fail. - // Sort our invalid-annotations list to make it easier to fix errors for the developer looking at the error output. - Collections.sort(invalidAnnotations, new Comparator() { - @Override - public int compare(InvalidAnnotationDescription o1, InvalidAnnotationDescription o2) { - int classComparison = ReflectionBasedJsr303AnnotationTrollerBase - .getOwnerClass(o1.annotatedElement) - .getName() - .compareTo(ReflectionBasedJsr303AnnotationTrollerBase.getOwnerClass(o2.annotatedElement).getName()); - - if (classComparison != 0) - return classComparison; - - return ReflectionBasedJsr303AnnotationTrollerBase - .getAnnotatedElementLocationAsString(o1.annotatedElement) - .compareTo( - ReflectionBasedJsr303AnnotationTrollerBase.getAnnotatedElementLocationAsString(o2.annotatedElement) - ); - } - }); - - // Generate a giant error output message containing all the invalid annotations and instructions on how to deal with them. - StringBuilder sb = new StringBuilder(); - sb.append("There are ").append(invalidAnnotations.size()) - .append( " JSR 303 validation annotations that are invalid. All validation annotations MUST contain a " - + "message and that message MUST map to one of the ApiError names contained in ") - .append(getProjectApiErrors().getClass().getName()) - .append(". If any of these are false positive errors then you must add the Class or specific " - + "Member/annotated element that owns the annotation to one of the exclusion lists in this unit test.\n") - .append("You should only exclude annotations, however, if you REALLY REALLY REALLY know what you're doing " - + "and can 100% GUARANTEE that the exclusion won't break your error handling contract, because " - + "this strict message requirement *IS* how we're guaranteeing your error handling contract.\n") - .append("Here are the invalid annotations, where they are found, and the offending message:") - .append("\nANNOTATION CLASS\t|\tLOCATION\t|\tMESSAGE"); - for (InvalidAnnotationDescription invalidAnnotation : invalidAnnotations) { - AnnotatedElement annotatedElement = invalidAnnotation.annotatedElement; - sb.append("\n@").append(invalidAnnotation.annotation.annotationType().getSimpleName()).append("\t|\t"); - - sb.append(ReflectionBasedJsr303AnnotationTrollerBase - .getAnnotatedElementLocationAsString(annotatedElement)); - - sb.append("\t|\t") - .append(invalidAnnotation.message); - } - // Fail the unit test with our custom giant error message. - throw new AssertionError(sb.toString()); - } - } - - /** - * DTO class describing the context of an invalid annotation. - */ - @SuppressWarnings("WeakerAccess") - private static class InvalidAnnotationDescription { - - public final Annotation annotation; - public final AnnotatedElement annotatedElement; - public final String message; - - private InvalidAnnotationDescription(Annotation annotation, AnnotatedElement annotatedElement, String message) { - this.annotation = annotation; - this.annotatedElement = annotatedElement; - this.message = message; - } - } -} diff --git a/backstopper-reusable-tests/src/main/java/com/nike/backstopper/apierror/projectspecificinfo/ProjectApiErrorsTestBase.java b/backstopper-reusable-tests/src/main/java/com/nike/backstopper/apierror/projectspecificinfo/ProjectApiErrorsTestBase.java deleted file mode 100644 index 307dd67..0000000 --- a/backstopper-reusable-tests/src/main/java/com/nike/backstopper/apierror/projectspecificinfo/ProjectApiErrorsTestBase.java +++ /dev/null @@ -1,416 +0,0 @@ -package com.nike.backstopper.apierror.projectspecificinfo; - -import com.nike.backstopper.apierror.ApiError; -import com.nike.backstopper.apierror.ApiErrorBase; -import com.nike.backstopper.apierror.testutil.BarebonesCoreApiErrorForTesting; -import com.nike.backstopper.apierror.testutil.ProjectApiErrorsForTesting; - -import org.junit.Test; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.SortedSet; -import java.util.TreeSet; -import java.util.UUID; - -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; - -/** - * Reusable test that verifies the functionality of your project's {@link ProjectApiErrors} helper methods and objects. - * This is intended to be extended for each project with {@link #getProjectApiErrors()} implemented to return that - * project's implementation. - *

- * NOTE: To take advantage of this prebuilt test class all you have to do is extend it and fill in the abstract - * {@link #getProjectApiErrors()} method to have it return your project's {@link ProjectApiErrors}. As long as - * your unit test runner picks it up and runs the test methods in this base class you should be good to go. - * This is especially important if your project is using TestNG instead of JUnit, for example, as this base - * prebuilt unit test is annotated with JUnit {@code @Test} annotations. You may need to create a new test - * method that is guaranteed to get fired during your unit tests and manually call the parent methods to perform - * the checks. For example: - *

- *              public class MyProjectApiErrorsTest extends ProjectApiErrorsTestBase {
- *
- *                  @org.testng.annotations.Test
- *                  @Override
- *                  public void verifyGetStatusCodePriorityOrderMethodContainsAllRelevantCodes() {
- *                      super.verifyGetStatusCodePriorityOrderMethodContainsAllRelevantCodes();
- *                  }
- *
- *                  @org.testng.annotations.Test
- *                  @Override
- *                  public void determineHighestPriorityHttpStatusCodeShouldReturnNullForEmptyErrorCollection() {
- *                      super.determineHighestPriorityHttpStatusCodeShouldReturnNullForEmptyErrorCollection();
- *                  }
- *
- *                  // ... etc
- *              }
- *       
- *

- * If you're using JUnit then it should be pretty trivial to set this up for your project's - * {@link ProjectApiErrors}. Here's a copy-paste of - * {@code com.nike.backstopper.apierror.SampleProjectApiErrorsBaseTest} (a unit test class living in the - * backstopper-core library's test source area) as a concrete complete real-world example of how simple it is to - * set up for a JUnit environment where the base class' tests get picked up automatically: - *

- *           public class SampleProjectApiErrorsBaseTest extends ProjectApiErrorsTestBase {
- *
- *              private static final ProjectApiErrors testProjectApiErrors = new SampleProjectApiErrorsBase() {
- *                  @Override
- *                  protected List getProjectSpecificApiErrors() {
- *                      return null;
- *                  }
- *
- *                  @Override
- *                  protected ProjectSpecificErrorCodeRange getProjectSpecificErrorCodeRange() {
- *                      return null;
- *                  }
- *              };
- *
- *              @Override
- *              protected ProjectApiErrors getProjectApiErrors() {
- *                  return testProjectApiErrors;
- *              }
- *           }
- *       
- * You would of course return your project's {@link ProjectApiErrors} rather than an anonymous class, but in the - * case of this example we're only testing the base class with its core errors, so this works fine since by - * default {@link ProjectApiErrors} includes all the {@link ProjectApiErrors#getCoreApiErrors()}. - * - * @deprecated This is the JUnit 4 version and will not be maintained long term. Please migrate to the JUnit 5 module: backstopper-reusable-tests-junit5 - * @author Nic Munroe - */ -@Deprecated -@SuppressWarnings("WeakerAccess") -public abstract class ProjectApiErrorsTestBase { - - protected ApiError findRandomApiErrorWithHttpStatusCode(int httpStatusCode) { - for (ApiError error : getProjectApiErrors().getProjectApiErrors()) { - if (error.getHttpStatusCode() == httpStatusCode) - return error; - } - throw new IllegalStateException("Couldn't find ApiError with HTTP status code: " + httpStatusCode); - } - - protected abstract ProjectApiErrors getProjectApiErrors(); - - @Test - public void verifyGetStatusCodePriorityOrderMethodContainsAllRelevantCodes() { - for (ApiError error : getProjectApiErrors().getProjectApiErrors()) { - int relevantCode = error.getHttpStatusCode(); - boolean containsRelevantCode = getProjectApiErrors().getStatusCodePriorityOrder().contains(relevantCode); - if (!containsRelevantCode) { - throw new AssertionError( - "getStatusCodePriorityOrder() did not contain HTTP Status Code: " + relevantCode + " for " - + getProjectApiErrors().getClass().getName() + "'s ApiError: " + error - ); - } - } - } - - @Test - public void determineHighestPriorityHttpStatusCodeShouldReturnNullForNullErrorCollection() { - assertThat(getProjectApiErrors().determineHighestPriorityHttpStatusCode(null), nullValue()); - } - - @Test - public void determineHighestPriorityHttpStatusCodeShouldReturnNullForEmptyErrorCollection() { - assertThat(getProjectApiErrors().determineHighestPriorityHttpStatusCode(Collections.emptyList()), - nullValue()); - } - - @Test - public void determineHighestPriorityHttpStatusCodeShouldReturnTheSameValueRegardlessOfErrorOrder() { - List list = Arrays.asList( - findRandomApiErrorWithHttpStatusCode(getProjectApiErrors().getStatusCodePriorityOrder() - .get(0)), - findRandomApiErrorWithHttpStatusCode(getProjectApiErrors().getStatusCodePriorityOrder() - .get(1))); - - int returnValNormalOrder = getProjectApiErrors().determineHighestPriorityHttpStatusCode(list); - Collections.reverse(list); - - int returnValReverseOrder = getProjectApiErrors().determineHighestPriorityHttpStatusCode(list); - assertThat(returnValNormalOrder, is(returnValReverseOrder)); - } - - @Test - public void determineHighestPriorityHttpStatusCodeShouldReturnTheCorrectValueWithAMixedList() { - List list = new ArrayList<>(Arrays.asList( - findRandomApiErrorWithHttpStatusCode(getProjectApiErrors().getStatusCodePriorityOrder() - .get(2)), - findRandomApiErrorWithHttpStatusCode(getProjectApiErrors().getStatusCodePriorityOrder() - .get(3)))); - - assertThat(getProjectApiErrors().determineHighestPriorityHttpStatusCode(list), - is(getProjectApiErrors().getStatusCodePriorityOrder().get(2))); - - list.add(findRandomApiErrorWithHttpStatusCode(getProjectApiErrors().getStatusCodePriorityOrder().get(1))); - - assertThat(getProjectApiErrors().determineHighestPriorityHttpStatusCode(list), - is(getProjectApiErrors().getStatusCodePriorityOrder().get(1))); - } - - @Test - public void determineHighestPriorityHttpStatusCodeShouldReturnNullIfNoApiErrorsYouPassItHasHttpStatusCodeInPriorityOrderList() { - ApiError mockApiError1 = mock(ApiError.class); - ApiError mockApiError2 = mock(ApiError.class); - doReturn(414141).when(mockApiError1).getHttpStatusCode(); - doReturn(424242).when(mockApiError2).getHttpStatusCode(); - List list = Arrays.asList(mockApiError1, mockApiError2); - - assertThat(getProjectApiErrors().determineHighestPriorityHttpStatusCode(list), nullValue()); - } - - @Test - public void - determineHighestPriorityHttpStatusCodeShouldReturnStatusCodeIfAtLeastOneApiErrorInListYouPassItHasHttpStatusCodeInPriorityOrderList() { - ApiError mockApiError1 = mock(ApiError.class); - ApiError mockApiError2 = mock(ApiError.class); - doReturn(424242).when(mockApiError1).getHttpStatusCode(); - doReturn(400).when(mockApiError2).getHttpStatusCode(); - List list = Arrays.asList(mockApiError1, mockApiError2); - - assertThat(getProjectApiErrors().determineHighestPriorityHttpStatusCode(list), is(400)); - } - - @Test - public void determineHighestPriorityHttpStatusCodeShouldReturnStatusCodeIfOnlyApiError() { - ApiError mockApiError = mock(ApiError.class); - doReturn(400).when(mockApiError).getHttpStatusCode(); - - assertThat(getProjectApiErrors().determineHighestPriorityHttpStatusCode(Collections.singleton(mockApiError)), - is(400)); - } - - @Test - public void - determineHighestPriorityHttpStatusCodeShouldReturnStatusCodeIfOnlyApiErrorEvenIfNotInPriorityOrderList() { - ApiError mockApiError = mock(ApiError.class); - doReturn(424242).when(mockApiError).getHttpStatusCode(); - - assertThat(getProjectApiErrors().determineHighestPriorityHttpStatusCode(Collections.singleton(mockApiError)), - is(424242)); - } - - @Test - public void getSublistContainingOnlyHttpStatusCodeShouldReturnEmptyListForNullErrorCollection() { - assertThat(getProjectApiErrors() - .getSublistContainingOnlyHttpStatusCode(null, getProjectApiErrors().getStatusCodePriorityOrder() - .get(0)).size(), is(0)); - } - - @Test - public void getSublistContainingOnlyHttpStatusCodeShouldReturnEmptyListForNullStatusCode() { - ApiError randomError = getProjectApiErrors().getProjectApiErrors().get(0); - assertThat( - getProjectApiErrors().getSublistContainingOnlyHttpStatusCode(Collections.singletonList(randomError), null) - .size(), is(0)); - } - - @Test - public void getSublistContainingOnlyHttpStatusCodeShouldFilterOutExpectedValues() { - List mixedList = Arrays.asList( - findRandomApiErrorWithHttpStatusCode(getProjectApiErrors().getStatusCodePriorityOrder() - .get(0)), - findRandomApiErrorWithHttpStatusCode(getProjectApiErrors().getStatusCodePriorityOrder() - .get(1))); - - List filteredList = getProjectApiErrors() - .getSublistContainingOnlyHttpStatusCode(mixedList, getProjectApiErrors().getStatusCodePriorityOrder() - .get(1)); - for (ApiError error : filteredList) { - assertThat(error.getHttpStatusCode(), is(getProjectApiErrors().getStatusCodePriorityOrder().get(1))); - } - } - - @Test - public void convertToApiErrorShouldReturnNullIfYouPassItNull() { - assertThat(getProjectApiErrors().convertToApiError(null), nullValue()); - } - - @Test - public void convertToApiErrorShouldReturnExpectedResultIfPassedValidNames() { - for (ApiError apiError : getProjectApiErrors().getProjectApiErrors()) { - assertThat("Did not get back the same instance for ApiError with name: " + apiError.getName() - + ". This is usually because you have duplicate ApiError names - see the output of the " - + "shouldNotContainDuplicateNamedApiErrors() test to be sure. If that's not the case then " - + "you'll probably need to do some breakpoint debugging.", - getProjectApiErrors().convertToApiError(apiError.getName()), is(apiError)); - } - } - - @Test - public void convertToApiErrorShouldReturnNullIfYouPassItGarbage() { - assertThat(getProjectApiErrors().convertToApiError(UUID.randomUUID().toString()), nullValue()); - } - - @Test - public void convertToApiErrorShouldUseFallbackOnNullValue() { - ApiError fallback = BarebonesCoreApiErrorForTesting.GENERIC_SERVICE_ERROR; - assertThat(getProjectApiErrors().convertToApiError(null, fallback), is(fallback)); - } - - @Test - public void convertToApiErrorShouldUseFallbackOnInvalidValue() { - ApiError fallback = BarebonesCoreApiErrorForTesting.GENERIC_SERVICE_ERROR; - assertThat(getProjectApiErrors().convertToApiError("notavaliderror", fallback), is(fallback)); - } - - @Test(expected = IllegalStateException.class) - public void verifyErrorsAreInRangeShouldThrowExceptionIfListIncludesNonCoreApiErrorAndRangeIsNull() { - ProjectApiErrorsForTesting - .withProjectSpecificData(Collections.singletonList(new ApiErrorBase("blah", 99001, "stuff", 400)), - null); - } - - @Test - public void verifyErrorsAreInRangeShouldNotThrowExceptionIfListIncludesCoreApiErrors() { - ProjectApiErrors pae = ProjectApiErrorsForTesting.withProjectSpecificData(null, null); - - assertThat(pae, notNullValue()); - assertThat(pae.getProjectApiErrors().contains(BarebonesCoreApiErrorForTesting.GENERIC_SERVICE_ERROR), is(true)); - } - - @Test - public void verifyErrorsAreInRangeShouldNotThrowExceptionIfListIncludesCoreApiErrorWrapper() { - ApiError coreApiError = BarebonesCoreApiErrorForTesting.GENERIC_SERVICE_ERROR; - final ApiError coreApiErrorWrapper = - new ApiErrorBase("blah", coreApiError.getErrorCode(), coreApiError.getMessage(), - coreApiError.getHttpStatusCode()); - ProjectApiErrors pae = - ProjectApiErrorsForTesting.withProjectSpecificData(Collections.singletonList(coreApiErrorWrapper), null); - - assertThat(pae, notNullValue()); - assertThat(pae.getProjectApiErrors().contains(coreApiErrorWrapper), is(true)); - } - - @Test(expected = IllegalStateException.class) - public void verifyErrorsAreInRangeShouldThrowExceptionIfListIncludesErrorOutOfRange() { - ProjectApiErrorsForTesting.withProjectSpecificData( - Collections.singletonList(new ApiErrorBase("blah", 1, "stuff", 400)), - new ProjectSpecificErrorCodeRange() { - @Override - public boolean isInRange(ApiError error) { - return "42".equals(error.getErrorCode()); - } - - @Override - public String getName() { - return "test error range"; - } - } - ); - } - - @Test - public void shouldNotContainDuplicateNamedApiErrors() { - Map nameToCountMap = new HashMap<>(); - SortedSet duplicateErrorNames = new TreeSet<>(); - for (ApiError apiError : getProjectApiErrors().getProjectApiErrors()) { - Integer currentCount = nameToCountMap.get(apiError.getName()); - if (currentCount == null) - currentCount = 0; - - Integer newCount = currentCount + 1; - nameToCountMap.put(apiError.getName(), newCount); - if (newCount > 1) - duplicateErrorNames.add(apiError.getName()); - } - - if (!duplicateErrorNames.isEmpty()) { - StringBuilder sb = new StringBuilder(); - sb.append( - "There are ApiError instances in the ProjectApiErrors that share duplicate names. [name, count]: "); - boolean first = true; - for (String dup : duplicateErrorNames) { - if (!first) - sb.append(", "); - - sb.append("[").append(dup).append(", ").append(nameToCountMap.get(dup)).append("]"); - - first = false; - } - - throw new AssertionError(sb.toString()); - } - } - - /** - * Override this if the should_not_contain_same_error_codes_for_different_instances_that_are_not_wrappers test is - * failing and you *really* want to allow one or more of your error codes to have duplicate ApiErrors that are - * not wrappers. This should be used with care. - */ - protected Set allowedDuplicateErrorCodes() { - return Collections.emptySet(); - } - - @Test - public void should_not_contain_same_error_codes_for_different_instances_that_are_not_wrappers() { - Set allowedDuplicateErrorCodes = allowedDuplicateErrorCodes(); - Map codeToErrorMap = new HashMap<>(); - for (ApiError apiError : getProjectApiErrors().getProjectApiErrors()) { - ApiError errorWithSameCode = codeToErrorMap.get(apiError.getErrorCode()); - - if (errorWithSameCode != null && !areWrappersOfEachOther(apiError, errorWithSameCode) - && !allowedDuplicateErrorCodes.contains(apiError.getErrorCode())) { - throw new AssertionError( - "There are ApiError instances in the ProjectApiErrors that share duplicate error codes and are not " - + "wrappers of each other. error_code=" + apiError.getErrorCode() + ", conflicting_api_errors=[" - + apiError.getName() + ", " + errorWithSameCode.getName() + "]" - ); - } - - codeToErrorMap.put(apiError.getErrorCode(), apiError); - } - } - - private boolean areWrappersOfEachOther(ApiError error1, ApiError error2) { - boolean errorCodeMatches = Objects.equals(error1.getErrorCode(), error2.getErrorCode()); - boolean messageMatches = Objects.equals(error1.getMessage(), error2.getMessage()); - boolean httpStatusCodeMatches = error1.getHttpStatusCode() == error2.getHttpStatusCode(); - //noinspection RedundantIfStatement - if (errorCodeMatches && messageMatches && httpStatusCodeMatches) { - return true; - } - - return false; - } - - @Test - public void allErrorsShouldBeCoreApiErrorsOrCoreApiErrorWrappersOrFallInProjectSpecificErrorRange() { - ProjectSpecificErrorCodeRange projectSpecificErrorCodeRange = - getProjectApiErrors().getProjectSpecificErrorCodeRange(); - - for (ApiError error : getProjectApiErrors().getProjectApiErrors()) { - boolean valid = false; - if (getProjectApiErrors().getCoreApiErrors().contains(error) || getProjectApiErrors() - .isWrapperAroundCoreError(error, getProjectApiErrors().getCoreApiErrors())) - valid = true; - else if (projectSpecificErrorCodeRange != null && projectSpecificErrorCodeRange.isInRange(error)) - valid = true; - - if (!valid) { - throw new AssertionError( - "Found an ApiError in the ProjectApiErrors that is not a core error or wrapper around a core error, and its error code does not fall in the " - + - "range of getProjectApiErrors().getProjectSpecificErrorCodeRange(). getProjectApiErrors().getProjectSpecificErrorCodeRange(): " - + projectSpecificErrorCodeRange + - ". Offending error info: name=" + error.getName() + ", errorCode=" + error.getErrorCode() - + ", message=\"" + error.getMessage() + "\", httpStatusCode=" + - error.getHttpStatusCode() + ", class=" + error.getClass().getName()); - } - } - } - -} diff --git a/backstopper-reusable-tests/src/main/java/com/nike/backstopper/apierror/testutil/BarebonesCoreApiErrorForTesting.java b/backstopper-reusable-tests/src/main/java/com/nike/backstopper/apierror/testutil/BarebonesCoreApiErrorForTesting.java deleted file mode 100644 index 7f5fa7e..0000000 --- a/backstopper-reusable-tests/src/main/java/com/nike/backstopper/apierror/testutil/BarebonesCoreApiErrorForTesting.java +++ /dev/null @@ -1,87 +0,0 @@ -package com.nike.backstopper.apierror.testutil; - -import com.nike.backstopper.apierror.ApiError; -import com.nike.backstopper.apierror.ApiErrorBase; -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; - -import java.util.Map; -import java.util.UUID; - -import static com.nike.backstopper.apierror.ApiErrorConstants.HTTP_STATUS_CODE_BAD_REQUEST; -import static com.nike.backstopper.apierror.ApiErrorConstants.HTTP_STATUS_CODE_FORBIDDEN; -import static com.nike.backstopper.apierror.ApiErrorConstants.HTTP_STATUS_CODE_INTERNAL_SERVER_ERROR; -import static com.nike.backstopper.apierror.ApiErrorConstants.HTTP_STATUS_CODE_METHOD_NOT_ALLOWED; -import static com.nike.backstopper.apierror.ApiErrorConstants.HTTP_STATUS_CODE_NOT_ACCEPTABLE; -import static com.nike.backstopper.apierror.ApiErrorConstants.HTTP_STATUS_CODE_NOT_FOUND; -import static com.nike.backstopper.apierror.ApiErrorConstants.HTTP_STATUS_CODE_SERVICE_UNAVAILABLE; -import static com.nike.backstopper.apierror.ApiErrorConstants.HTTP_STATUS_CODE_TOO_MANY_REQUESTS; -import static com.nike.backstopper.apierror.ApiErrorConstants.HTTP_STATUS_CODE_UNAUTHORIZED; -import static com.nike.backstopper.apierror.ApiErrorConstants.HTTP_STATUS_CODE_UNSUPPORTED_MEDIA_TYPE; - -/** - * A barebones set of core errors that can be used for testing. See {@link ProjectApiErrorsForTesting} for a {@link - * ProjectApiErrors} impl that uses this and is also intended for testing. - * - * @deprecated This is the JUnit 4 version and will not be maintained long term. Please migrate to the JUnit 5 module: backstopper-reusable-tests-junit5 - * @author Nic Munroe - */ -@Deprecated -public enum BarebonesCoreApiErrorForTesting implements ApiError { - GENERIC_SERVICE_ERROR(10, "An error occurred while fulfilling the request", HTTP_STATUS_CODE_INTERNAL_SERVER_ERROR), - OUTSIDE_DEPENDENCY_RETURNED_AN_UNRECOVERABLE_ERROR(GENERIC_SERVICE_ERROR), - SERVERSIDE_VALIDATION_ERROR(GENERIC_SERVICE_ERROR), - TEMPORARY_SERVICE_PROBLEM(20, "Service is temporarily unavailable, try again later", - HTTP_STATUS_CODE_SERVICE_UNAVAILABLE), - OUTSIDE_DEPENDENCY_RETURNED_A_TEMPORARY_ERROR(TEMPORARY_SERVICE_PROBLEM), - GENERIC_BAD_REQUEST(30, "Invalid request", HTTP_STATUS_CODE_BAD_REQUEST), - MISSING_EXPECTED_CONTENT(40, "Missing expected content", HTTP_STATUS_CODE_BAD_REQUEST), - TYPE_CONVERSION_ERROR(50, "Type conversion error", HTTP_STATUS_CODE_BAD_REQUEST), - MALFORMED_REQUEST(60, "Malformed request", HTTP_STATUS_CODE_BAD_REQUEST), - UNAUTHORIZED(70, "Unauthorized access", HTTP_STATUS_CODE_UNAUTHORIZED), - FORBIDDEN(80, "Forbidden access", HTTP_STATUS_CODE_FORBIDDEN), - NOT_FOUND(90, "The requested resource was not found", HTTP_STATUS_CODE_NOT_FOUND), - METHOD_NOT_ALLOWED(100, "Http Request method not allowed for this resource", HTTP_STATUS_CODE_METHOD_NOT_ALLOWED), - NO_ACCEPTABLE_REPRESENTATION(110, "No acceptable representation for this resource", - HTTP_STATUS_CODE_NOT_ACCEPTABLE), - UNSUPPORTED_MEDIA_TYPE(120, "Unsupported media type", HTTP_STATUS_CODE_UNSUPPORTED_MEDIA_TYPE), - TOO_MANY_REQUESTS(130, "Too many requests or simultaneous requests not allowed for this endpoint", - HTTP_STATUS_CODE_TOO_MANY_REQUESTS); - - private final ApiError delegate; - - BarebonesCoreApiErrorForTesting(ApiError delegate) { - this.delegate = delegate; - } - - BarebonesCoreApiErrorForTesting(int errorCode, String message, int httpStatusCode) { - this(new ApiErrorBase( - "delegated-to-enum-wrapper-" + UUID.randomUUID().toString(), errorCode, message, httpStatusCode - )); - } - - @Override - public String getName() { - return this.name(); - } - - @Override - public String getErrorCode() { - return delegate.getErrorCode(); - } - - @Override - public String getMessage() { - return delegate.getMessage(); - } - - @Override - public int getHttpStatusCode() { - return delegate.getHttpStatusCode(); - } - - @Override - public Map getMetadata() { - return delegate.getMetadata(); - } - -} \ No newline at end of file diff --git a/backstopper-reusable-tests/src/main/java/com/nike/backstopper/apierror/testutil/ProjectApiErrorsForTesting.java b/backstopper-reusable-tests/src/main/java/com/nike/backstopper/apierror/testutil/ProjectApiErrorsForTesting.java deleted file mode 100644 index a8f77bc..0000000 --- a/backstopper-reusable-tests/src/main/java/com/nike/backstopper/apierror/testutil/ProjectApiErrorsForTesting.java +++ /dev/null @@ -1,122 +0,0 @@ -package com.nike.backstopper.apierror.testutil; - -import com.nike.backstopper.apierror.ApiError; -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.apierror.projectspecificinfo.ProjectSpecificErrorCodeRange; - -import java.util.Arrays; -import java.util.List; - -/** - * An implementation of {@link ProjectApiErrors} intended to make unit and component testing easier. This uses {@link BarebonesCoreApiErrorForTesting} - * values for the core errors and special error method return values. You can pass in whatever you want for {@link #getProjectSpecificApiErrors()} - * and {@link #getProjectSpecificErrorCodeRange()} via the constructor (including null if that's what your test calls for). - * - * @deprecated This is the JUnit 4 version and will not be maintained long term. Please migrate to the JUnit 5 module: backstopper-reusable-tests-junit5 - * @author Nic Munroe - */ -@Deprecated -public abstract class ProjectApiErrorsForTesting extends ProjectApiErrors { - - private static final List BAREBONES_CORE_API_ERRORS_AS_LIST = Arrays.asList(BarebonesCoreApiErrorForTesting.values()); - - public static ProjectApiErrorsForTesting withProjectSpecificData(final List projectSpecificErrors, - final ProjectSpecificErrorCodeRange projectSpecificErrorCodeRange) { - return new ProjectApiErrorsForTesting() { - @Override - protected List getProjectSpecificApiErrors() { - return projectSpecificErrors; - } - - @Override - protected ProjectSpecificErrorCodeRange getProjectSpecificErrorCodeRange() { - return projectSpecificErrorCodeRange; - } - }; - } - - @Override - protected List getCoreApiErrors() { - return BAREBONES_CORE_API_ERRORS_AS_LIST; - } - - @Override - public ApiError getGenericServiceError() { - return BarebonesCoreApiErrorForTesting.GENERIC_SERVICE_ERROR; - } - - @Override - public ApiError getOusideDependencyReturnedAnUnrecoverableErrorApiError() { - return BarebonesCoreApiErrorForTesting.OUTSIDE_DEPENDENCY_RETURNED_AN_UNRECOVERABLE_ERROR; - } - - @Override - public ApiError getServersideValidationApiError() { - return BarebonesCoreApiErrorForTesting.SERVERSIDE_VALIDATION_ERROR; - } - - @Override - public ApiError getTemporaryServiceProblemApiError() { - return BarebonesCoreApiErrorForTesting.TEMPORARY_SERVICE_PROBLEM; - } - - @Override - public ApiError getOutsideDependencyReturnedTemporaryErrorApiError() { - return BarebonesCoreApiErrorForTesting.OUTSIDE_DEPENDENCY_RETURNED_A_TEMPORARY_ERROR; - } - - @Override - public ApiError getGenericBadRequestApiError() { - return BarebonesCoreApiErrorForTesting.GENERIC_BAD_REQUEST; - } - - @Override - public ApiError getMissingExpectedContentApiError() { - return BarebonesCoreApiErrorForTesting.MISSING_EXPECTED_CONTENT; - } - - @Override - public ApiError getTypeConversionApiError() { - return BarebonesCoreApiErrorForTesting.TYPE_CONVERSION_ERROR; - } - - @Override - public ApiError getMalformedRequestApiError() { - return BarebonesCoreApiErrorForTesting.MALFORMED_REQUEST; - } - - @Override - public ApiError getUnauthorizedApiError() { - return BarebonesCoreApiErrorForTesting.UNAUTHORIZED; - } - - @Override - public ApiError getForbiddenApiError() { - return BarebonesCoreApiErrorForTesting.FORBIDDEN; - } - - @Override - public ApiError getNotFoundApiError() { - return BarebonesCoreApiErrorForTesting.NOT_FOUND; - } - - @Override - public ApiError getMethodNotAllowedApiError() { - return BarebonesCoreApiErrorForTesting.METHOD_NOT_ALLOWED; - } - - @Override - public ApiError getNoAcceptableRepresentationApiError() { - return BarebonesCoreApiErrorForTesting.NO_ACCEPTABLE_REPRESENTATION; - } - - @Override - public ApiError getUnsupportedMediaTypeApiError() { - return BarebonesCoreApiErrorForTesting.UNSUPPORTED_MEDIA_TYPE; - } - - @Override - public ApiError getTooManyRequestsApiError() { - return BarebonesCoreApiErrorForTesting.TOO_MANY_REQUESTS; - } -} diff --git a/backstopper-reusable-tests/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/ReflectionBasedJsr303AnnotationTrollerBaseTest.java b/backstopper-reusable-tests/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/ReflectionBasedJsr303AnnotationTrollerBaseTest.java deleted file mode 100644 index d8db3bf..0000000 --- a/backstopper-reusable-tests/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/ReflectionBasedJsr303AnnotationTrollerBaseTest.java +++ /dev/null @@ -1,187 +0,0 @@ -package com.nike.backstopper.apierror.contract.jsr303convention; - -import org.assertj.core.api.ThrowableAssert; -import org.junit.Test; - -import java.lang.annotation.Annotation; -import java.lang.reflect.AnnotatedElement; -import java.lang.reflect.Constructor; -import java.lang.reflect.Field; -import java.lang.reflect.Member; -import java.lang.reflect.Method; -import java.util.UUID; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.catchThrowable; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; - -/** - * Mainly here to get code coverage for {@link ReflectionBasedJsr303AnnotationTrollerBase} above and beyond what - * {@link ReflectionMagicWorksTest} does. - * - * @author Nic Munroe - */ -public class ReflectionBasedJsr303AnnotationTrollerBaseTest { - - @Test - public void getAnnotatedElementLocationAsString_adds_correct_info_for_constructor() throws NoSuchMethodException { - // given - Constructor constructor = TestClass.class.getConstructor(); - - // when - String result = ReflectionBasedJsr303AnnotationTrollerBase.getAnnotatedElementLocationAsString(constructor); - - // then - assertThat(result).isEqualTo(TestClass.class.getName() + "[CONSTRUCTOR]"); - } - - @Test - public void getAnnotatedElementLocationAsString_adds_correct_info_for_class() throws NoSuchMethodException { - // given - Class clazz = TestClass.class; - - // when - String result = ReflectionBasedJsr303AnnotationTrollerBase.getAnnotatedElementLocationAsString(clazz); - - // then - assertThat(result).isEqualTo(TestClass.class.getName() + "[CLASS]"); - } - - @Test - public void getAnnotatedElementLocationAsString_adds_correct_info_for_method() throws NoSuchMethodException { - // given - Method method = TestClass.class.getDeclaredMethod("fooMethod"); - - // when - String result = ReflectionBasedJsr303AnnotationTrollerBase.getAnnotatedElementLocationAsString(method); - - // then - assertThat(result).isEqualTo(TestClass.class.getName() + "." + method.getName() + "[METHOD]"); - } - - @Test - public void getAnnotatedElementLocationAsString_adds_correct_info_for_field() - throws NoSuchMethodException, NoSuchFieldException { - // given - Field field = TestClass.class.getDeclaredField("fooField"); - - // when - String result = ReflectionBasedJsr303AnnotationTrollerBase.getAnnotatedElementLocationAsString(field); - - // then - assertThat(result).isEqualTo(TestClass.class.getName() + "." + field.getName() + "[FIELD]"); - } - - @Test - public void getAnnotatedElementLocationAsString_adds_correct_info_for_unknown_AnnotatedElement() { - // given - AnnotatedElement oddThing = new OddThing(); - - // when - String result = ReflectionBasedJsr303AnnotationTrollerBase.getAnnotatedElementLocationAsString(oddThing); - - // then - assertThat(result).isEqualTo(OddThing.class.getName() + "." + oddThing.toString() + "[???]"); - } - - @Test - public void getOwnerClass_throws_IllegalArgumentException_if_AnnotatedElement_is_not_Member_or_Class() { - // given - final AnnotatedElement notMemberOrClass = mock(AnnotatedElement.class); - - // when - Throwable ex = catchThrowable(new ThrowableAssert.ThrowingCallable() { - @Override - public void call() throws Throwable { - ReflectionBasedJsr303AnnotationTrollerBase.getOwnerClass(notMemberOrClass); - } - }); - - // then - assertThat(ex).isInstanceOf(IllegalArgumentException.class); - } - - @Test - public void extractMessageFromAnnotation_throws_wrapped_RuntimeException_if_annotation_blows_up() { - // given - RuntimeException exToThrow = new RuntimeException("kaboom"); - final Annotation annotation = mock(Annotation.class); - doThrow(exToThrow).when(annotation).annotationType(); - - // when - Throwable actual = catchThrowable(new ThrowableAssert.ThrowingCallable() { - @Override - public void call() throws Throwable { - ReflectionBasedJsr303AnnotationTrollerBase.extractMessageFromAnnotation(annotation); - } - }); - - // then - assertThat(actual) - .isNotEqualTo(exToThrow) - .isInstanceOf(RuntimeException.class) - .hasCause(exToThrow); - } - - private static class TestClass { - - public String fooField = "fooField"; - - public TestClass() {} - - public String fooMethod() { - return "fooMethod"; - } - } - - private static class OddThing implements AnnotatedElement, Member { - - public final String toStringVal = UUID.randomUUID().toString(); - - @Override - public String toString() { - return toStringVal; - } - - @Override - public Class getDeclaringClass() { - return OddThing.class; - } - - @Override - public boolean isAnnotationPresent(Class annotationClass) { - return false; - } - - @Override - public T getAnnotation(Class annotationClass) { - return null; - } - - @Override - public Annotation[] getAnnotations() { - return new Annotation[0]; - } - - @Override - public Annotation[] getDeclaredAnnotations() { - return new Annotation[0]; - } - - @Override - public String getName() { - return null; - } - - @Override - public int getModifiers() { - return 0; - } - - @Override - public boolean isSynthetic() { - return false; - } - } -} \ No newline at end of file diff --git a/backstopper-reusable-tests/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/ReflectionMagicWorksTest.java b/backstopper-reusable-tests/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/ReflectionMagicWorksTest.java deleted file mode 100644 index f97e33a..0000000 --- a/backstopper-reusable-tests/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/ReflectionMagicWorksTest.java +++ /dev/null @@ -1,273 +0,0 @@ -package com.nike.backstopper.apierror.contract.jsr303convention; - -import com.nike.internal.util.Pair; - -import com.google.common.base.Predicate; -import com.google.common.base.Predicates; - -import org.junit.Test; - -import java.lang.annotation.Annotation; -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; -import java.lang.reflect.AnnotatedElement; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import javax.validation.Constraint; -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; -import javax.validation.Payload; -import javax.validation.constraints.AssertFalse; -import javax.validation.constraints.AssertTrue; -import javax.validation.constraints.Max; -import javax.validation.constraints.Min; -import javax.validation.constraints.NotNull; - -import static com.nike.backstopper.apierror.contract.jsr303convention.ReflectionBasedJsr303AnnotationTrollerBase.extractMessageFromAnnotation; -import static com.nike.backstopper.apierror.contract.jsr303convention.ReflectionBasedJsr303AnnotationTrollerBase.generateExclusionForAnnotatedElementAndAnnotationClass; -import static com.nike.backstopper.apierror.contract.jsr303convention.ReflectionBasedJsr303AnnotationTrollerBase.getSubAnnotationListForAnnotationsOfClassType; -import static com.nike.backstopper.apierror.contract.jsr303convention.ReflectionBasedJsr303AnnotationTrollerBase.getSubAnnotationListForElementsOfOwnerClass; -import static com.nike.backstopper.apierror.contract.jsr303convention.ReflectionBasedJsr303AnnotationTrollerBase.getSubAnnotationListUsingExclusionFilters; -import static java.lang.annotation.ElementType.ANNOTATION_TYPE; -import static java.lang.annotation.ElementType.TYPE; -import static java.lang.annotation.RetentionPolicy.RUNTIME; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertTrue; - -/** - * Verifies the logic in {@link ReflectionBasedJsr303AnnotationTrollerBase} to make sure that its reflection magic works the way we expect it to. - * - * @author Nic Munroe - */ -public class ReflectionMagicWorksTest { - // This is static to make sure that it only gets created once in a JUnit context (rather than once per test method) because this is potentially time consuming. - private static final ReflectionBasedJsr303AnnotationTrollerBase TROLLER = new ReflectionBasedJsr303AnnotationTrollerBase() { - @Override - protected List> ignoreAllAnnotationsAssociatedWithTheseProjectClasses() { - return null; - } - - @Override - protected List>> specificAnnotationDeclarationExclusionsForProject() throws Exception { - return null; - } - }; - - private static final List>> STRICT_MEMBER_CHECK_EXCLUSIONS = StrictMemberCheck.getStrictMemberCheckExclusionsForNonCompliantDeclarations(); - - // ============== THE FOLLOWING TESTS VERIFY THAT ReflectionBasedJsr303AnnotationTrollerBase's CRAZY REFLECTION MAGIC STUFF WORKS AS EXPECTED. ======================== - /** - * Makes sure that the Reflections helper stuff is working properly and capturing all annotation possibilities (class type, constructor, constructor param, method, method param, and field). - */ - @Test - public void verifyThatTheReflectionsConfigurationIsCapturingAllAnnotationPossibilities() { - List> annotationOptionsClassAnnotations = getSubAnnotationListForElementsOfOwnerClass(TROLLER.allConstraintAnnotationsMasterList, - DifferentValidationAnnotationOptions.class); - assertThat(annotationOptionsClassAnnotations.size(), is(10)); - assertThat(getSubAnnotationListForAnnotationsOfClassType(annotationOptionsClassAnnotations, SomeClassLevelJsr303Annotation.class).size(), is(2)); - assertThat(getSubAnnotationListForAnnotationsOfClassType(annotationOptionsClassAnnotations, OtherClassLevelJsr303Annotation.class).size(), is(1)); - assertThat(getSubAnnotationListForAnnotationsOfClassType(annotationOptionsClassAnnotations, AssertTrue.class).size(), is(1)); - assertThat(getSubAnnotationListForAnnotationsOfClassType(annotationOptionsClassAnnotations, AssertFalse.class).size(), is(1)); - assertThat(getSubAnnotationListForAnnotationsOfClassType(annotationOptionsClassAnnotations, NotNull.class).size(), is(2)); - assertThat(getSubAnnotationListForAnnotationsOfClassType(annotationOptionsClassAnnotations, Min.class).size(), is(2)); - assertThat(getSubAnnotationListForAnnotationsOfClassType(annotationOptionsClassAnnotations, Max.class).size(), is(1)); - } - - /** - * Verifies that {@link ReflectionBasedJsr303AnnotationTrollerBase#getSubAnnotationListUsingExclusionFilters(java.util.List, java.util.List, java.util.List)} helper method for this test class is working properly when passing - * in annotatedElementOwnerClassesToExclude exclusion filter. - */ - @Test - public void verifyThatExclusionFilterMethodIsExcludingSpecifiedClasses() { - List> annotationOptionsClassAnnotations = getSubAnnotationListForElementsOfOwnerClass(TROLLER.allConstraintAnnotationsMasterList, - DifferentValidationAnnotationOptions.class); - List> strictMemberCheckClassAnnotations = getSubAnnotationListForElementsOfOwnerClass(TROLLER.allConstraintAnnotationsMasterList, - StrictMemberCheck.class); - - List> combinedAnnotations = new ArrayList<>(annotationOptionsClassAnnotations); - combinedAnnotations.addAll(strictMemberCheckClassAnnotations); - - assertThat(getSubAnnotationListUsingExclusionFilters(combinedAnnotations, Arrays.>asList(DifferentValidationAnnotationOptions.class), null).size(), is(strictMemberCheckClassAnnotations.size())); - assertThat(getSubAnnotationListUsingExclusionFilters(combinedAnnotations, Arrays.>asList(StrictMemberCheck.class), null).size(), is(annotationOptionsClassAnnotations.size())); - assertThat(getSubAnnotationListUsingExclusionFilters(combinedAnnotations, Arrays.>asList(DifferentValidationAnnotationOptions.class, StrictMemberCheck.class), null).size(), is(0)); - } - - /** - * Another test for {@link ReflectionBasedJsr303AnnotationTrollerBase#getSubAnnotationListUsingExclusionFilters(java.util.List, java.util.List, java.util.List)}, this time for the specificAnnotationDeclarationExclusionMatchers - * exclusion filter. - */ - @Test - public void verifyThatExclusionFilterMethodIsExcludingSpecifiedMembers() throws NoSuchFieldException, NoSuchMethodException { - List> strictMemberCheckClassAnnotations = getSubAnnotationListForElementsOfOwnerClass(TROLLER.allConstraintAnnotationsMasterList, - StrictMemberCheck.class); - - assertTrue(strictMemberCheckClassAnnotations.size() > 4); - assertThat( - getSubAnnotationListUsingExclusionFilters(strictMemberCheckClassAnnotations, null, STRICT_MEMBER_CHECK_EXCLUSIONS).size(), - is(strictMemberCheckClassAnnotations.size() - 4)); - } - - /** - * Another test for {@link ReflectionBasedJsr303AnnotationTrollerBase#getSubAnnotationListUsingExclusionFilters(java.util.List, java.util.List, java.util.List)} - */ - @Test - public void verifyThatExclusionFilterMethodIsExcludingBoth() throws NoSuchFieldException, NoSuchMethodException { - List> annotationOptionsClassAnnotations = getSubAnnotationListForElementsOfOwnerClass(TROLLER.allConstraintAnnotationsMasterList, - DifferentValidationAnnotationOptions.class); - List> strictMemberCheckClassAnnotations = getSubAnnotationListForElementsOfOwnerClass(TROLLER.allConstraintAnnotationsMasterList, - StrictMemberCheck.class); - - List> combinedAnnotations = new ArrayList<>(annotationOptionsClassAnnotations); - combinedAnnotations.addAll(strictMemberCheckClassAnnotations); - - assertTrue(strictMemberCheckClassAnnotations.size() > 4); - assertThat( - getSubAnnotationListUsingExclusionFilters(strictMemberCheckClassAnnotations, Arrays.>asList(DifferentValidationAnnotationOptions.class), - STRICT_MEMBER_CHECK_EXCLUSIONS).size(), - is(strictMemberCheckClassAnnotations.size() - 4)); - } - - @SomeClassLevelJsr303Annotation.List( - { - @SomeClassLevelJsr303Annotation(message = "I am a class annotated with a constraint in a list 1"), - @SomeClassLevelJsr303Annotation(message = "I am a class annotated with a constraint in a list 2") - } - ) - @OtherClassLevelJsr303Annotation(message = "I am a class annotated with a constraint NOT in a list") - public static class DifferentValidationAnnotationOptions { - @Min(value = 1, message = "I am a field annotated with a constraint") - private Integer annotatedField; - - @AssertTrue(message = "I am a constructor annotated with a constraint even though it doesn't really make sense") - public DifferentValidationAnnotationOptions(String nonAnnotatedConstructorParam, - @NotNull(message = "I am a constructor param annotated with a constraint 1") String annotatedConstructorParam1, - @NotNull(message = "I am a constructor param annotated with a constraint 2") String annotatedConstructorParam2, - String alsoNotAnnotatedConstructorParam) { - - } - - @AssertFalse(message = "I am an annotated method") - public boolean annotatedMethod(String nonAnnotatedMethodParam, - @Max(value = 42, message = "I am an annotated method param 1") Integer annotatedMethodParam1, - @Min(value = 42, message = "I am an annotated method param 2") Integer annotatedMethodParam2, - String alsoNotAnnotatedMethodParam) { - return true; - } - } - - @SomeClassLevelJsr303Annotation.List( - { - @SomeClassLevelJsr303Annotation(message = "I am not a ApiError enum name - class annotation"), - @SomeClassLevelJsr303Annotation(message = "INVALID_COUNT_VALUE") - } - ) - @OtherClassLevelJsr303Annotation(message = "I am also not a ApiError enum name - class annotation 2") - public static class StrictMemberCheck { - @Min(value = 1, message="INVALID_COUNT_VALUE") - public Integer compliantField; - @Min(value = 1, message="I am not a ApiError enum name - field annotation") - private Integer nonCompliantField; - - @NotNull(message = "TYPE_CONVERSION_ERROR") - private String compliantMethod() { - return null; - } - - @NotNull(message = "I am not a ApiError enum name - method annotation") - public String nonCompliantMethod() { - return null; - } - - /** - * @return The list of annotation exclusions (generated by {@link ReflectionBasedJsr303AnnotationTrollerBase#generateExclusionForAnnotatedElementAndAnnotationClass(java.lang.reflect.AnnotatedElement, Class)}) - * for this StrictMemberCheck class that are intentionally not compliant with the "all JSR 303 messages should point to {@link com.nike.backstopper.apierror.ApiError}" requirement - * (and should therefore not cause the unit tests based on {@link ReflectionBasedJsr303AnnotationTrollerBase} to fail). - */ - public static List>> getStrictMemberCheckExclusionsForNonCompliantDeclarations() { - try { - List>> strictExclusionsList = new ArrayList<>(); - - strictExclusionsList.addAll( - Arrays.asList(generateExclusionForAnnotatedElementAndAnnotationClass(StrictMemberCheck.class.getDeclaredField("nonCompliantField"), Min.class), - generateExclusionForAnnotatedElementAndAnnotationClass(StrictMemberCheck.class.getDeclaredMethod("nonCompliantMethod"), NotNull.class), - generateExclusionForAnnotatedElementAndAnnotationClass(StrictMemberCheck.class, OtherClassLevelJsr303Annotation.class), - Predicates.and(generateExclusionForAnnotatedElementAndAnnotationClass(StrictMemberCheck.class, SomeClassLevelJsr303Annotation.class), - new Predicate>() { - public boolean apply(Pair input) { - // At this point we know it's StrictMemberCheck class and a SomeClassLevelJsr303Annotation annotation. We want to exclude the one we know is bad that we - // don't care about. - String message = extractMessageFromAnnotation(input.getLeft()); - return message.equals("I am not a ApiError enum name - class annotation"); - } - }))); - - return strictExclusionsList; - } catch (Exception e) { - throw new RuntimeException(e); - } - } - } - - @Target({TYPE, ANNOTATION_TYPE}) - @Retention(RUNTIME) - @Documented - @Constraint(validatedBy = SomeClassLevelJsr303AnnotationValidator.class) - public @interface SomeClassLevelJsr303Annotation { - String message() default "{SomeClassLevelJsr303Annotation.message}"; - Class[] groups() default { }; - Class[] payload() default {}; - - @Target({TYPE, ANNOTATION_TYPE}) - @Retention(RUNTIME) - @Documented - @interface List { - SomeClassLevelJsr303Annotation[] value(); - } - } - - public class SomeClassLevelJsr303AnnotationValidator implements ConstraintValidator { - @Override - public void initialize(SomeClassLevelJsr303Annotation constraintAnnotation) { - - } - - @Override - public boolean isValid(Object value, ConstraintValidatorContext context) { - return true; - } - } - - @Target({TYPE, ANNOTATION_TYPE}) - @Retention(RUNTIME) - @Documented - @Constraint(validatedBy = OtherClassLevelJsr303AnnotationValidator.class) - public @interface OtherClassLevelJsr303Annotation { - String message() default "{OtherClassLevelJsr303Annotation.message}"; - Class[] groups() default { }; - Class[] payload() default {}; - - @Target({TYPE, ANNOTATION_TYPE}) - @Retention(RUNTIME) - @Documented - @interface List { - OtherClassLevelJsr303Annotation[] value(); - } - } - - public class OtherClassLevelJsr303AnnotationValidator implements ConstraintValidator { - @Override - public void initialize(OtherClassLevelJsr303Annotation constraintAnnotation) { - - } - - @Override - public boolean isValid(Object value, ConstraintValidatorContext context) { - return true; - } - } - -} diff --git a/backstopper-reusable-tests/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyEnumsReferencedByStringConvertsToClassTypeJsr303AnnotationsAreJacksonCaseInsensitiveTestTest.java b/backstopper-reusable-tests/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyEnumsReferencedByStringConvertsToClassTypeJsr303AnnotationsAreJacksonCaseInsensitiveTestTest.java deleted file mode 100644 index 97b3272..0000000 --- a/backstopper-reusable-tests/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyEnumsReferencedByStringConvertsToClassTypeJsr303AnnotationsAreJacksonCaseInsensitiveTestTest.java +++ /dev/null @@ -1,128 +0,0 @@ -package com.nike.backstopper.apierror.contract.jsr303convention; - -import com.nike.backstopper.validation.constraints.StringConvertsToClassType; -import com.nike.internal.util.Pair; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.google.common.base.Predicate; - -import org.assertj.core.api.ThrowableAssert; -import org.junit.Test; - -import java.lang.annotation.Annotation; -import java.lang.reflect.AnnotatedElement; -import java.util.ArrayList; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.ThrowableAssert.catchThrowable; - -/** - * Tests basic functionality of {@link VerifyEnumsReferencedByStringConvertsToClassTypeJsr303AnnotationsAreJacksonCaseInsensitiveTest}. - * - * @author Nic Munroe - */ -public class VerifyEnumsReferencedByStringConvertsToClassTypeJsr303AnnotationsAreJacksonCaseInsensitiveTestTest { - - @Test - public void verify_test_passes_for_valid_annotations_and_enum_definitions() { - // given - final VerifyEnumsReferencedByStringConvertsToClassTypeJsr303AnnotationsAreJacksonCaseInsensitiveTest testImpl = - getTestImpl("caseSensitiveEnumFieldWithInsensitivityAllowed"); - - // when - Throwable ex = catchThrowable(new ThrowableAssert.ThrowingCallable() { - @Override - public void call() throws Throwable { - testImpl.verifyEnumsReferencedByStringConvertsToClassTypeJsr303AnnotationsAreCaseInsensitive(); - } - }); - - // then - assertThat(ex).isNull(); - } - - @Test - public void verify_test_fails_for_annotation_that_allows_case_insensitivity_but_enum_that_is_case_sensitive() { - // given - final VerifyEnumsReferencedByStringConvertsToClassTypeJsr303AnnotationsAreJacksonCaseInsensitiveTest testImpl = - getTestImpl(); - - // when - Throwable ex = catchThrowable(new ThrowableAssert.ThrowingCallable() { - @Override - public void call() throws Throwable { - testImpl.verifyEnumsReferencedByStringConvertsToClassTypeJsr303AnnotationsAreCaseInsensitive(); - } - }); - - // then - assertThat(ex) - .isInstanceOf(AssertionError.class) - .hasMessageContaining("$CaseSensitiveEnum") - .hasMessageContaining("AnnotatedClass.caseSensitiveEnumFieldWithInsensitivityAllowed"); - } - - private VerifyEnumsReferencedByStringConvertsToClassTypeJsr303AnnotationsAreJacksonCaseInsensitiveTest - getTestImpl(final String ... fieldsToIgnore) { - return new VerifyEnumsReferencedByStringConvertsToClassTypeJsr303AnnotationsAreJacksonCaseInsensitiveTest() { - @Override - protected ReflectionBasedJsr303AnnotationTrollerBase getAnnotationTroller() { - return new ReflectionBasedJsr303AnnotationTrollerBase() { - @Override - protected List> ignoreAllAnnotationsAssociatedWithTheseProjectClasses() { - return null; - } - - @Override - protected List>> specificAnnotationDeclarationExclusionsForProject() - throws Exception { - if (fieldsToIgnore == null || fieldsToIgnore.length == 0) - return null; - - List>> ignoreList = new ArrayList<>(); - for (String fieldName : fieldsToIgnore) { - ignoreList.add( - ReflectionBasedJsr303AnnotationTrollerBase - .generateExclusionForAnnotatedElementAndAnnotationClass( - AnnotatedClass.class.getDeclaredField(fieldName), StringConvertsToClassType.class) - ); - } - return ignoreList; - } - }; - } - }; - } - - private enum CaseSensitiveEnum { - foo, BAR, BlAh - } - - private enum CaseInsensitiveEnum { - BAZ, bat, wHeE; - - @JsonCreator - public static CaseInsensitiveEnum toCaseInsensitiveEnum(String stringVal) { - for (CaseInsensitiveEnum enumVal : values()) { - if (enumVal.name().equalsIgnoreCase(stringVal)) - return enumVal; - } - throw new IllegalArgumentException("Cannot convert the string: \"" + stringVal + "\" to a valid CaseInsensitiveEnum enum value."); - } - } - - private static class AnnotatedClass { - @StringConvertsToClassType(message = "GENERIC_BAD_REQUEST", classType = CaseSensitiveEnum.class, allowCaseInsensitiveEnumMatch = true) - public String caseSensitiveEnumFieldWithInsensitivityAllowed; - - @StringConvertsToClassType(message = "GENERIC_BAD_REQUEST", classType = CaseSensitiveEnum.class, allowCaseInsensitiveEnumMatch = false) - public String caseSensitiveEnumFieldWithInsensitivityDisallowed; - - @StringConvertsToClassType(message = "GENERIC_BAD_REQUEST", classType = CaseInsensitiveEnum.class, allowCaseInsensitiveEnumMatch = true) - public String caseInsensitiveEnumFieldWithInsensitivityAllowed; - - @StringConvertsToClassType(message = "GENERIC_BAD_REQUEST", classType = CaseInsensitiveEnum.class, allowCaseInsensitiveEnumMatch = false) - public String caseInsensitiveEnumFieldWithInsensitivityDisallowed; - } -} \ No newline at end of file diff --git a/backstopper-reusable-tests/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyJsr303ValidationMessagesPointToApiErrorsTestTest.java b/backstopper-reusable-tests/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyJsr303ValidationMessagesPointToApiErrorsTestTest.java deleted file mode 100644 index 3f9e353..0000000 --- a/backstopper-reusable-tests/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyJsr303ValidationMessagesPointToApiErrorsTestTest.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.nike.backstopper.apierror.contract.jsr303convention; - -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.apierror.testutil.ProjectApiErrorsForTesting; -import com.nike.internal.util.Pair; - -import com.google.common.base.Predicate; - -import org.assertj.core.api.ThrowableAssert; -import org.junit.Test; - -import java.lang.annotation.Annotation; -import java.lang.reflect.AnnotatedElement; -import java.lang.reflect.InvocationTargetException; -import java.util.Arrays; -import java.util.List; - -import javax.validation.constraints.Min; -import javax.validation.constraints.NotNull; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.catchThrowable; - -/** - * Tests basic functionality of {@link VerifyJsr303ValidationMessagesPointToApiErrorsTest}. - * - * @author Nic Munroe - */ -public class VerifyJsr303ValidationMessagesPointToApiErrorsTestTest { - - @Test - public void verify_basic_functionality() - throws NoSuchMethodException, IllegalAccessException, InvocationTargetException { - // given - final VerifyJsr303ValidationMessagesPointToApiErrorsTest tester = new VerifyJsr303ValidationMessagesPointToApiErrorsTest() { - @Override - protected ReflectionBasedJsr303AnnotationTrollerBase getAnnotationTroller() { - return new ReflectionBasedJsr303AnnotationTrollerBase() { - @Override - protected List> ignoreAllAnnotationsAssociatedWithTheseProjectClasses() { - return Arrays.asList( - ReflectionMagicWorksTest.DifferentValidationAnnotationOptions.class, - ReflectionMagicWorksTest.StrictMemberCheck.class - ); - } - - @Override - protected List>> specificAnnotationDeclarationExclusionsForProject() - throws Exception { - return null; - } - }; - } - - @Override - protected ProjectApiErrors getProjectApiErrors() { - return ProjectApiErrorsForTesting.withProjectSpecificData(null, null); - } - }; - - // when - Throwable ex = catchThrowable(new ThrowableAssert.ThrowingCallable() { - @Override - public void call() throws Throwable { - tester.verifyThatAllValidationAnnotationsReferToApiErrors(); - } - }); - - // then - assertThat(ex) - .isInstanceOf(AssertionError.class) - .hasMessageContaining("GARBAGE") - .hasMessageContaining("NOT_A_THING"); - assertThat(ex.getMessage()).doesNotContain("GENERIC_BAD_REQUEST"); - } - - private static class TestValidationObject { - - @NotNull(message = "GENERIC_BAD_REQUEST") // This one should not trigger a unit test error since it exists in the ProjectApiErrors - public String fooString; - - @Min(message = "GARBAGE", value = 0) - public Integer fooInteger; - - @NotNull(message = "NOT_A_THING") - public Object fooObject; - } - -} \ No newline at end of file diff --git a/backstopper-reusable-tests/src/test/java/com/nike/backstopper/apierror/projectspecificinfo/ProjectApiErrorsTestBaseTest.java b/backstopper-reusable-tests/src/test/java/com/nike/backstopper/apierror/projectspecificinfo/ProjectApiErrorsTestBaseTest.java deleted file mode 100644 index baf79bd..0000000 --- a/backstopper-reusable-tests/src/test/java/com/nike/backstopper/apierror/projectspecificinfo/ProjectApiErrorsTestBaseTest.java +++ /dev/null @@ -1,226 +0,0 @@ -package com.nike.backstopper.apierror.projectspecificinfo; - -import com.nike.backstopper.apierror.ApiError; -import com.nike.backstopper.apierror.ApiErrorBase; -import com.nike.backstopper.apierror.testutil.BarebonesCoreApiErrorForTesting; -import com.nike.backstopper.apierror.testutil.ProjectApiErrorsForTesting; - -import org.assertj.core.api.ThrowableAssert; -import org.junit.Test; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.UUID; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.catchThrowable; -import static org.mockito.Matchers.any; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; - -/** - * Runs the {@link ProjectApiErrorsTestBase} tests on a default {@link ProjectApiErrors}. Helps catch when the tests need to change - * due to legitimate changes to the code. - */ -public class ProjectApiErrorsTestBaseTest extends ProjectApiErrorsTestBase { - - @Override - protected ProjectApiErrors getProjectApiErrors() { - return ProjectApiErrorsForTesting.withProjectSpecificData(null, null); - } - - @Test - public void shouldNotContainDuplicateNamedApiErrors_blows_up_if_it_finds_duplicate_ApiErrors() { - // given - final ProjectApiErrorsTestBase base = new ProjectApiErrorsTestBase() { - @Override - protected ProjectApiErrors getProjectApiErrors() { - return ProjectApiErrorsForTesting.withProjectSpecificData(Arrays.asList( - new ApiErrorBase("DUPNAME1", 42, "foo", 400), - new ApiErrorBase("DUPNAME1", 4242, "bar", 500), - new ApiErrorBase("DUPNAME2", 52, "foo2", 401), - new ApiErrorBase("DUPNAME2", 5252, "bar2", 501), - new ApiErrorBase("DUPNAME2", 525252, "baz", 900) - ), ProjectSpecificErrorCodeRange.ALLOW_ALL_ERROR_CODES); - } - }; - - // when - Throwable ex = catchThrowable(new ThrowableAssert.ThrowingCallable() { - @Override - public void call() throws Throwable { - base.shouldNotContainDuplicateNamedApiErrors(); - } - }); - - // then - assertThat(ex) - .isInstanceOf(AssertionError.class) - .hasMessageContaining("[DUPNAME1, 2], [DUPNAME2, 3]"); - } - - - @Test - public void findRandomApiErrorWithHttpStatusCode_throws_IllegalStateException_if_it_cannot_find_error_with_specified_status_code() { - // given - final ProjectApiErrorsTestBase base = new ProjectApiErrorsTestBase() { - @Override - protected ProjectApiErrors getProjectApiErrors() { - return ProjectApiErrorsForTesting.withProjectSpecificData(null, null); - } - }; - - // when - Throwable ex = catchThrowable(new ThrowableAssert.ThrowingCallable() { - @Override - public void call() throws Throwable { - base.findRandomApiErrorWithHttpStatusCode(42424242); - } - }); - - // then - assertThat(ex).isInstanceOf(IllegalStateException.class); - } - - @Test - public void verifyGetStatusCodePriorityOrderMethodContainsAllRelevantCodes_throws_AssertionError_if_it_finds_bad_state() { - // given - final ProjectApiErrorsTestBase base = new ProjectApiErrorsTestBase() { - @Override - protected ProjectApiErrors getProjectApiErrors() { - return ProjectApiErrorsForTesting.withProjectSpecificData(Arrays.asList( - new ApiErrorBase("FOOBAR", 42, "foo", 123456) - ), ProjectSpecificErrorCodeRange.ALLOW_ALL_ERROR_CODES); - } - }; - - // when - Throwable ex = catchThrowable(new ThrowableAssert.ThrowingCallable() { - @Override - public void call() throws Throwable { - base.verifyGetStatusCodePriorityOrderMethodContainsAllRelevantCodes(); - } - }); - - // then - assertThat(ex) - .isInstanceOf(AssertionError.class) - .hasMessageContaining("getStatusCodePriorityOrder() did not contain HTTP Status Code: 123456"); - } - - @Test - public void allErrorsShouldBeCoreApiErrorsOrCoreApiErrorWrappersOrFallInProjectSpecificErrorRange_works_for_valid_cases() { - // given - final ApiError coreError = BarebonesCoreApiErrorForTesting.TYPE_CONVERSION_ERROR; - final ApiError coreErrorWrapper = new ApiErrorBase(coreError, "FOOBAR"); - final String customErrorCode = UUID.randomUUID().toString(); - final ApiError validErrorInRange = new ApiErrorBase("WHEEE", customErrorCode, "whee message", 400); - final ProjectSpecificErrorCodeRange restrictiveErrorCodeRange = new ProjectSpecificErrorCodeRange() { - @Override - public boolean isInRange(ApiError error) { - return customErrorCode.equals(error.getErrorCode()); - } - - @Override - public String getName() { - return "RANGE_FOR_TESTING"; - } - }; - final ProjectApiErrorsTestBase base = new ProjectApiErrorsTestBase() { - @Override - protected ProjectApiErrors getProjectApiErrors() { - return ProjectApiErrorsForTesting.withProjectSpecificData(Arrays.asList(coreErrorWrapper, validErrorInRange), - restrictiveErrorCodeRange); - } - }; - - // when - Throwable ex = catchThrowable(new ThrowableAssert.ThrowingCallable() { - @Override - public void call() throws Throwable { - base.allErrorsShouldBeCoreApiErrorsOrCoreApiErrorWrappersOrFallInProjectSpecificErrorRange(); - } - }); - - // then - assertThat(ex).isNull(); - } - - @Test - public void allErrorsShouldBeCoreApiErrorsOrCoreApiErrorWrappersOrFallInProjectSpecificErrorRange_should_explode_if_there_are_non_core_errors_and_range_is_null() { - // given - final ApiError nonCoreError = new ApiErrorBase("FOO", UUID.randomUUID().toString(), "foo message", 500); - final List coreErrors = Arrays.asList(BarebonesCoreApiErrorForTesting.values()); - final List allProjectErrors = new ArrayList<>(coreErrors); - allProjectErrors.add(nonCoreError); - final ProjectApiErrors projectApiErrorsMock = mock(ProjectApiErrors.class); - final ProjectApiErrorsTestBase base = new ProjectApiErrorsTestBase() { - @Override - protected ProjectApiErrors getProjectApiErrors() { - return projectApiErrorsMock; - } - }; - doReturn(null).when(projectApiErrorsMock).getProjectSpecificErrorCodeRange(); - doReturn(coreErrors).when(projectApiErrorsMock).getCoreApiErrors(); - doReturn(false).when(projectApiErrorsMock).isWrapperAroundCoreError(any(ApiError.class), any(List.class)); - doReturn(allProjectErrors).when(projectApiErrorsMock).getProjectApiErrors(); - - // when - Throwable ex = catchThrowable(new ThrowableAssert.ThrowingCallable() { - @Override - public void call() throws Throwable { - base.allErrorsShouldBeCoreApiErrorsOrCoreApiErrorWrappersOrFallInProjectSpecificErrorRange(); - } - }); - - // then - assertThat(ex) - .isInstanceOf(AssertionError.class) - .hasMessageContaining(nonCoreError.getErrorCode()); - } - - @Test - public void allErrorsShouldBeCoreApiErrorsOrCoreApiErrorWrappersOrFallInProjectSpecificErrorRange_should_explode_if_there_are_non_core_errors_that_are_also_not_in_range() { - // given - final ApiError nonCoreError = new ApiErrorBase("FOO", UUID.randomUUID().toString(), "foo message", 500); - final List coreErrors = Arrays.asList(BarebonesCoreApiErrorForTesting.values()); - final List allProjectErrors = new ArrayList<>(coreErrors); - allProjectErrors.add(nonCoreError); - final ProjectApiErrors projectApiErrorsMock = mock(ProjectApiErrors.class); - final ProjectApiErrorsTestBase base = new ProjectApiErrorsTestBase() { - @Override - protected ProjectApiErrors getProjectApiErrors() { - return projectApiErrorsMock; - } - }; - ProjectSpecificErrorCodeRange range = new ProjectSpecificErrorCodeRange() { - @Override - public boolean isInRange(ApiError error) { - return false; - } - - @Override - public String getName() { - return "RANGE_FOR_TESTING"; - } - }; - doReturn(range).when(projectApiErrorsMock).getProjectSpecificErrorCodeRange(); - doReturn(coreErrors).when(projectApiErrorsMock).getCoreApiErrors(); - doReturn(false).when(projectApiErrorsMock).isWrapperAroundCoreError(any(ApiError.class), any(List.class)); - doReturn(allProjectErrors).when(projectApiErrorsMock).getProjectApiErrors(); - - // when - Throwable ex = catchThrowable(new ThrowableAssert.ThrowingCallable() { - @Override - public void call() throws Throwable { - base.allErrorsShouldBeCoreApiErrorsOrCoreApiErrorWrappersOrFallInProjectSpecificErrorRange(); - } - }); - - // then - assertThat(ex) - .isInstanceOf(AssertionError.class) - .hasMessageContaining(nonCoreError.getErrorCode()); - } -} \ No newline at end of file From d8af9d31b234c82f56e83db79e4a452363860440 Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Mon, 9 Sep 2024 18:38:05 -0700 Subject: [PATCH 10/42] Migrate backstopper-spring-web-mvc from javax to jakarta --- backstopper-spring-web-mvc/README.md | 106 ++++++++++++------ backstopper-spring-web-mvc/build.gradle | 24 ++-- .../spring/SpringApiExceptionHandler.java | 10 +- .../SpringApiExceptionHandlerUtils.java | 4 +- .../SpringContainerErrorController.java | 2 +- .../SpringUnhandledExceptionHandler.java | 10 +- .../config/BackstopperSpringWebMvcConfig.java | 2 +- .../ApiExceptionHandlerListenerList.java | 6 +- ...bMvcFrameworkExceptionHandlerListener.java | 6 +- .../BaseSpringEnabledValidationTestCase.java | 10 +- .../base/TestCaseValidationSpringConfig.java | 6 +- .../handler/ClientfacingErrorITest.java | 10 +- .../spring/SpringApiExceptionHandlerTest.java | 15 +-- .../SpringApiExceptionHandlerUtilsTest.java | 2 +- .../SpringContainerErrorControllerTest.java | 2 +- .../SpringUnhandledExceptionHandlerTest.java | 4 +- ...FrameworkExceptionHandlerListenerTest.java | 2 +- ...lFastServersideValidationServiceITest.java | 6 +- build.gradle | 2 +- settings.gradle | 4 +- 20 files changed, 130 insertions(+), 103 deletions(-) diff --git a/backstopper-spring-web-mvc/README.md b/backstopper-spring-web-mvc/README.md index ed2b647..0db37b5 100644 --- a/backstopper-spring-web-mvc/README.md +++ b/backstopper-spring-web-mvc/README.md @@ -1,47 +1,70 @@ # Backstopper - spring-web-mvc -Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 7 and greater. +Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 17 and greater. -This readme focuses specifically on the Backstopper Spring Web MVC integration. If you are looking for a different framework integration check out the [relevant section](../README.md#framework_modules) of the base readme to see if one already exists. The [base project README.md](../README.md) and [User Guide](../USER_GUIDE.md) contain the main bulk of information regarding Backstopper. +(NOTE: The [Backstopper 1.x branch](https://github.com/Nike-Inc/backstopper/tree/v1.x) contains a version of +Backstopper for Java 7+, and for the `javax` ecosystem. The current Backstopper supports Java 17+ and the `jakarta` +ecosystem. It also contains support for Spring 4 and 5, and Springboot 1 and 2.) -**NOTE: There is a [Spring Web MVC sample application](../samples/sample-spring-web-mvc/) that provides a simple concrete example of the information covered in this readme.** +This readme focuses specifically on the Backstopper Spring Web MVC integration. If you are looking for a different +framework integration check out the [relevant section](../README.md#framework_modules) of the base readme to see if one +already exists. The [base project README.md](../README.md) and [User Guide](../USER_GUIDE.md) contain the main bulk of +information regarding Backstopper. + +**NOTE: There is a [Spring Web MVC sample application](../samples/sample-spring-web-mvc/) that provides a simple +concrete example of the information covered in this readme.** ## Backstopper Spring Web MVC Setup, Configuration, and Usage ### Spring / Spring Boot Versions -This `backstopper-spring-web-mvc` library can be used in any Spring Web MVC `4.3.x` environment or later. This includes -Spring 5, Spring Boot 1, and Spring Boot 2. +// TODO javax-to-jakarta: verify this statement about spring boot 3 + +This `backstopper-spring-web-mvc` library can be used in any Spring Web MVC `6.x.x` environment or later. This includes +Spring 6 and Spring Boot 3. -_NOTE: **This library does not cover Spring WebFlux (Netty) applications.** Spring Web MVC and Spring WebFlux are +_NOTE: **This library does not cover Spring WebFlux (Netty) applications.** Spring Web MVC and Spring WebFlux are [mutually exclusive](https://stackoverflow.com/questions/53883037/can-i-use-springmvc-and-webflux-together). If you're -looking for Backstopper support for Spring WebFlux please see the +looking for Backstopper support for Spring WebFlux please see the [backstopper-spring-web-flux](../backstopper-spring-web-flux) module instead of this one._ - ### Setup * Pull in the `com.nike.backstopper:backstopper-spring-web-mvc` dependency into your project. -* Register Backstopper components with Spring Web MVC, either via `@Import({BackstopperSpringWebMvcConfig.class})`, or -`@ComponentScan(basePackages = "com.nike.backstopper")`. See the javadocs on `BackstopperSpringWebMvcConfig` for some -related details. - * This causes `SpringApiExceptionHandler` and `SpringUnhandledExceptionHandler` to be registered with the Spring - Web MVC error handling chain in a way that overrides the default Spring Web MVC error handlers so that the - Backstopper handlers will take care of *all* errors. It sets up `SpringApiExceptionHandler` with a default list - of `ApiExceptionHandlerListener` listeners that should be sufficient for most projects. You can override that list - of listeners (and/or many other Backstopper components) if needed in your project's Spring config. - * It also registers `SpringContainerErrorController` to handle errors that happen outside Spring - Web MVC (i.e. in the Servlet container), and make sure they're routed through Backstopper as well. Note that you'll - need to configure your Servlet container to route container errors to the path this controller listens on - (default `/error`) for it to work. -* Expose your project's `ProjectApiErrors` and a JSR 303 `javax.validation.Validator` implementation in your Spring dependency injection config. - * `ProjectApiErrors` creation is discussed in the base Backstopper readme [here](../README.md#quickstart_usage_project_api_errors). - * JSR 303 setup and generation of a `Validator` is discussed in the Backstopper User Guide [here](../USER_GUIDE.md#jsr_303_basic_setup). If you're not going to be doing any JSR 303 validation outside what is built-in supported by Spring Web MVC, *and* you don't want to bother jumping through the hoops to get a handle on Spring's JSR 303 validator impl provided by `WebMvcConfigurer.getValidator()`, *and* you don't want to bother creating a real `Validator` yourself then you can simply register `NoOpJsr303Validator#SINGLETON_IMPL` as the `Validator` that gets exposed by your Spring config. `ClientDataValidationService` and `FailFastServersideValidationService` would fail to do anything, but if you don't use those then it wouldn't matter. -* Setup the reusable unit tests for your project as described in the Backstopper User Guide [here](../USER_GUIDE.md#reusable_tests) and shown in the sample application. +* Register Backstopper components with Spring Web MVC, either via `@Import({BackstopperSpringWebMvcConfig.class})`, or + `@ComponentScan(basePackages = "com.nike.backstopper")`. See the javadocs on `BackstopperSpringWebMvcConfig` for some + related details. + * This causes `SpringApiExceptionHandler` and `SpringUnhandledExceptionHandler` to be registered with the Spring + Web MVC error handling chain in a way that overrides the default Spring Web MVC error handlers so that the + Backstopper handlers will take care of *all* errors. It sets up `SpringApiExceptionHandler` with a default list + of `ApiExceptionHandlerListener` listeners that should be sufficient for most projects. You can override that list + of listeners (and/or many other Backstopper components) if needed in your project's Spring config. + * It also registers `SpringContainerErrorController` to handle errors that happen outside Spring + Web MVC (i.e. in the Servlet container), and make sure they're routed through Backstopper as well. Note that + you'll need to configure your Servlet container to route container errors to the path this controller listens on + (default `/error`) for it to work. +* Expose your project's `ProjectApiErrors` and a JSR 303 `jakarta.validation.Validator` implementation in your Spring + dependency injection config. + * `ProjectApiErrors` creation is discussed in the base Backstopper + readme [here](../README.md#quickstart_usage_project_api_errors). + * JSR 303 setup and generation of a `Validator` is discussed in the Backstopper User + Guide [here](../USER_GUIDE.md#jsr_303_basic_setup). If you're not going to be doing any JSR 303 validation outside + what is built-in supported by Spring Web MVC, *and* you don't want to bother jumping through the hoops to get a + handle on Spring's JSR 303 validator impl provided by `WebMvcConfigurer.getValidator()`, *and* you don't want to + bother creating a real `Validator` yourself then you can simply register `NoOpJsr303Validator#SINGLETON_IMPL` as + the `Validator` that gets exposed by your Spring config. `ClientDataValidationService` and + `FailFastServersideValidationService` would fail to do anything, but if you don't use those then it wouldn't + matter. +* Setup the reusable unit tests for your project as described in the Backstopper User + Guide [here](../USER_GUIDE.md#reusable_tests) and shown in the sample application. ### Usage -The base Backstopper readme covers the [usage basics](../README.md#quickstart_usage). There should be no difference when running in a Spring Web MVC environment, but since Spring Web MVC integrates a JSR 303 validation system into its core functionality we can get one extra nice tidbit: to have Spring Web MVC run validation on objects deserialized from incoming user data you can simply add `@Valid` annotations on the objects you're deserializing for your controller endpoints (`@RequestBody` object, `@ModelAttribute` objects, etc). For example: +The base Backstopper readme covers the [usage basics](../README.md#quickstart_usage). There should be no difference when +running in a Spring Web MVC environment, but since Spring Web MVC integrates a JSR 303 validation system into its core +functionality we can get one extra nice tidbit: to have Spring Web MVC run validation on objects deserialized from +incoming user data you can simply add `@Valid` annotations on the objects you're deserializing for your controller +endpoints (`@RequestBody` object, `@ModelAttribute` objects, etc). For example: ``` java @RequestMapping(method=RequestMethod.POST) @@ -56,29 +79,38 @@ public SomeOutputObject postSomeInput( } ``` -This method signature with the two `@Valid` annotations would cause both the `@ModelAttribute` `headersAndQueryParams` and `@RequestBody` `inputObject` arguments to be run through JSR 303 validation. Any constraint violations caught at this time will cause a Spring-specific exception to be thrown with the constraint violation details buried inside. This `backstopper-spring-web-mvc` plugin library's error handler listeners know how to convert this to the appropriate set of `ApiError` cases (from your `ProjectApiErrors`) automatically using the [Backstopper JSR 303 naming convention](../USER_GUIDE.md#jsr303_conventions), which are then returned to the client using the standard error contract. +This method signature with the two `@Valid` annotations would cause both the `@ModelAttribute` `headersAndQueryParams` +and `@RequestBody` `inputObject` arguments to be run through JSR 303 validation. Any constraint violations caught at +this time will cause a Spring-specific exception to be thrown with the constraint violation details buried inside. This +`backstopper-spring-web-mvc` plugin library's error handler listeners know how to convert this to the appropriate set of +`ApiError` cases (from your `ProjectApiErrors`) automatically using +the [Backstopper JSR 303 naming convention](../USER_GUIDE.md#jsr303_conventions), which are then returned to the client +using the standard error contract. -This feature allows you to enjoy the Backstopper JSR 303 validation integration support automatically at the point where caller-provided data is deserialized and passed to your controller endpoint without having to inject and manually call a `ClientDataValidationService`. +This feature allows you to enjoy the Backstopper JSR 303 validation integration support automatically at the point where +caller-provided data is deserialized and passed to your controller endpoint without having to inject and manually call a +`ClientDataValidationService`. ## NOTE - Spring WebMVC and Servlet API dependencies required at runtime -This `backstopper-spring-web-mvc` module does not export any transitive Spring or Servlet API dependencies to prevent runtime -version conflicts with whatever Spring and Servlet environment you deploy to. +This `backstopper-spring-web-mvc` module does not export any transitive Spring or Servlet API dependencies to prevent +runtime version conflicts with whatever Spring and Servlet environment you deploy to. This should not affect most users since this library is likely to be used in a Spring/Servlet environment where the -required dependencies are already on the classpath at runtime, however if you receive class-not-found errors related to -Spring or Servlet API classes then you'll need to pull the necessary dependency into your project. +required dependencies are already on the classpath at runtime, however if you receive class-not-found errors related to +Spring or Servlet API classes then you'll need to pull the necessary dependency into your project. The dependencies you may need to pull in: -* Spring Web MVC: [org.springframework:spring-webmvc:\[spring-version\]](https://search.maven.org/search?q=g:org.springframework%20AND%20a:spring-webmvc) -* Servlet API (choose one of the following, depending on your environment needs): - + Servlet 3+ API: [javax.servlet:javax.servlet-api:\[servlet-api-version\]](https://search.maven.org/search?q=g:javax.servlet%20AND%20a:javax.servlet-api) - + Servlet 2 API: [javax.servlet:servlet-api:\[servlet-2-api-version\]](https://search.maven.org/search?q=g:javax.servlet%20AND%20a:servlet-api) - +* Spring Web MVC: + [org.springframework:spring-webmvc:\[spring-version\]](https://search.maven.org/search?q=g:org.springframework%20AND%20a:spring-webmvc) +* Jakarta Servlet API: + [jakarta.servlet:jakarta.servlet-api:\[servlet-api-version\]](https://search.maven.org/search?q=g:jakarta.servlet%20AND%20a:jakarta.servlet-api) + ## More Info -See the [base project README.md](../README.md), [User Guide](../USER_GUIDE.md), and Backstopper repository source code and javadocs for all further information. +See the [base project README.md](../README.md), [User Guide](../USER_GUIDE.md), and Backstopper repository source code +and javadocs for all further information. ## License diff --git a/backstopper-spring-web-mvc/build.gradle b/backstopper-spring-web-mvc/build.gradle index b5fd1d7..1cb0dd5 100644 --- a/backstopper-spring-web-mvc/build.gradle +++ b/backstopper-spring-web-mvc/build.gradle @@ -1,14 +1,5 @@ evaluationDependsOn(':') -compileTestJava { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - -ext { - springSecurityVersion = '4.2.13.RELEASE' -} - dependencies { api( project(":backstopper-servlet-api"), @@ -17,8 +8,8 @@ dependencies { ) compileOnly( "org.jetbrains:annotations:$jetbrainsAnnotationsVersion", - "org.springframework:spring-webmvc:$spring4Version", - "javax.servlet:javax.servlet-api:$servletApiVersion", + "org.springframework:spring-webmvc:$spring6Version", + "jakarta.servlet:jakarta.servlet-api:$servletApiVersion", ) testImplementation( project(":backstopper-core").sourceSets.test.output, @@ -29,12 +20,11 @@ dependencies { "org.assertj:assertj-core:$assertJVersion", "com.tngtech.java:junit-dataprovider:$junitDataproviderVersion", "org.hamcrest:hamcrest-all:$hamcrestVersion", - "org.springframework:spring-test:$spring4Version", - "org.hibernate:hibernate-validator:$hibernateValidatorVersion", - "javax.servlet:javax.servlet-api:$servletApiVersion", - "org.springframework:spring-webmvc:$spring4Version", + "org.springframework:spring-test:$spring6Version", + "org.hibernate.validator:hibernate-validator:$hibernateValidatorVersion", + "org.glassfish.expressly:expressly:$glassfishExpresslyVersion", + "jakarta.servlet:jakarta.servlet-api:$servletApiVersion", + "org.springframework:spring-webmvc:$spring6Version", "org.springframework.security:spring-security-core:$springSecurityVersion", - // The jaxb-api is needed for building on the java 11 JDK as these classes were moved out of the Java SE libs. - "javax.xml.bind:jaxb-api:$jaxbApiVersion", ) } diff --git a/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandler.java b/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandler.java index 6d3f7f4..daf51a5 100644 --- a/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandler.java +++ b/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandler.java @@ -18,11 +18,11 @@ import java.util.Collection; -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Singleton; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; /** * An {@link ApiExceptionHandlerServletApiBase} extension that hooks into Spring Web MVC via its diff --git a/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerUtils.java b/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerUtils.java index 9459838..faf9f14 100644 --- a/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerUtils.java +++ b/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerUtils.java @@ -12,8 +12,8 @@ import java.util.Collection; -import javax.inject.Named; -import javax.inject.Singleton; +import jakarta.inject.Named; +import jakarta.inject.Singleton; /** * Similar to {@link com.nike.backstopper.handler.ApiExceptionHandlerUtils}, but provides helpers specific to this diff --git a/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/SpringContainerErrorController.java b/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/SpringContainerErrorController.java index 7cde97f..8b5df19 100644 --- a/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/SpringContainerErrorController.java +++ b/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/SpringContainerErrorController.java @@ -11,7 +11,7 @@ import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; -import javax.servlet.ServletRequest; +import jakarta.servlet.ServletRequest; import static com.nike.backstopper.handler.spring.SpringContainerErrorController.SpringbootErrorControllerIsNotOnClasspath; diff --git a/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/SpringUnhandledExceptionHandler.java b/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/SpringUnhandledExceptionHandler.java index f3b5df2..99db514 100644 --- a/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/SpringUnhandledExceptionHandler.java +++ b/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/SpringUnhandledExceptionHandler.java @@ -18,11 +18,11 @@ import java.util.Map; import java.util.Set; -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Singleton; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; /** * An extension of {@link UnhandledExceptionHandlerServletApiBase} that acts as a final catch-all exception handler. diff --git a/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/config/BackstopperSpringWebMvcConfig.java b/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/config/BackstopperSpringWebMvcConfig.java index abdcaef..9d07e24 100644 --- a/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/config/BackstopperSpringWebMvcConfig.java +++ b/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/config/BackstopperSpringWebMvcConfig.java @@ -18,7 +18,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -import javax.validation.Validator; +import jakarta.validation.Validator; /** * This Spring Web MVC configuration is an alternative to simply scanning all of {@code com.nike.backstopper}. You can diff --git a/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/listener/ApiExceptionHandlerListenerList.java b/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/listener/ApiExceptionHandlerListenerList.java index 8281494..f647d91 100644 --- a/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/listener/ApiExceptionHandlerListenerList.java +++ b/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/listener/ApiExceptionHandlerListenerList.java @@ -11,9 +11,9 @@ import java.util.Arrays; import java.util.List; -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Singleton; /** * Specifies the list of {@link ApiExceptionHandlerListener}s that should be available to diff --git a/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringWebMvcFrameworkExceptionHandlerListener.java b/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringWebMvcFrameworkExceptionHandlerListener.java index ff30ee1..9288f9f 100644 --- a/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringWebMvcFrameworkExceptionHandlerListener.java +++ b/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringWebMvcFrameworkExceptionHandlerListener.java @@ -18,9 +18,9 @@ import java.util.ArrayList; import java.util.List; -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Singleton; /** * An extension and concrete implementation of {@link OneOffSpringCommonFrameworkExceptionHandlerListener} that diff --git a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/apierror/testing/base/BaseSpringEnabledValidationTestCase.java b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/apierror/testing/base/BaseSpringEnabledValidationTestCase.java index 77a03e5..ba8f49d 100644 --- a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/apierror/testing/base/BaseSpringEnabledValidationTestCase.java +++ b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/apierror/testing/base/BaseSpringEnabledValidationTestCase.java @@ -18,6 +18,7 @@ import org.springframework.test.context.TestExecutionListeners; import org.springframework.test.context.junit4.AbstractJUnit4SpringContextTests; import org.springframework.test.context.support.AbstractTestExecutionListener; +import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; @@ -30,7 +31,7 @@ import java.util.Collections; import java.util.List; -import javax.inject.Inject; +import jakarta.inject.Inject; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; @@ -44,8 +45,11 @@ * * @author Nic Munroe */ -@TestExecutionListeners({BaseSpringEnabledValidationTestCase.LogBeforeClass.class, - BaseSpringEnabledValidationTestCase.LogAfterClass.class}) +@TestExecutionListeners({ + DependencyInjectionTestExecutionListener.class, // Needed for dependency injection of the test classes to happen. + BaseSpringEnabledValidationTestCase.LogBeforeClass.class, + BaseSpringEnabledValidationTestCase.LogAfterClass.class +}) @ContextConfiguration( classes = {TestCaseValidationSpringConfig.class} ) diff --git a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/apierror/testing/base/TestCaseValidationSpringConfig.java b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/apierror/testing/base/TestCaseValidationSpringConfig.java index 90f8ace..c50d50e 100644 --- a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/apierror/testing/base/TestCaseValidationSpringConfig.java +++ b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/apierror/testing/base/TestCaseValidationSpringConfig.java @@ -8,7 +8,7 @@ import com.nike.backstopper.handler.spring.config.BackstopperSpringWebMvcConfig; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.PropertyNamingStrategy; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; @@ -63,7 +63,7 @@ public RestTemplate restTemplate() { @Bean public ObjectMapper objectMapper() { ObjectMapper mapper = new ObjectMapper(); - mapper.setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES); + mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); return mapper; } @@ -74,7 +74,7 @@ public void configureMessageConverters(List> converters) converters.add(new StringHttpMessageConverter()); final MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); final ObjectMapper objectMapper = new ObjectMapper(); - objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES); + objectMapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); converter.setObjectMapper(objectMapper); converters.add(converter); super.configureMessageConverters(converters); diff --git a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/ClientfacingErrorITest.java b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/ClientfacingErrorITest.java index 984d9c5..a2ad088 100644 --- a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/ClientfacingErrorITest.java +++ b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/ClientfacingErrorITest.java @@ -42,10 +42,10 @@ import java.util.Arrays; import java.util.List; -import javax.inject.Inject; -import javax.validation.Valid; -import javax.validation.constraints.Min; -import javax.validation.constraints.Pattern; +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Pattern; import static java.util.Collections.singletonList; import static org.hamcrest.CoreMatchers.is; @@ -349,7 +349,7 @@ public void throwServerTimeoutException() { @RequestMapping("/throwServerUnknownHttpStatusCodeException") public void throwServerUnknownHttpStatusCodeException() { - UnknownHttpStatusCodeException serverResponseEx = new UnknownHttpStatusCodeException(42, null, null, null, null); + UnknownHttpStatusCodeException serverResponseEx = new UnknownHttpStatusCodeException(142, null, null, null, null); throw new ServerUnknownHttpStatusCodeException(new Exception("Intentional test exception"), "FOO", serverResponseEx, serverResponseEx.getRawStatusCode(), serverResponseEx.getResponseHeaders(), serverResponseEx.getResponseBodyAsString()); } diff --git a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerTest.java b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerTest.java index 2bcee34..6fd14ca 100644 --- a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerTest.java +++ b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerTest.java @@ -13,11 +13,12 @@ import java.util.Collections; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Matchers.any; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; @@ -50,13 +51,13 @@ public void beforeMethod() { public void resolveException_returns_null_if_maybeHandleException_returns_null() throws UnexpectedMajorExceptionHandlingError { // given - doReturn(null).when(handlerSpy).maybeHandleException(any(Throwable.class), any(HttpServletRequest.class), any(HttpServletResponse.class)); + doReturn(null).when(handlerSpy).maybeHandleException(isNull(), isNull(), isNull()); // when ModelAndView result = handlerSpy.resolveException(null, null, null, null); // then - verify(handlerSpy).maybeHandleException(any(Throwable.class), any(HttpServletRequest.class), any(HttpServletResponse.class)); + verify(handlerSpy).maybeHandleException(isNull(), isNull(), isNull()); assertThat(result).isNull(); } @@ -65,13 +66,13 @@ public void resolveException_returns_null_if_maybeHandleException_throws_Unexpec throws UnexpectedMajorExceptionHandlingError { // given doThrow(new UnexpectedMajorExceptionHandlingError("foo", null)) - .when(handlerSpy).maybeHandleException(any(Throwable.class), any(HttpServletRequest.class), any(HttpServletResponse.class)); + .when(handlerSpy).maybeHandleException(isNull(), isNull(), isNull()); // when ModelAndView result = handlerSpy.resolveException(null, null, null, null); // then - verify(handlerSpy).maybeHandleException(any(Throwable.class), any(HttpServletRequest.class), any(HttpServletResponse.class)); + verify(handlerSpy).maybeHandleException(isNull(), isNull(), isNull()); assertThat(result).isNull(); } diff --git a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerUtilsTest.java b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerUtilsTest.java index 7de74a5..e236ceb 100644 --- a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerUtilsTest.java +++ b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerUtilsTest.java @@ -15,7 +15,7 @@ import java.util.Arrays; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletResponse; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; diff --git a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringContainerErrorControllerTest.java b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringContainerErrorControllerTest.java index df96055..29fa22d 100644 --- a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringContainerErrorControllerTest.java +++ b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringContainerErrorControllerTest.java @@ -6,7 +6,7 @@ import org.junit.Before; import org.junit.Test; -import javax.servlet.ServletRequest; +import jakarta.servlet.ServletRequest; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; diff --git a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringUnhandledExceptionHandlerTest.java b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringUnhandledExceptionHandlerTest.java index 20298bf..350a17f 100644 --- a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringUnhandledExceptionHandlerTest.java +++ b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringUnhandledExceptionHandlerTest.java @@ -25,8 +25,8 @@ import java.util.Map; import java.util.UUID; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; diff --git a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringWebMvcFrameworkExceptionHandlerListenerTest.java b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringWebMvcFrameworkExceptionHandlerListenerTest.java index 5d993f8..c2967db 100644 --- a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringWebMvcFrameworkExceptionHandlerListenerTest.java +++ b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringWebMvcFrameworkExceptionHandlerListenerTest.java @@ -154,7 +154,7 @@ public void shouldHandleException_returns_MALFORMED_REQUEST_for_ServletRequestBi String expectedExceptionMessage = (isMissingRequestParamEx) - ? String.format("Required %s parameter '%s' is not present", missingParamType, missingParamName) + ? String.format("Required request parameter '%s' for method parameter type %s is not present", missingParamName, missingParamType) : "foo"; // when diff --git a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/service/FailFastServersideValidationServiceITest.java b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/service/FailFastServersideValidationServiceITest.java index e68d851..bfe97fc 100644 --- a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/service/FailFastServersideValidationServiceITest.java +++ b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/service/FailFastServersideValidationServiceITest.java @@ -22,9 +22,9 @@ import java.io.Serializable; import java.util.Arrays; -import javax.inject.Inject; -import javax.validation.Valid; -import javax.validation.constraints.NotNull; +import jakarta.inject.Inject; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; diff --git a/build.gradle b/build.gradle index 381fc25..3086ed4 100644 --- a/build.gradle +++ b/build.gradle @@ -87,7 +87,7 @@ ext { jakartaValidationVersion = '3.0.2' servletApiVersion = '6.0.0' // Compatible with Jakarta EE 10 spring6Version = '6.0.23' // Compatible with Jakarta EE 9/10 - springSecurityVersion = '6.1.9' // Closest spring secrity version to our pinned spring6Version, but without going over to avoid versions being bumped above what we want for testing. + springSecurityVersion = '6.1.9' // Closest spring secrity version to our pinned spring6Version, but without going over to avoid versions being transitively bumped above what we want for testing. springboot1Version = '1.5.2.RELEASE' springboot2Version = '2.6.3' jersey1Version = '1.19.2' diff --git a/settings.gradle b/settings.gradle index d3d94a6..16166a7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -7,8 +7,8 @@ include "nike-internal-util", "backstopper-reusable-tests-junit5", "backstopper-jackson", "backstopper-servlet-api", - "backstopper-spring-web" -// "backstopper-spring-web-mvc", + "backstopper-spring-web", + "backstopper-spring-web-mvc" // "backstopper-spring-web-flux", // "backstopper-spring-boot1", // "backstopper-spring-boot2-webmvc", From 45f600f170e7921680823160cd4ff17854b86e0d Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Mon, 9 Sep 2024 18:42:11 -0700 Subject: [PATCH 11/42] Delete jaxrs and jersey support --- README.md | 4 +- backstopper-jaxrs/README.md | 33 -- backstopper-jaxrs/build.gradle | 28 -- .../jaxrs/JaxRsApiExceptionHandler.java | 150 ------ .../jaxrs/JaxRsUnhandledExceptionHandler.java | 76 --- .../JaxRsApiExceptionHandlerListenerList.java | 52 -- ...ebApplicationExceptionHandlerListener.java | 101 ---- .../jaxrs/JaxRsApiExceptionHandlerTest.java | 228 --------- .../JaxRsUnhandledExceptionHandlerTest.java | 83 ---- ...plicationExceptionHandlerListenerTest.java | 140 ------ backstopper-jersey1/README.md | 45 -- backstopper-jersey1/build.gradle | 28 -- .../jersey1/Jersey1ApiExceptionHandler.java | 110 ----- .../Jersey1UnhandledExceptionHandler.java | 71 --- .../Jersey1BackstopperConfigHelper.java | 47 -- ...ebApplicationExceptionHandlerListener.java | 109 ----- .../Jersey1ApiExceptionHandlerTest.java | 181 ------- .../Jersey1UnhandledExceptionHandlerTest.java | 82 ---- ...plicationExceptionHandlerListenerTest.java | 142 ------ backstopper-jersey2/README.md | 45 -- backstopper-jersey2/build.gradle | 27 - .../jersey2/Jersey2ApiExceptionHandler.java | 33 -- .../Jersey2BackstopperConfigHelper.java | 214 -------- ...ebApplicationExceptionHandlerListener.java | 80 --- .../Jersey2ApiExceptionHandlerTest.java | 181 ------- .../Jersey2BackstopperConfigHelperTest.java | 190 -------- ...plicationExceptionHandlerListenerTest.java | 121 ----- samples/sample-jersey1/README.md | 42 -- samples/sample-jersey1/build.gradle | 40 -- samples/sample-jersey1/buildSample.sh | 2 - samples/sample-jersey1/runSample.sh | 3 - .../nike/backstopper/jersey1sample/Main.java | 57 --- .../config/Jersey1SampleConfigHelper.java | 49 -- .../error/SampleProjectApiError.java | 91 ---- .../error/SampleProjectApiErrorsImpl.java | 41 -- .../jersey1sample/model/RgbColor.java | 27 - .../jersey1sample/model/SampleModel.java | 51 -- .../resource/SampleResource.java | 123 ----- .../src/main/resources/logback.xml | 11 - .../ApplicationJsr303AnnotationTroller.java | 46 -- .../VerifyJsr303ContractTest.java | 32 -- ...rtsToClassTypeAnnotationsAreValidTest.java | 23 - ...xpectedErrorsAreReturnedComponentTest.java | 424 ---------------- .../error/SampleProjectApiErrorsImplTest.java | 20 - samples/sample-jersey2/README.md | 47 -- samples/sample-jersey2/build.gradle | 43 -- samples/sample-jersey2/buildSample.sh | 2 - samples/sample-jersey2/runSample.sh | 3 - .../nike/backstopper/jersey2sample/Main.java | 49 -- .../config/Jersey2SampleResourceConfig.java | 52 -- .../error/SampleProjectApiError.java | 91 ---- .../error/SampleProjectApiErrorsImpl.java | 41 -- .../jersey2sample/model/RgbColor.java | 27 - .../jersey2sample/model/SampleModel.java | 51 -- .../resource/SampleResource.java | 154 ------ .../src/main/resources/logback.xml | 11 - .../ApplicationJsr303AnnotationTroller.java | 46 -- .../VerifyJsr303ContractTest.java | 32 -- ...rtsToClassTypeAnnotationsAreValidTest.java | 23 - ...xpectedErrorsAreReturnedComponentTest.java | 460 ------------------ .../error/SampleProjectApiErrorsImplTest.java | 20 - settings.gradle | 7 +- 62 files changed, 4 insertions(+), 4838 deletions(-) delete mode 100644 backstopper-jaxrs/README.md delete mode 100644 backstopper-jaxrs/build.gradle delete mode 100644 backstopper-jaxrs/src/main/java/com/nike/backstopper/handler/jaxrs/JaxRsApiExceptionHandler.java delete mode 100644 backstopper-jaxrs/src/main/java/com/nike/backstopper/handler/jaxrs/JaxRsUnhandledExceptionHandler.java delete mode 100644 backstopper-jaxrs/src/main/java/com/nike/backstopper/handler/jaxrs/config/JaxRsApiExceptionHandlerListenerList.java delete mode 100644 backstopper-jaxrs/src/main/java/com/nike/backstopper/handler/jaxrs/listener/impl/JaxRsWebApplicationExceptionHandlerListener.java delete mode 100644 backstopper-jaxrs/src/test/java/com/nike/backstopper/handler/jaxrs/JaxRsApiExceptionHandlerTest.java delete mode 100644 backstopper-jaxrs/src/test/java/com/nike/backstopper/handler/jaxrs/JaxRsUnhandledExceptionHandlerTest.java delete mode 100644 backstopper-jaxrs/src/test/java/com/nike/backstopper/handler/jaxrs/listener/impl/JaxRsWebApplicationExceptionHandlerListenerTest.java delete mode 100644 backstopper-jersey1/README.md delete mode 100644 backstopper-jersey1/build.gradle delete mode 100644 backstopper-jersey1/src/main/java/com/nike/backstopper/handler/jersey1/Jersey1ApiExceptionHandler.java delete mode 100644 backstopper-jersey1/src/main/java/com/nike/backstopper/handler/jersey1/Jersey1UnhandledExceptionHandler.java delete mode 100644 backstopper-jersey1/src/main/java/com/nike/backstopper/handler/jersey1/config/Jersey1BackstopperConfigHelper.java delete mode 100644 backstopper-jersey1/src/main/java/com/nike/backstopper/handler/jersey1/listener/impl/Jersey1WebApplicationExceptionHandlerListener.java delete mode 100644 backstopper-jersey1/src/test/java/com/nike/backstopper/handler/jersey1/Jersey1ApiExceptionHandlerTest.java delete mode 100644 backstopper-jersey1/src/test/java/com/nike/backstopper/handler/jersey1/Jersey1UnhandledExceptionHandlerTest.java delete mode 100644 backstopper-jersey1/src/test/java/com/nike/backstopper/handler/jersey1/listener/impl/Jersey1WebApplicationExceptionHandlerListenerTest.java delete mode 100644 backstopper-jersey2/README.md delete mode 100644 backstopper-jersey2/build.gradle delete mode 100644 backstopper-jersey2/src/main/java/com/nike/backstopper/handler/jersey2/Jersey2ApiExceptionHandler.java delete mode 100644 backstopper-jersey2/src/main/java/com/nike/backstopper/handler/jersey2/config/Jersey2BackstopperConfigHelper.java delete mode 100644 backstopper-jersey2/src/main/java/com/nike/backstopper/handler/jersey2/listener/impl/Jersey2WebApplicationExceptionHandlerListener.java delete mode 100644 backstopper-jersey2/src/test/java/com/nike/backstopper/handler/jersey2/Jersey2ApiExceptionHandlerTest.java delete mode 100644 backstopper-jersey2/src/test/java/com/nike/backstopper/handler/jersey2/config/Jersey2BackstopperConfigHelperTest.java delete mode 100644 backstopper-jersey2/src/test/java/com/nike/backstopper/handler/jersey2/listener/impl/Jersey2WebApplicationExceptionHandlerListenerTest.java delete mode 100644 samples/sample-jersey1/README.md delete mode 100644 samples/sample-jersey1/build.gradle delete mode 100755 samples/sample-jersey1/buildSample.sh delete mode 100755 samples/sample-jersey1/runSample.sh delete mode 100644 samples/sample-jersey1/src/main/java/com/nike/backstopper/jersey1sample/Main.java delete mode 100644 samples/sample-jersey1/src/main/java/com/nike/backstopper/jersey1sample/config/Jersey1SampleConfigHelper.java delete mode 100644 samples/sample-jersey1/src/main/java/com/nike/backstopper/jersey1sample/error/SampleProjectApiError.java delete mode 100644 samples/sample-jersey1/src/main/java/com/nike/backstopper/jersey1sample/error/SampleProjectApiErrorsImpl.java delete mode 100644 samples/sample-jersey1/src/main/java/com/nike/backstopper/jersey1sample/model/RgbColor.java delete mode 100644 samples/sample-jersey1/src/main/java/com/nike/backstopper/jersey1sample/model/SampleModel.java delete mode 100644 samples/sample-jersey1/src/main/java/com/nike/backstopper/jersey1sample/resource/SampleResource.java delete mode 100644 samples/sample-jersey1/src/main/resources/logback.xml delete mode 100644 samples/sample-jersey1/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/ApplicationJsr303AnnotationTroller.java delete mode 100644 samples/sample-jersey1/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyJsr303ContractTest.java delete mode 100644 samples/sample-jersey1/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyStringConvertsToClassTypeAnnotationsAreValidTest.java delete mode 100644 samples/sample-jersey1/src/test/java/com/nike/backstopper/jersey1sample/componenttest/VerifyExpectedErrorsAreReturnedComponentTest.java delete mode 100644 samples/sample-jersey1/src/test/java/com/nike/backstopper/jersey1sample/error/SampleProjectApiErrorsImplTest.java delete mode 100644 samples/sample-jersey2/README.md delete mode 100644 samples/sample-jersey2/build.gradle delete mode 100755 samples/sample-jersey2/buildSample.sh delete mode 100755 samples/sample-jersey2/runSample.sh delete mode 100644 samples/sample-jersey2/src/main/java/com/nike/backstopper/jersey2sample/Main.java delete mode 100644 samples/sample-jersey2/src/main/java/com/nike/backstopper/jersey2sample/config/Jersey2SampleResourceConfig.java delete mode 100644 samples/sample-jersey2/src/main/java/com/nike/backstopper/jersey2sample/error/SampleProjectApiError.java delete mode 100644 samples/sample-jersey2/src/main/java/com/nike/backstopper/jersey2sample/error/SampleProjectApiErrorsImpl.java delete mode 100644 samples/sample-jersey2/src/main/java/com/nike/backstopper/jersey2sample/model/RgbColor.java delete mode 100644 samples/sample-jersey2/src/main/java/com/nike/backstopper/jersey2sample/model/SampleModel.java delete mode 100644 samples/sample-jersey2/src/main/java/com/nike/backstopper/jersey2sample/resource/SampleResource.java delete mode 100644 samples/sample-jersey2/src/main/resources/logback.xml delete mode 100644 samples/sample-jersey2/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/ApplicationJsr303AnnotationTroller.java delete mode 100644 samples/sample-jersey2/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyJsr303ContractTest.java delete mode 100644 samples/sample-jersey2/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyStringConvertsToClassTypeAnnotationsAreValidTest.java delete mode 100644 samples/sample-jersey2/src/test/java/com/nike/backstopper/jersey2sample/componenttest/VerifyExpectedErrorsAreReturnedComponentTest.java delete mode 100644 samples/sample-jersey2/src/test/java/com/nike/backstopper/jersey2sample/error/SampleProjectApiErrorsImplTest.java diff --git a/README.md b/README.md index b9a2d61..4d93c43 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,9 @@ throw ApiException.newBuilder() ### Framework-Specific Modules - + +// TODO javax-to-jakarta: Explain why jaxrs and jersey3 are not currently supported, but could be if there's interest. + * [backstopper-jaxrs](backstopper-jaxrs) - Integration library for JAX-RS. If you want to integrate Backstopper into a JAX-RS project other than Jersey then start here (see below for the Jersey-specific modules). * [backstopper-jersey1](backstopper-jersey1/) - Integration library for the Jersey 1 framework. If you want to integrate Backstopper into a project running in Jersey 1 then start here. There is a [Jersey 1 sample project](samples/sample-jersey1/) complete with integration tests you can use as an example. * [backstopper-jersey2](backstopper-jersey2/) - Integration library for the Jersey 2 framework. If you want to integrate Backstopper into a project running in Jersey 2 then start here. There is a [Jersey 2 sample project](samples/sample-jersey2/) complete with integration tests you can use as an example. diff --git a/backstopper-jaxrs/README.md b/backstopper-jaxrs/README.md deleted file mode 100644 index e940521..0000000 --- a/backstopper-jaxrs/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# Backstopper - JAX-RS - -Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 7 and greater. - -This readme focuses specifically on the integration of Backstopper in non-Jersey JAX-RS applications (there are [Jersey 1](../backstopper-jersey1) and [Jersey 2](../backstopper-jersey2) specific modules for applications in those environments). If you are looking for a different framework integration check out the [relevant section](../README.md#framework_modules) of the base readme to see if one already exists. The [base project README.md](../README.md) and [User Guide](../USER_GUIDE.md) contain the main bulk of information regarding Backstopper. - -## Setup - -The `JaxRsApiExceptionHandler` is annotated as a JAX-RS `@Provider`, so configures itself as an `ExceptionHandler` automatically. Note that this should be the *only* `ExceptionHandler` in your application for Backstopper to work properly. - -## NOTE - JAX-RS and Servlet API dependencies required at runtime - -This `backstopper-jaxrs` module does not export any transitive JAX-RS or Servlet API dependencies to prevent runtime -version conflicts with whatever JAX-RS and Servlet environment you deploy to. - -This should not affect most users since this library is likely to be used in a JAX-RS/Servlet environment where the -required dependencies are already on the classpath at runtime, however if you receive class-not-found errors related to -JAX-RS or Servlet API classes then you'll need to pull the necessary dependency into your project. - -The dependencies you may need to pull in: - -* JAX-RS: [javax.ws.rs:javax.ws.rs-api:\[jax-rs-version\]](https://search.maven.org/search?q=g:javax.ws.rs%20AND%20a:javax.ws.rs-api) -* Servlet API (choose one of the following, depending on your environment needs): - + Servlet 3+ API: [javax.servlet:javax.servlet-api:\[servlet-api-version\]](https://search.maven.org/search?q=g:javax.servlet%20AND%20a:javax.servlet-api) - + Servlet 2 API: [javax.servlet:servlet-api:\[servlet-2-api-version\]](https://search.maven.org/search?q=g:javax.servlet%20AND%20a:servlet-api) - -## More Info - -See the [base project README.md](../README.md), [User Guide](../USER_GUIDE.md), and Backstopper repository source code and javadocs for all further information. - -## License - -Backstopper is released under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/backstopper-jaxrs/build.gradle b/backstopper-jaxrs/build.gradle deleted file mode 100644 index 9a06eb9..0000000 --- a/backstopper-jaxrs/build.gradle +++ /dev/null @@ -1,28 +0,0 @@ -evaluationDependsOn(':') - -compileTestJava { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - -dependencies { - api( - project(":backstopper-servlet-api"), - project(":backstopper-jackson"), - ) - compileOnly( - "javax.ws.rs:javax.ws.rs-api:$jaxRsVersion", - "javax.servlet:javax.servlet-api:$servletApiVersion", - ) - testImplementation( - project(":backstopper-core").sourceSets.test.output, - "junit:junit:$junitVersion", - "org.mockito:mockito-core:$mockitoVersion", - "ch.qos.logback:logback-classic:$logbackVersion", - "org.assertj:assertj-core:$assertJVersion", - "com.tngtech.java:junit-dataprovider:$junitDataproviderVersion", - "org.hamcrest:hamcrest-all:$hamcrestVersion", - "org.glassfish.jersey.core:jersey-server:$jersey2Version", - "javax.servlet:javax.servlet-api:$servletApiVersion", - ) -} diff --git a/backstopper-jaxrs/src/main/java/com/nike/backstopper/handler/jaxrs/JaxRsApiExceptionHandler.java b/backstopper-jaxrs/src/main/java/com/nike/backstopper/handler/jaxrs/JaxRsApiExceptionHandler.java deleted file mode 100644 index 519502c..0000000 --- a/backstopper-jaxrs/src/main/java/com/nike/backstopper/handler/jaxrs/JaxRsApiExceptionHandler.java +++ /dev/null @@ -1,150 +0,0 @@ -package com.nike.backstopper.handler.jaxrs; - -import com.nike.backstopper.apierror.ApiError; -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.handler.ApiExceptionHandlerServletApiBase; -import com.nike.backstopper.handler.ApiExceptionHandlerUtils; -import com.nike.backstopper.handler.ErrorResponseInfo; -import com.nike.backstopper.handler.RequestInfoForLogging; -import com.nike.backstopper.handler.UnexpectedMajorExceptionHandlingError; -import com.nike.backstopper.handler.jaxrs.config.JaxRsApiExceptionHandlerListenerList; -import com.nike.backstopper.handler.listener.ApiExceptionHandlerListener; -import com.nike.backstopper.model.DefaultErrorContractDTO; -import com.nike.backstopper.model.util.JsonUtilWithDefaultErrorContractDTOSupport; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Collection; -import java.util.List; - -import javax.inject.Inject; -import javax.inject.Singleton; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import javax.ws.rs.ext.ExceptionMapper; -import javax.ws.rs.ext.Provider; - -/** - * An {@link ApiExceptionHandlerServletApiBase} extension that hooks into JAX-RS via its - * {@link ExceptionMapper} implementation, specifically {@link ExceptionMapper#toResponse(Throwable)}. - * - *

Any errors not handled here are things we don't know how to deal with and will fall through to {@link - * JaxRsUnhandledExceptionHandler}. - * - *

It is expected and recommended that this handler (or an extension of it) is the sole {@link ExceptionMapper} for - * the application. - * - * @author Michael Irwin - */ -@Provider -@Singleton -public class JaxRsApiExceptionHandler extends ApiExceptionHandlerServletApiBase - implements ExceptionMapper { - - private final Logger logger = LoggerFactory.getLogger(this.getClass()); - - /** - * The {@link JaxRsUnhandledExceptionHandler} that handles any - * exceptions we weren't anticipating - */ - @SuppressWarnings("WeakerAccess") - protected final JaxRsUnhandledExceptionHandler jerseyUnhandledExceptionHandler; - - @Context - protected HttpServletRequest request; - - @Context - protected HttpServletResponse response; - - @Inject - public JaxRsApiExceptionHandler(ProjectApiErrors projectApiErrors, - JaxRsApiExceptionHandlerListenerList apiExceptionHandlerListeners, - ApiExceptionHandlerUtils apiExceptionHandlerUtils, - JaxRsUnhandledExceptionHandler jerseyUnhandledExceptionHandler) { - - this(projectApiErrors, apiExceptionHandlerListeners.listeners, apiExceptionHandlerUtils, jerseyUnhandledExceptionHandler); - } - - public JaxRsApiExceptionHandler(ProjectApiErrors projectApiErrors, - List apiExceptionHandlerListeners, - ApiExceptionHandlerUtils apiExceptionHandlerUtils, - JaxRsUnhandledExceptionHandler jerseyUnhandledExceptionHandler) { - - super(projectApiErrors, apiExceptionHandlerListeners, apiExceptionHandlerUtils); - - if (jerseyUnhandledExceptionHandler == null) - throw new IllegalArgumentException("jerseyUnhandledExceptionHandler cannot be null"); - - this.jerseyUnhandledExceptionHandler = jerseyUnhandledExceptionHandler; - } - - @Override - public Response.ResponseBuilder prepareFrameworkRepresentation( - DefaultErrorContractDTO errorContractDTO, int httpStatusCode, Collection rawFilteredApiErrors, - Throwable originalException, RequestInfoForLogging request - ) { - return Response.status(httpStatusCode).entity( - JsonUtilWithDefaultErrorContractDTOSupport.writeValueAsString(errorContractDTO)); - } - - @Override - public Response toResponse(Throwable e) { - - ErrorResponseInfo exceptionHandled; - - try { - exceptionHandled = maybeHandleException(e, request, response); - - if (exceptionHandled == null) { - if (logger.isDebugEnabled()) { - logger.debug("No suitable handlers found for exception=" + e.getMessage() + ". " - + JaxRsUnhandledExceptionHandler.class.getName() + " should handle it."); - } - exceptionHandled = jerseyUnhandledExceptionHandler.handleException(e, request, response); - } - - } - catch (UnexpectedMajorExceptionHandlingError ohNoException) { - logger.error("Unexpected major error while handling exception. " + - JaxRsUnhandledExceptionHandler.class.getName() + " should handle it.", ohNoException); - exceptionHandled = jerseyUnhandledExceptionHandler.handleException(e, request, response); - } - - Response.ResponseBuilder responseBuilder = setContentType(exceptionHandled, request, response, e); - - // NOTE: We don't have to add headers to the response here - it's already been done in the - // ApiExceptionHandlerServletApiBase.processServletResponse(...) method. - - return responseBuilder.build(); - } - - /** - * Sets the {@code Content-Type} header on the given {@link ErrorResponseInfo#frameworkRepresentationObj} - * (which is a {@link javax.ws.rs.core.Response.ResponseBuilder}) and then returns it. Defaults to {@link - * MediaType#APPLICATION_JSON}. If you need a different content type for your response you can override this method - * to do something else. - * - * @param errorResponseInfo The {@link ErrorResponseInfo} that was generated for this request. The expectation is - * that any override of this method will {@code return errorResponseInfo.frameworkRepresentationObj.header(...)} - * to set the {@code Content-Type} header for the response. - * @param request The {@link HttpServletRequest} for this request - useful if you need it to help determine the - * {@code Content-Type} for the response. - * @param response The {@link HttpServletResponse} for this request - useful if you need it to help determine the - * {@code Content-Type} for the response. - * @param ex The exception that backstopper is handling - useful if you need it to help determine the - * {@code Content-Type} for the response. - * @return The given {@link ErrorResponseInfo}'s {@link ErrorResponseInfo#frameworkRepresentationObj} after setting - * the {@code Content-Type} header on it - defaults to {@link MediaType#APPLICATION_JSON}. Override this method - * if you need to send a different {@code Content-Type}. - */ - protected Response.ResponseBuilder setContentType( - ErrorResponseInfo errorResponseInfo, HttpServletRequest request, - HttpServletResponse response, Throwable ex - ) { - return errorResponseInfo.frameworkRepresentationObj.header("Content-Type", MediaType.APPLICATION_JSON); - } -} diff --git a/backstopper-jaxrs/src/main/java/com/nike/backstopper/handler/jaxrs/JaxRsUnhandledExceptionHandler.java b/backstopper-jaxrs/src/main/java/com/nike/backstopper/handler/jaxrs/JaxRsUnhandledExceptionHandler.java deleted file mode 100644 index 6c4d900..0000000 --- a/backstopper-jaxrs/src/main/java/com/nike/backstopper/handler/jaxrs/JaxRsUnhandledExceptionHandler.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.nike.backstopper.handler.jaxrs; - -import com.nike.backstopper.apierror.ApiError; -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.handler.ApiExceptionHandlerUtils; -import com.nike.backstopper.handler.ErrorResponseInfo; -import com.nike.backstopper.handler.RequestInfoForLogging; -import com.nike.backstopper.handler.UnhandledExceptionHandlerServletApiBase; -import com.nike.backstopper.model.DefaultErrorContractDTO; -import com.nike.backstopper.model.util.JsonUtilWithDefaultErrorContractDTOSupport; - -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import javax.inject.Inject; -import javax.inject.Singleton; -import javax.ws.rs.core.Response; - -/** - * An extension of {@link UnhandledExceptionHandlerServletApiBase} that acts as a final catch-all exception handler. - * Translates *all* exceptions to a {@link ProjectApiErrors#getGenericServiceError()}, which is then converted - * to a {@link Response.ResponseBuilder} for the caller with a response entity payload built by - * {@link JsonUtilWithDefaultErrorContractDTOSupport#writeValueAsString(Object)}. - * - * @author dsand7 - * @author Michael Irwin - */ -@Singleton -public class JaxRsUnhandledExceptionHandler extends UnhandledExceptionHandlerServletApiBase { - - protected final Set singletonGenericServiceError; - protected final int genericServiceErrorHttpStatusCode; - - /** - * Creates a new instance with the given arguments. - * - * @param projectApiErrors The {@link ProjectApiErrors} used for this project - cannot be null. - * @param utils The {@link ApiExceptionHandlerUtils} that should be used by this instance. You can pass - * in {@link ApiExceptionHandlerUtils#DEFAULT_IMPL} if you don't need custom logic. - */ - @Inject - public JaxRsUnhandledExceptionHandler(ProjectApiErrors projectApiErrors, ApiExceptionHandlerUtils utils) { - super(projectApiErrors, utils); - this.singletonGenericServiceError = Collections.singleton(projectApiErrors.getGenericServiceError()); - this.genericServiceErrorHttpStatusCode = projectApiErrors.getGenericServiceError().getHttpStatusCode(); - } - - @Override - protected Response.ResponseBuilder prepareFrameworkRepresentation(DefaultErrorContractDTO errorContractDTO, - int httpStatusCode, - Collection rawFilteredApiErrors, - Throwable originalException, - RequestInfoForLogging request) { - - return Response.status(httpStatusCode).entity( - JsonUtilWithDefaultErrorContractDTOSupport.writeValueAsString(errorContractDTO)); - } - - @Override - protected ErrorResponseInfo generateLastDitchFallbackErrorResponseInfo(Throwable ex, - RequestInfoForLogging request, - String errorUid, - Map> headersForResponseWithErrorUid) { - DefaultErrorContractDTO errorContract = new DefaultErrorContractDTO(errorUid, singletonGenericServiceError); - return new ErrorResponseInfo<>( - genericServiceErrorHttpStatusCode, - Response.status(genericServiceErrorHttpStatusCode).entity( - JsonUtilWithDefaultErrorContractDTOSupport.writeValueAsString(errorContract) - ), - headersForResponseWithErrorUid - ); - } -} diff --git a/backstopper-jaxrs/src/main/java/com/nike/backstopper/handler/jaxrs/config/JaxRsApiExceptionHandlerListenerList.java b/backstopper-jaxrs/src/main/java/com/nike/backstopper/handler/jaxrs/config/JaxRsApiExceptionHandlerListenerList.java deleted file mode 100644 index 45b0a5a..0000000 --- a/backstopper-jaxrs/src/main/java/com/nike/backstopper/handler/jaxrs/config/JaxRsApiExceptionHandlerListenerList.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.nike.backstopper.handler.jaxrs.config; - -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.handler.ApiExceptionHandlerUtils; -import com.nike.backstopper.handler.jaxrs.listener.impl.JaxRsWebApplicationExceptionHandlerListener; -import com.nike.backstopper.handler.listener.ApiExceptionHandlerListener; -import com.nike.backstopper.handler.listener.impl.ClientDataValidationErrorHandlerListener; -import com.nike.backstopper.handler.listener.impl.DownstreamNetworkExceptionHandlerListener; -import com.nike.backstopper.handler.listener.impl.GenericApiExceptionHandlerListener; -import com.nike.backstopper.handler.listener.impl.ServersideValidationErrorHandlerListener; - -import java.util.Arrays; -import java.util.List; - -import javax.inject.Inject; -import javax.inject.Singleton; - -/** - * Collection {@link Singleton} class that provides a collection of default {@link ApiExceptionHandlerListener}s. - * - * @author Michael Irwin - */ -@Singleton -public class JaxRsApiExceptionHandlerListenerList { - - public final List listeners; - - @Inject - @SuppressWarnings("unused") - public JaxRsApiExceptionHandlerListenerList(ProjectApiErrors projectApiErrors, ApiExceptionHandlerUtils utils) { - this(defaultApiExceptionHandlerListeners(projectApiErrors, utils)); - } - - public JaxRsApiExceptionHandlerListenerList(List listeners) { - this.listeners = listeners; - } - - /** - * @return The basic set of handler listeners that are appropriate for most JAX-RS applications. - */ - public static List defaultApiExceptionHandlerListeners( - ProjectApiErrors projectApiErrors, ApiExceptionHandlerUtils utils - ) { - return Arrays.asList( - new GenericApiExceptionHandlerListener(), - new ServersideValidationErrorHandlerListener(projectApiErrors, utils), - new ClientDataValidationErrorHandlerListener(projectApiErrors, utils), - new DownstreamNetworkExceptionHandlerListener(projectApiErrors), - new JaxRsWebApplicationExceptionHandlerListener(projectApiErrors, utils)); - } - -} diff --git a/backstopper-jaxrs/src/main/java/com/nike/backstopper/handler/jaxrs/listener/impl/JaxRsWebApplicationExceptionHandlerListener.java b/backstopper-jaxrs/src/main/java/com/nike/backstopper/handler/jaxrs/listener/impl/JaxRsWebApplicationExceptionHandlerListener.java deleted file mode 100644 index eb12efd..0000000 --- a/backstopper-jaxrs/src/main/java/com/nike/backstopper/handler/jaxrs/listener/impl/JaxRsWebApplicationExceptionHandlerListener.java +++ /dev/null @@ -1,101 +0,0 @@ -package com.nike.backstopper.handler.jaxrs.listener.impl; - -import com.nike.backstopper.apierror.ApiError; -import com.nike.backstopper.apierror.SortedApiErrorSet; -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.handler.ApiExceptionHandlerUtils; -import com.nike.backstopper.handler.listener.ApiExceptionHandlerListener; -import com.nike.backstopper.handler.listener.ApiExceptionHandlerListenerResult; -import com.nike.internal.util.Pair; - -import com.fasterxml.jackson.core.JsonProcessingException; - -import java.util.ArrayList; -import java.util.List; - -import javax.inject.Inject; -import javax.inject.Singleton; -import javax.servlet.http.HttpServletResponse; -import javax.ws.rs.NotFoundException; -import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.Response; - -import static com.nike.backstopper.apierror.SortedApiErrorSet.singletonSortedSetOf; - -/** - * Handles any known errors thrown by the JAX-RS framework. - * - * @author dsand7 - * @author Michael Irwin - */ -@Singleton -@SuppressWarnings("WeakerAccess") -public class JaxRsWebApplicationExceptionHandlerListener implements ApiExceptionHandlerListener { - - protected final ProjectApiErrors projectApiErrors; - protected final ApiExceptionHandlerUtils utils; - - /** - * @param projectApiErrors The {@link ProjectApiErrors} that should be used by this instance when finding {@link - * ApiError}s. Cannot be null. - * @param utils The {@link ApiExceptionHandlerUtils} that should be used by this instance. - */ - @Inject - public JaxRsWebApplicationExceptionHandlerListener(ProjectApiErrors projectApiErrors, - ApiExceptionHandlerUtils utils) { - if (projectApiErrors == null) - throw new IllegalArgumentException("ProjectApiErrors cannot be null"); - - if (utils == null) - throw new IllegalArgumentException("ApiExceptionHandlerUtils cannot be null"); - - this.projectApiErrors = projectApiErrors; - this.utils = utils; - } - - @Override - public ApiExceptionHandlerListenerResult shouldHandleException(Throwable ex) { - - ApiExceptionHandlerListenerResult result; - SortedApiErrorSet handledErrors = null; - List> extraDetailsForLogging = new ArrayList<>(); - - if (ex instanceof NotFoundException) { - handledErrors = singletonSortedSetOf(projectApiErrors.getNotFoundApiError()); - } - else if (ex instanceof WebApplicationException) { - utils.addBaseExceptionMessageToExtraDetailsForLogging(ex, extraDetailsForLogging); - WebApplicationException webex = (WebApplicationException) ex; - Response webExResponse = webex.getResponse(); - if (webExResponse != null) { - int webExStatusCode = webExResponse.getStatus(); - if (webExStatusCode == HttpServletResponse.SC_NOT_ACCEPTABLE) { - handledErrors = singletonSortedSetOf(projectApiErrors.getNoAcceptableRepresentationApiError()); - } - else if (webExStatusCode == HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE) { - handledErrors = singletonSortedSetOf(projectApiErrors.getUnsupportedMediaTypeApiError()); - } - else if (webExStatusCode == HttpServletResponse.SC_METHOD_NOT_ALLOWED) { - handledErrors = singletonSortedSetOf(projectApiErrors.getMethodNotAllowedApiError()); - } - else if (webExStatusCode == HttpServletResponse.SC_UNAUTHORIZED) { - handledErrors = singletonSortedSetOf(projectApiErrors.getUnauthorizedApiError()); - } - } - } - else if (ex instanceof JsonProcessingException) { - utils.addBaseExceptionMessageToExtraDetailsForLogging(ex, extraDetailsForLogging); - handledErrors = singletonSortedSetOf(projectApiErrors.getMalformedRequestApiError()); - } - - // Return an indication that we will handle this exception if handledErrors got set - if (handledErrors != null) { - result = ApiExceptionHandlerListenerResult.handleResponse(handledErrors, extraDetailsForLogging); - } - else { - result = ApiExceptionHandlerListenerResult.ignoreResponse(); - } - - return result; - } -} diff --git a/backstopper-jaxrs/src/test/java/com/nike/backstopper/handler/jaxrs/JaxRsApiExceptionHandlerTest.java b/backstopper-jaxrs/src/test/java/com/nike/backstopper/handler/jaxrs/JaxRsApiExceptionHandlerTest.java deleted file mode 100644 index 525692f..0000000 --- a/backstopper-jaxrs/src/test/java/com/nike/backstopper/handler/jaxrs/JaxRsApiExceptionHandlerTest.java +++ /dev/null @@ -1,228 +0,0 @@ -package com.nike.backstopper.handler.jaxrs; - -import com.nike.backstopper.apierror.ApiError; -import com.nike.backstopper.apierror.ApiErrorBase; -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.apierror.projectspecificinfo.ProjectSpecificErrorCodeRange; -import com.nike.backstopper.apierror.testutil.BarebonesCoreApiErrorForTesting; -import com.nike.backstopper.apierror.testutil.ProjectApiErrorsForTesting; -import com.nike.backstopper.exception.ApiException; -import com.nike.backstopper.handler.ApiExceptionHandlerUtils; -import com.nike.backstopper.handler.ErrorResponseInfo; -import com.nike.backstopper.handler.UnexpectedMajorExceptionHandlingError; -import com.nike.backstopper.handler.jaxrs.config.JaxRsApiExceptionHandlerListenerList; -import com.nike.backstopper.model.DefaultErrorContractDTO; -import com.nike.internal.util.testing.Glassbox; - -import com.tngtech.java.junit.dataprovider.DataProvider; -import com.tngtech.java.junit.dataprovider.DataProviderRunner; - -import org.assertj.core.api.ThrowableAssert; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.util.UUID; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.ws.rs.NotFoundException; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; - -import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.catchThrowable; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -/** - * Tests the functionality of {@link JaxRsApiExceptionHandler } - * - * @author dsand7 - * @author Michael Irwin - */ -@RunWith(DataProviderRunner.class) -public class JaxRsApiExceptionHandlerTest { - - private JaxRsApiExceptionHandler handlerSpy; - private JaxRsUnhandledExceptionHandler unhandledSpy; - private JaxRsApiExceptionHandlerListenerList listenerList; - private static final ApiError EXPECTED_ERROR = new ApiErrorBase("test", 99008, "test", 8); - - private static final ProjectApiErrors testProjectApiErrors = ProjectApiErrorsForTesting.withProjectSpecificData( - singletonList(EXPECTED_ERROR), ProjectSpecificErrorCodeRange.ALLOW_ALL_ERROR_CODES - ); - - @Before - public void beforeMethod() { - HttpServletRequest mockRequest = mock(HttpServletRequest.class); - when(mockRequest.getRequestURI()).thenReturn("/fake/path"); - when(mockRequest.getMethod()).thenReturn("GET"); - when(mockRequest.getQueryString()).thenReturn("queryString"); - - listenerList = new JaxRsApiExceptionHandlerListenerList(testProjectApiErrors, ApiExceptionHandlerUtils.DEFAULT_IMPL); - - unhandledSpy = spy(new JaxRsUnhandledExceptionHandler(testProjectApiErrors, ApiExceptionHandlerUtils.DEFAULT_IMPL)); - - handlerSpy = spy(new JaxRsApiExceptionHandler( - testProjectApiErrors, - listenerList, - ApiExceptionHandlerUtils.DEFAULT_IMPL, unhandledSpy)); - Glassbox.setInternalState(handlerSpy, "request", mockRequest); - Glassbox.setInternalState(handlerSpy, "response", mock(HttpServletResponse.class)); - } - - @Test - public void constructor_throws_IllegalArgumentException_if_jaxRsUnhandledExceptionHandler_is_null() { - // when - Throwable ex = catchThrowable(new ThrowableAssert.ThrowingCallable() { - @Override - public void call() throws Throwable { - new JaxRsApiExceptionHandler(testProjectApiErrors, listenerList, ApiExceptionHandlerUtils.DEFAULT_IMPL, null); - } - }); - - // then - assertThat(ex).isInstanceOf(IllegalArgumentException.class); - } - - @Test - public void toResponseReturnsCorrectResponseForCoreApiErrorException() { - - ApiError expectedError = BarebonesCoreApiErrorForTesting.MALFORMED_REQUEST; - ApiException.Builder exceptionBuilder = ApiException.Builder.newBuilder(); - exceptionBuilder.withExceptionMessage("test this"); - exceptionBuilder.withApiErrors(expectedError); - Response actualResponse = handlerSpy.toResponse(exceptionBuilder.build()); - - Assert.assertEquals(expectedError.getHttpStatusCode(), actualResponse.getStatus()); - } - - @Test - public void toResponseReturnsCorrectResponseForJaxRsWebApplicationException() { - - NotFoundException exception = new NotFoundException("uri not found!"); - Response actualResponse = handlerSpy.toResponse(exception); - - Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, actualResponse.getStatus()); - } - - @Test - public void prepareFrameworkRepresentationContainsAppropriateStatusCode() { - - Response.ResponseBuilder response = handlerSpy.prepareFrameworkRepresentation(new DefaultErrorContractDTO(null, null), - HttpServletResponse.SC_OK, null, null, null); - Assert.assertEquals(HttpServletResponse.SC_OK, response.build().getStatus()); - } - - @Test - public void prepareFrameworkRepresentationContainsEntity() { - - Response.ResponseBuilder response = handlerSpy.prepareFrameworkRepresentation(new DefaultErrorContractDTO(null, null), - HttpServletResponse.SC_OK, null, null, null); - Assert.assertNotNull(response.build().getEntity()); - } - - @DataProvider(value = { - "true", - "false" - }) - @Test - public void toResponseContainsContentTypeHeaderForHandledException(boolean overrideDefaultContentType) { - // given - ApiException exception = ApiException.Builder.newBuilder() - .withExceptionMessage("test this") - .withApiErrors(BarebonesCoreApiErrorForTesting.MALFORMED_REQUEST) - .build(); - - String expectedContentType = MediaType.APPLICATION_JSON; - if (overrideDefaultContentType) { - final String finalExpectedContentType = UUID.randomUUID().toString(); - expectedContentType = finalExpectedContentType; - doAnswer(invocation -> { - ErrorResponseInfo errorResponseInfo = - (ErrorResponseInfo) invocation.getArguments()[0]; - return errorResponseInfo.frameworkRepresentationObj.header("Content-Type", finalExpectedContentType); - }).when(handlerSpy).setContentType( - any(ErrorResponseInfo.class), any(HttpServletRequest.class), any(HttpServletResponse.class), - any(Throwable.class) - ); - } - - // when - Response response = handlerSpy.toResponse(exception); - - // then - verify(handlerSpy).setContentType( - any(ErrorResponseInfo.class), any(HttpServletRequest.class), any(HttpServletResponse.class), - eq(exception) - ); - assertThat(response.getMetadata()).isNotNull(); - assertThat(response.getMetadata().get("Content-Type")).isEqualTo(singletonList(expectedContentType)); - } - - @DataProvider(value = { - "true", - "false" - }) - @Test - public void toResponseContainsContentTypeHeaderForUnhandledException(boolean overrideDefaultContentType) { - // given - String expectedContentType = MediaType.APPLICATION_JSON; - if (overrideDefaultContentType) { - final String finalExpectedContentType = UUID.randomUUID().toString(); - expectedContentType = finalExpectedContentType; - doAnswer(invocation -> { - ErrorResponseInfo errorResponseInfo = - (ErrorResponseInfo) invocation.getArguments()[0]; - return errorResponseInfo.frameworkRepresentationObj.header("Content-Type", finalExpectedContentType); - }).when(handlerSpy).setContentType( - any(ErrorResponseInfo.class), any(HttpServletRequest.class), any(HttpServletResponse.class), - any(Throwable.class) - ); - } - - // when - Response response = handlerSpy.toResponse(new Exception("not handled")); - - // then - verify(handlerSpy).setContentType( - any(ErrorResponseInfo.class), any(HttpServletRequest.class), any(HttpServletResponse.class), - any(Throwable.class) - ); - assertThat(response.getMetadata()).isNotNull(); - assertThat(response.getMetadata().get("Content-Type")).isEqualTo(singletonList(expectedContentType)); - } - - @Test - public void toResponse_delegates_to_unhandled_exception_handler_if_maybeHandleException_throws_UnexpectedMajorExceptionHandlingError() - throws UnexpectedMajorExceptionHandlingError { - // given - doThrow(new UnexpectedMajorExceptionHandlingError("foo", null)) - .when(handlerSpy).maybeHandleException(any(Throwable.class), any(HttpServletRequest.class), any(HttpServletResponse.class)); - Exception ex = new Exception("kaboom"); - String uniqueHeader = UUID.randomUUID().toString(); - ErrorResponseInfo unhandledHandlerResponse = - new ErrorResponseInfo<>(500, Response.serverError().header("unique-header", uniqueHeader), null); - doReturn(unhandledHandlerResponse) - .when(unhandledSpy).handleException(any(Throwable.class), any(HttpServletRequest.class), any(HttpServletResponse.class)); - - // when - Response response = handlerSpy.toResponse(ex); - - // then - verify(handlerSpy).maybeHandleException(any(Throwable.class), any(HttpServletRequest.class), any(HttpServletResponse.class)); - verify(unhandledSpy).handleException(any(Throwable.class), any(HttpServletRequest.class), any(HttpServletResponse.class)); - assertThat(response.getMetadata().get("unique-header")).isEqualTo(singletonList(uniqueHeader)); - } - -} diff --git a/backstopper-jaxrs/src/test/java/com/nike/backstopper/handler/jaxrs/JaxRsUnhandledExceptionHandlerTest.java b/backstopper-jaxrs/src/test/java/com/nike/backstopper/handler/jaxrs/JaxRsUnhandledExceptionHandlerTest.java deleted file mode 100644 index c7ac8e7..0000000 --- a/backstopper-jaxrs/src/test/java/com/nike/backstopper/handler/jaxrs/JaxRsUnhandledExceptionHandlerTest.java +++ /dev/null @@ -1,83 +0,0 @@ -package com.nike.backstopper.handler.jaxrs; - -import com.nike.backstopper.apierror.ApiError; -import com.nike.backstopper.apierror.ApiErrorBase; -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.apierror.projectspecificinfo.ProjectSpecificErrorCodeRange; -import com.nike.backstopper.apierror.testutil.ProjectApiErrorsForTesting; -import com.nike.backstopper.handler.ApiExceptionHandlerUtils; -import com.nike.backstopper.handler.ErrorResponseInfo; -import com.nike.backstopper.handler.RequestInfoForLogging; -import com.nike.backstopper.model.DefaultErrorContractDTO; -import com.nike.backstopper.model.util.JsonUtilWithDefaultErrorContractDTOSupport; -import com.nike.internal.util.MapBuilder; - -import org.junit.BeforeClass; -import org.junit.Test; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -import javax.servlet.http.HttpServletResponse; -import javax.ws.rs.core.Response; - -import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - -/** - * Tests the functionality of {@link JaxRsUnhandledExceptionHandler } - * - * @author dsand7 - * @author Michael Irwin - */ -public class JaxRsUnhandledExceptionHandlerTest { - - private static JaxRsUnhandledExceptionHandler handler; - private static final ApiError EXPECTED_ERROR = new ApiErrorBase("test", 99008, "test", 8); - - private static final ProjectApiErrors testProjectApiErrors = ProjectApiErrorsForTesting.withProjectSpecificData( - singletonList(EXPECTED_ERROR), ProjectSpecificErrorCodeRange.ALLOW_ALL_ERROR_CODES - ); - - @BeforeClass - public static void doBeforeClass() { - handler = new JaxRsUnhandledExceptionHandler(testProjectApiErrors, ApiExceptionHandlerUtils.DEFAULT_IMPL); - } - - @Test - public void prepareFrameworkRepresentationReturnsCorrectStatusCode() { - - int expectedCode = HttpServletResponse.SC_ACCEPTED; - Response.ResponseBuilder actualResponse = handler.prepareFrameworkRepresentation(null, expectedCode, null, null, null); - assertThat(actualResponse.build().getStatus()).isEqualTo(expectedCode); - } - - @Test - public void generateLastDitchFallbackErrorResponseInfo_returns_expected_value() { - // given - Exception ex = new Exception("kaboom"); - RequestInfoForLogging reqMock = mock(RequestInfoForLogging.class); - String errorId = UUID.randomUUID().toString(); - Map> headersMap = MapBuilder.builder("error_uid", singletonList(errorId)).build(); - - ApiError expectedGenericError = testProjectApiErrors.getGenericServiceError(); - int expectedHttpStatusCode = expectedGenericError.getHttpStatusCode(); - Map> expectedHeadersMap = new HashMap<>(headersMap); - String expectedBodyPayload = JsonUtilWithDefaultErrorContractDTOSupport.writeValueAsString( - new DefaultErrorContractDTO(errorId, singletonList(expectedGenericError)) - ); - - // when - ErrorResponseInfo response = handler.generateLastDitchFallbackErrorResponseInfo(ex, reqMock, errorId, headersMap); - - // then - assertThat(response.httpStatusCode).isEqualTo(expectedHttpStatusCode); - assertThat(response.headersToAddToResponse).isEqualTo(expectedHeadersMap); - Response builtFrameworkResponse = response.frameworkRepresentationObj.build(); - assertThat(builtFrameworkResponse.getStatus()).isEqualTo(expectedHttpStatusCode); - assertThat(builtFrameworkResponse.getEntity()).isEqualTo(expectedBodyPayload); - } -} diff --git a/backstopper-jaxrs/src/test/java/com/nike/backstopper/handler/jaxrs/listener/impl/JaxRsWebApplicationExceptionHandlerListenerTest.java b/backstopper-jaxrs/src/test/java/com/nike/backstopper/handler/jaxrs/listener/impl/JaxRsWebApplicationExceptionHandlerListenerTest.java deleted file mode 100644 index d7f18cc..0000000 --- a/backstopper-jaxrs/src/test/java/com/nike/backstopper/handler/jaxrs/listener/impl/JaxRsWebApplicationExceptionHandlerListenerTest.java +++ /dev/null @@ -1,140 +0,0 @@ -package com.nike.backstopper.handler.jaxrs.listener.impl; - -import com.nike.backstopper.apierror.ApiError; -import com.nike.backstopper.apierror.SortedApiErrorSet; -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.apierror.testutil.ProjectApiErrorsForTesting; -import com.nike.backstopper.exception.ApiException; -import com.nike.backstopper.handler.ApiExceptionHandlerUtils; -import com.nike.backstopper.handler.listener.ApiExceptionHandlerListenerResult; -import com.nike.backstopper.handler.listener.impl.ListenerTestBase; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.tngtech.java.junit.dataprovider.DataProvider; -import com.tngtech.java.junit.dataprovider.DataProviderRunner; -import com.tngtech.java.junit.dataprovider.UseDataProvider; - -import org.assertj.core.api.Assertions; -import org.assertj.core.api.ThrowableAssert; -import org.junit.BeforeClass; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.util.Collections; - -import javax.servlet.http.HttpServletResponse; -import javax.ws.rs.NotFoundException; -import javax.ws.rs.WebApplicationException; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - -/** - * Tests the functionality of {@link JaxRsWebApplicationExceptionHandlerListener} - * - * @author dsand7 - * @author Michael Irwin - */ -@RunWith(DataProviderRunner.class) -public class JaxRsWebApplicationExceptionHandlerListenerTest extends ListenerTestBase { - - private static JaxRsWebApplicationExceptionHandlerListener listener; - private static final ProjectApiErrors testProjectApiErrors = ProjectApiErrorsForTesting.withProjectSpecificData(null, null); - private static final ApiExceptionHandlerUtils utils = ApiExceptionHandlerUtils.DEFAULT_IMPL; - - @BeforeClass - public static void setupClass() { - listener = new JaxRsWebApplicationExceptionHandlerListener(testProjectApiErrors, utils); - } - - @Test - public void constructor_sets_fields_to_passed_in_args() { - // given - ProjectApiErrors projectErrorsMock = mock(ProjectApiErrors.class); - ApiExceptionHandlerUtils utilsMock = mock(ApiExceptionHandlerUtils.class); - - // when - JaxRsWebApplicationExceptionHandlerListener - impl = new JaxRsWebApplicationExceptionHandlerListener(projectErrorsMock, utilsMock); - - // then - assertThat(impl.projectApiErrors).isSameAs(projectErrorsMock); - assertThat(impl.utils).isSameAs(utilsMock); - } - - @Test - public void constructor_throws_IllegalArgumentException_if_passed_null_projectApiErrors() { - // when - Throwable ex = Assertions.catchThrowable(new ThrowableAssert.ThrowingCallable() { - @Override - public void call() throws Throwable { - new JaxRsWebApplicationExceptionHandlerListener(null, utils); - } - }); - - // then - assertThat(ex).isInstanceOf(IllegalArgumentException.class); - } - - @Test - public void constructor_throws_IllegalArgumentException_if_passed_null_utils() { - // when - Throwable ex = Assertions.catchThrowable(new ThrowableAssert.ThrowingCallable() { - @Override - public void call() throws Throwable { - new JaxRsWebApplicationExceptionHandlerListener(testProjectApiErrors, null); - } - }); - - // then - assertThat(ex).isInstanceOf(IllegalArgumentException.class); - } - - @Test - public void shouldIgnoreExceptionThatItDoesNotWantToHandle() { - validateResponse(listener.shouldHandleException(new ApiException(testProjectApiErrors.getGenericServiceError())), false, null); - } - - @Test - public void shouldCreateValidationErrorsForWebApplicationException() { - - NotFoundException exception = new NotFoundException("/fake/uri"); - - ApiExceptionHandlerListenerResult result = listener.shouldHandleException(exception); - - validateResponse(result, true, Collections.singletonList(testProjectApiErrors.getNotFoundApiError())); - } - - @Test - public void shouldIgnoreWebApplicationExceptionThatItDoesNotWantToHandle() { - - WebApplicationException exception = new WebApplicationException(); - - ApiExceptionHandlerListenerResult result = listener.shouldHandleException(exception); - - validateResponse(result, false, null); - } - - @DataProvider - public static Object[][] dataProviderForShouldHandleException() { - return new Object[][] { - { new NotFoundException(), testProjectApiErrors.getNotFoundApiError() }, - { new WebApplicationException(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE), testProjectApiErrors.getUnsupportedMediaTypeApiError() }, - { new WebApplicationException(HttpServletResponse.SC_METHOD_NOT_ALLOWED), testProjectApiErrors.getMethodNotAllowedApiError() }, - { new WebApplicationException(HttpServletResponse.SC_UNAUTHORIZED), testProjectApiErrors.getUnauthorizedApiError() }, - { new WebApplicationException(HttpServletResponse.SC_NOT_ACCEPTABLE), testProjectApiErrors.getNoAcceptableRepresentationApiError() }, - { mock(JsonProcessingException.class), testProjectApiErrors.getMalformedRequestApiError() } - }; - } - - @UseDataProvider("dataProviderForShouldHandleException") - @Test - public void shouldHandleException_handles_exceptions_it_knows_about(Exception ex, ApiError expectedResultError) { - // when - ApiExceptionHandlerListenerResult result = listener.shouldHandleException(ex); - - // then - assertThat(result.shouldHandleResponse).isTrue(); - assertThat(result.errors).isEqualTo(SortedApiErrorSet.singletonSortedSetOf(expectedResultError)); - } -} diff --git a/backstopper-jersey1/README.md b/backstopper-jersey1/README.md deleted file mode 100644 index c094a57..0000000 --- a/backstopper-jersey1/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# Backstopper - jersey1 - -Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 7 and greater. - -This readme focuses specifically on the Backstopper Jersey 1 integration. If you are looking for a different framework integration check out the [relevant section](../README.md#framework_modules) of the base readme to see if one already exists. The [base project README.md](../README.md) and [User Guide](../USER_GUIDE.md) contain the main bulk of information regarding Backstopper. - -**NOTE: There is a [Jersey 1 sample application](../samples/sample-jersey1/) that provides a simple concrete example of the information covered in this readme.** - -## Backstopper Jersey 1 Setup, Configuration, and Usage - -### Setup - -* Pull in the `com.nike.backstopper:backstopper-jersey1` dependency into your project. -* Register Backstopper components with Jersey 1. Jersey has many ways to configure itself, so this is often a project-specific process. `Jersey1BackstopperConfigHelper` contains some helpers that will be useful. See the [Jersey 1 sample application](../samples/sample-jersey1/)'s `Jersey1SampleConfigHelper` and `Main` classes for a concrete example which should help guide you even if you don't end up registering things the same way in your project. - * This causes `Jersey1ApiExceptionHandler` to be registered with the Jersey 1 error mapping system so that the Backstopper handlers will take care of *all* `Throwable`s. - * Your project's `ProjectApiErrors` will need to be provided when the `Jersey1ApiExceptionHandler` is created. `ProjectApiErrors` creation is discussed in the base Backstopper readme [here](../README.md#quickstart_usage_project_api_errors). -* Setup the reusable unit tests for your project as described in the base Backstopper readme [here](../USER_GUIDE.md#reusable_tests) and shown in the sample application. - -### Usage - -The base Backstopper readme covers the [usage basics](../README.md#quickstart_usage). There should be no difference when running in a Jersey 1 environment, other than `Jersey1ApiExceptionHandler` knowing how to handle Jersey 1 framework exceptions properly (this should happen automatically without any effort from you). - -## NOTE - Jersey 1 and Servlet API dependencies required at runtime - -This `backstopper-jersey1` module does not export any transitive Jersey 1 or Servlet API dependencies to prevent runtime -version conflicts with whatever Jersey 1 and Servlet environment you deploy to. - -This should not affect most users since this library is likely to be used in a Jersey 1/Servlet environment where the -required dependencies are already on the classpath at runtime, however if you receive class-not-found errors related to -Jersey 1 or Servlet API classes then you'll need to pull the necessary dependency into your project. - -The dependencies you may need to pull in: - -* Jersey 1: [com.sun.jersey:jersey-server:\[jersey1-version\]](https://search.maven.org/search?q=g:com.sun.jersey%20AND%20a:jersey-server) -* Servlet API (choose one of the following, depending on your environment needs): - + Servlet 3+ API: [javax.servlet:javax.servlet-api:\[servlet-api-version\]](https://search.maven.org/search?q=g:javax.servlet%20AND%20a:javax.servlet-api) - + Servlet 2 API: [javax.servlet:servlet-api:\[servlet-2-api-version\]](https://search.maven.org/search?q=g:javax.servlet%20AND%20a:servlet-api) - -## More Info - -See the [base project README.md](../README.md), [User Guide](../USER_GUIDE.md), and Backstopper repository source code and javadocs for all further information. - -## License - -Backstopper is released under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/backstopper-jersey1/build.gradle b/backstopper-jersey1/build.gradle deleted file mode 100644 index 7c0a929..0000000 --- a/backstopper-jersey1/build.gradle +++ /dev/null @@ -1,28 +0,0 @@ -evaluationDependsOn(':') - -compileTestJava { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - -dependencies { - api( - project(":backstopper-servlet-api"), - project(":backstopper-jackson"), - ) - compileOnly( - "com.sun.jersey:jersey-server:$jersey1Version", - "javax.servlet:javax.servlet-api:$servletApiVersion", - ) - testImplementation( - project(":backstopper-core").sourceSets.test.output, - "junit:junit:$junitVersion", - "org.mockito:mockito-core:$mockitoVersion", - "ch.qos.logback:logback-classic:$logbackVersion", - "org.assertj:assertj-core:$assertJVersion", - "com.tngtech.java:junit-dataprovider:$junitDataproviderVersion", - "org.hamcrest:hamcrest-all:$hamcrestVersion", - "com.sun.jersey:jersey-server:$jersey1Version", - "javax.servlet:javax.servlet-api:$servletApiVersion", - ) -} diff --git a/backstopper-jersey1/src/main/java/com/nike/backstopper/handler/jersey1/Jersey1ApiExceptionHandler.java b/backstopper-jersey1/src/main/java/com/nike/backstopper/handler/jersey1/Jersey1ApiExceptionHandler.java deleted file mode 100644 index e0231f5..0000000 --- a/backstopper-jersey1/src/main/java/com/nike/backstopper/handler/jersey1/Jersey1ApiExceptionHandler.java +++ /dev/null @@ -1,110 +0,0 @@ -package com.nike.backstopper.handler.jersey1; - -import com.nike.backstopper.apierror.ApiError; -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.handler.ApiExceptionHandlerServletApiBase; -import com.nike.backstopper.handler.ApiExceptionHandlerUtils; -import com.nike.backstopper.handler.ErrorResponseInfo; -import com.nike.backstopper.handler.RequestInfoForLogging; -import com.nike.backstopper.handler.UnexpectedMajorExceptionHandlingError; -import com.nike.backstopper.handler.jersey1.config.Jersey1BackstopperConfigHelper.ApiExceptionHandlerListenerList; -import com.nike.backstopper.model.DefaultErrorContractDTO; -import com.nike.backstopper.model.util.JsonUtilWithDefaultErrorContractDTOSupport; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Collection; - -import javax.inject.Inject; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import javax.ws.rs.ext.ExceptionMapper; -import javax.ws.rs.ext.Provider; - -/** - * An {@link ApiExceptionHandlerServletApiBase} extension that hooks into Jersey via its - * {@link ExceptionMapper} implementation, specifically {@link ExceptionMapper#toResponse(Throwable)}. - * - *

Any errors not handled here are things we don't know how to deal with and will fall through to {@link - * Jersey1UnhandledExceptionHandler}. - * - * Created by dsand7 on 9/19/14. - */ -@Provider -public class Jersey1ApiExceptionHandler extends ApiExceptionHandlerServletApiBase - implements ExceptionMapper { - - private final Logger logger = LoggerFactory.getLogger(this.getClass()); - - /** - * The {@link Jersey1UnhandledExceptionHandler} that handles any - * exceptions we weren't anticipating - */ - @SuppressWarnings("WeakerAccess") - protected final Jersey1UnhandledExceptionHandler jerseyUnhandledExceptionHandler; - - @Context - protected HttpServletRequest request; - - @Context - protected HttpServletResponse response; - - @Inject - public Jersey1ApiExceptionHandler(ProjectApiErrors projectApiErrors, - ApiExceptionHandlerListenerList apiExceptionHandlerListenerList, - ApiExceptionHandlerUtils apiExceptionHandlerUtils, - Jersey1UnhandledExceptionHandler jerseyUnhandledExceptionHandler) { - - super(projectApiErrors, apiExceptionHandlerListenerList.listeners, apiExceptionHandlerUtils); - - if (jerseyUnhandledExceptionHandler == null) - throw new IllegalArgumentException("jerseyUnhandledExceptionHandler cannot be null"); - - this.jerseyUnhandledExceptionHandler = jerseyUnhandledExceptionHandler; - } - - @Override - protected Response.ResponseBuilder prepareFrameworkRepresentation( - DefaultErrorContractDTO errorContractDTO, int httpStatusCode, Collection rawFilteredApiErrors, - Throwable originalException, RequestInfoForLogging request - ) { - return Response.status(httpStatusCode).entity( - JsonUtilWithDefaultErrorContractDTOSupport.writeValueAsString(errorContractDTO)); - } - - @Override - public Response toResponse(Throwable e) { - - ErrorResponseInfo exceptionHandled; - - try { - exceptionHandled = maybeHandleException(e, request, response); - - if (exceptionHandled == null) { - if (logger.isDebugEnabled()) { - logger.debug("No suitable handlers found for exception=" + e.getMessage() + ". " - + Jersey1UnhandledExceptionHandler.class.getName() + " should handle it."); - } - exceptionHandled = jerseyUnhandledExceptionHandler.handleException(e, request, response); - } - - } - catch (UnexpectedMajorExceptionHandlingError ohNoException) { - logger.error("Unexpected major error while handling exception. " + - Jersey1UnhandledExceptionHandler.class.getName() + " should handle it.", ohNoException); - exceptionHandled = jerseyUnhandledExceptionHandler.handleException(e, request, response); - } - - Response.ResponseBuilder responseBuilder = exceptionHandled.frameworkRepresentationObj - .header("Content-Type", MediaType.APPLICATION_JSON); - - // NOTE: We don't have to add headers to the response here - it's already been done in the - // ApiExceptionHandlerServletApiBase.processServletResponse(...) method. - - return responseBuilder.build(); - } -} diff --git a/backstopper-jersey1/src/main/java/com/nike/backstopper/handler/jersey1/Jersey1UnhandledExceptionHandler.java b/backstopper-jersey1/src/main/java/com/nike/backstopper/handler/jersey1/Jersey1UnhandledExceptionHandler.java deleted file mode 100644 index b6e5077..0000000 --- a/backstopper-jersey1/src/main/java/com/nike/backstopper/handler/jersey1/Jersey1UnhandledExceptionHandler.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.nike.backstopper.handler.jersey1; - -import com.nike.backstopper.apierror.ApiError; -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.handler.ApiExceptionHandlerUtils; -import com.nike.backstopper.handler.ErrorResponseInfo; -import com.nike.backstopper.handler.RequestInfoForLogging; -import com.nike.backstopper.handler.UnhandledExceptionHandlerServletApiBase; -import com.nike.backstopper.model.DefaultErrorContractDTO; -import com.nike.backstopper.model.util.JsonUtilWithDefaultErrorContractDTOSupport; - -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import javax.ws.rs.core.Response; - -/** - * An extension of {@link UnhandledExceptionHandlerServletApiBase} that acts as a final catch-all exception handler. - * Translates *all* exceptions to a {@link ProjectApiErrors#getGenericServiceError()}, which is then converted - * to a {@link Response.ResponseBuilder} for the caller with a response entity payload built by - * {@link JsonUtilWithDefaultErrorContractDTOSupport#writeValueAsString(Object)}. - * - * Created by dsand7 on 9/23/14. - */ -public class Jersey1UnhandledExceptionHandler extends UnhandledExceptionHandlerServletApiBase { - - protected final Set singletonGenericServiceError; - protected final int genericServiceErrorHttpStatusCode; - - /** - * Creates a new instance with the given arguments. - * - * @param projectApiErrors The {@link ProjectApiErrors} used for this project - cannot be null. - * @param utils The {@link ApiExceptionHandlerUtils} that should be used by this instance. You can pass - * in {@link ApiExceptionHandlerUtils#DEFAULT_IMPL} if you don't need custom logic. - */ - public Jersey1UnhandledExceptionHandler(ProjectApiErrors projectApiErrors, ApiExceptionHandlerUtils utils) { - super(projectApiErrors, utils); - this.singletonGenericServiceError = Collections.singleton(projectApiErrors.getGenericServiceError()); - this.genericServiceErrorHttpStatusCode = projectApiErrors.getGenericServiceError().getHttpStatusCode(); - } - - @Override - protected Response.ResponseBuilder prepareFrameworkRepresentation(DefaultErrorContractDTO errorContractDTO, - int httpStatusCode, - Collection rawFilteredApiErrors, - Throwable originalException, - RequestInfoForLogging request) { - - return Response.status(httpStatusCode).entity( - JsonUtilWithDefaultErrorContractDTOSupport.writeValueAsString(errorContractDTO)); - } - - @Override - protected ErrorResponseInfo generateLastDitchFallbackErrorResponseInfo(Throwable ex, - RequestInfoForLogging request, - String errorUid, - Map> headersForResponseWithErrorUid) { - DefaultErrorContractDTO errorContract = new DefaultErrorContractDTO(errorUid, singletonGenericServiceError); - return new ErrorResponseInfo<>( - genericServiceErrorHttpStatusCode, - Response.status(genericServiceErrorHttpStatusCode).entity( - JsonUtilWithDefaultErrorContractDTOSupport.writeValueAsString(errorContract) - ), - headersForResponseWithErrorUid - ); - } -} diff --git a/backstopper-jersey1/src/main/java/com/nike/backstopper/handler/jersey1/config/Jersey1BackstopperConfigHelper.java b/backstopper-jersey1/src/main/java/com/nike/backstopper/handler/jersey1/config/Jersey1BackstopperConfigHelper.java deleted file mode 100644 index 5efeae5..0000000 --- a/backstopper-jersey1/src/main/java/com/nike/backstopper/handler/jersey1/config/Jersey1BackstopperConfigHelper.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.nike.backstopper.handler.jersey1.config; - -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.handler.ApiExceptionHandlerUtils; -import com.nike.backstopper.handler.jersey1.listener.impl.Jersey1WebApplicationExceptionHandlerListener; -import com.nike.backstopper.handler.listener.ApiExceptionHandlerListener; -import com.nike.backstopper.handler.listener.impl.ClientDataValidationErrorHandlerListener; -import com.nike.backstopper.handler.listener.impl.DownstreamNetworkExceptionHandlerListener; -import com.nike.backstopper.handler.listener.impl.GenericApiExceptionHandlerListener; -import com.nike.backstopper.handler.listener.impl.ServersideValidationErrorHandlerListener; - -import java.util.Arrays; -import java.util.List; - -/** - * Helper utility class for generating a default list of {@link ApiExceptionHandlerListener} from a project's - * {@link ProjectApiErrors} and {@link ApiExceptionHandlerUtils}. - * - * Created by dsand7 on 9/22/14. - */ -public class Jersey1BackstopperConfigHelper { - - /** - * @return The basic set of handler listeners that are appropriate for most applications. - */ - public static List defaultApiExceptionHandlerListeners( - ProjectApiErrors projectApiErrors, ApiExceptionHandlerUtils utils - ) { - return Arrays.asList( - new GenericApiExceptionHandlerListener(), - new ServersideValidationErrorHandlerListener(projectApiErrors, utils), - new ClientDataValidationErrorHandlerListener(projectApiErrors, utils), - new DownstreamNetworkExceptionHandlerListener(projectApiErrors), - new Jersey1WebApplicationExceptionHandlerListener(projectApiErrors, utils)); - } - - /** - * Necessary because dependency injection of a collection as a bean doesn't work the way you think it should. - */ - public static class ApiExceptionHandlerListenerList { - public final List listeners; - - public ApiExceptionHandlerListenerList(List listeners) { - this.listeners = listeners; - } - } -} diff --git a/backstopper-jersey1/src/main/java/com/nike/backstopper/handler/jersey1/listener/impl/Jersey1WebApplicationExceptionHandlerListener.java b/backstopper-jersey1/src/main/java/com/nike/backstopper/handler/jersey1/listener/impl/Jersey1WebApplicationExceptionHandlerListener.java deleted file mode 100644 index 2e88c44..0000000 --- a/backstopper-jersey1/src/main/java/com/nike/backstopper/handler/jersey1/listener/impl/Jersey1WebApplicationExceptionHandlerListener.java +++ /dev/null @@ -1,109 +0,0 @@ -package com.nike.backstopper.handler.jersey1.listener.impl; - -import com.nike.backstopper.apierror.ApiError; -import com.nike.backstopper.apierror.SortedApiErrorSet; -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.handler.ApiExceptionHandlerUtils; -import com.nike.backstopper.handler.listener.ApiExceptionHandlerListener; -import com.nike.backstopper.handler.listener.ApiExceptionHandlerListenerResult; -import com.nike.internal.util.Pair; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.sun.jersey.api.NotFoundException; -import com.sun.jersey.api.ParamException; - -import java.util.ArrayList; -import java.util.List; - -import javax.inject.Inject; -import javax.servlet.http.HttpServletResponse; -import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.Response; - -import static com.nike.backstopper.apierror.SortedApiErrorSet.singletonSortedSetOf; - -/** - * Handles any known errors thrown by the Jersey framework. - * - * Created by dsand7 on 9/22/14. - */ -@SuppressWarnings("WeakerAccess") -public class Jersey1WebApplicationExceptionHandlerListener implements ApiExceptionHandlerListener { - - protected final ProjectApiErrors projectApiErrors; - protected final ApiExceptionHandlerUtils utils; - - /** - * @param projectApiErrors The {@link ProjectApiErrors} that should be used by this instance when finding {@link - * ApiError}s. Cannot be null. - * @param utils The {@link ApiExceptionHandlerUtils} that should be used by this instance. - */ - @Inject - public Jersey1WebApplicationExceptionHandlerListener(ProjectApiErrors projectApiErrors, - ApiExceptionHandlerUtils utils) { - if (projectApiErrors == null) - throw new IllegalArgumentException("ProjectApiErrors cannot be null"); - - if (utils == null) - throw new IllegalArgumentException("ApiExceptionHandlerUtils cannot be null"); - - this.projectApiErrors = projectApiErrors; - this.utils = utils; - } - - @Override - public ApiExceptionHandlerListenerResult shouldHandleException(Throwable ex) { - - ApiExceptionHandlerListenerResult result; - SortedApiErrorSet handledErrors = null; - List> extraDetailsForLogging = new ArrayList<>(); - - if (ex instanceof NotFoundException) { - handledErrors = singletonSortedSetOf(projectApiErrors.getNotFoundApiError()); - } - else if (ex instanceof ParamException.URIParamException) { - utils.addBaseExceptionMessageToExtraDetailsForLogging(ex, extraDetailsForLogging); - // Returning a 404 is intentional here. - // The Jersey contract for URIParamException states it should map to a 404. - handledErrors = singletonSortedSetOf(projectApiErrors.getNotFoundApiError()); - } - else if (ex instanceof ParamException) { - utils.addBaseExceptionMessageToExtraDetailsForLogging(ex, extraDetailsForLogging); - handledErrors = singletonSortedSetOf(projectApiErrors.getMalformedRequestApiError()); - } - else if (ex instanceof WebApplicationException) { - utils.addBaseExceptionMessageToExtraDetailsForLogging(ex, extraDetailsForLogging); - WebApplicationException webex = (WebApplicationException) ex; - Response webExResponse = webex.getResponse(); - if (webExResponse != null) { - int webExStatusCode = webExResponse.getStatus(); - if (webExStatusCode == HttpServletResponse.SC_NOT_ACCEPTABLE) { - handledErrors = singletonSortedSetOf(projectApiErrors.getNoAcceptableRepresentationApiError()); - } - else if (webExStatusCode == HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE) { - handledErrors = singletonSortedSetOf(projectApiErrors.getUnsupportedMediaTypeApiError()); - } - else if (webExStatusCode == HttpServletResponse.SC_METHOD_NOT_ALLOWED) { - handledErrors = singletonSortedSetOf(projectApiErrors.getMethodNotAllowedApiError()); - } - else if (webExStatusCode == HttpServletResponse.SC_UNAUTHORIZED) { - handledErrors = singletonSortedSetOf(projectApiErrors.getUnauthorizedApiError()); - } - } - } - else if (ex instanceof JsonProcessingException) { - utils.addBaseExceptionMessageToExtraDetailsForLogging(ex, extraDetailsForLogging); - handledErrors = singletonSortedSetOf(projectApiErrors.getMalformedRequestApiError()); - } - - // Return an indication that we will handle this exception if handledErrors got set - if (handledErrors != null) { - result = ApiExceptionHandlerListenerResult.handleResponse(handledErrors, extraDetailsForLogging); - } - else { - result = ApiExceptionHandlerListenerResult.ignoreResponse(); - } - - return result; - } -} diff --git a/backstopper-jersey1/src/test/java/com/nike/backstopper/handler/jersey1/Jersey1ApiExceptionHandlerTest.java b/backstopper-jersey1/src/test/java/com/nike/backstopper/handler/jersey1/Jersey1ApiExceptionHandlerTest.java deleted file mode 100644 index ab8e02d..0000000 --- a/backstopper-jersey1/src/test/java/com/nike/backstopper/handler/jersey1/Jersey1ApiExceptionHandlerTest.java +++ /dev/null @@ -1,181 +0,0 @@ -package com.nike.backstopper.handler.jersey1; - -import com.nike.backstopper.apierror.ApiError; -import com.nike.backstopper.apierror.ApiErrorBase; -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.apierror.projectspecificinfo.ProjectSpecificErrorCodeRange; -import com.nike.backstopper.apierror.testutil.BarebonesCoreApiErrorForTesting; -import com.nike.backstopper.apierror.testutil.ProjectApiErrorsForTesting; -import com.nike.backstopper.exception.ApiException; -import com.nike.backstopper.handler.ApiExceptionHandlerUtils; -import com.nike.backstopper.handler.ErrorResponseInfo; -import com.nike.backstopper.handler.UnexpectedMajorExceptionHandlingError; -import com.nike.backstopper.handler.jersey1.config.Jersey1BackstopperConfigHelper; -import com.nike.backstopper.handler.jersey1.config.Jersey1BackstopperConfigHelper.ApiExceptionHandlerListenerList; -import com.nike.backstopper.model.DefaultErrorContractDTO; -import com.nike.internal.util.testing.Glassbox; - -import com.sun.jersey.api.NotFoundException; - -import org.assertj.core.api.ThrowableAssert; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; - -import java.util.List; -import java.util.UUID; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; - -import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.catchThrowable; -import static org.mockito.Matchers.any; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -/** - * Tests the functionality of {@link Jersey1ApiExceptionHandler } - * - * Created by dsand7 on 9/25/14. - */ -public class Jersey1ApiExceptionHandlerTest { - - private Jersey1ApiExceptionHandler handlerSpy; - private Jersey1UnhandledExceptionHandler unhandledSpy; - private ApiExceptionHandlerListenerList listenerList; - private static final ApiError EXPECTED_ERROR = new ApiErrorBase("test", 99008, "test", 8); - - private static final ProjectApiErrors testProjectApiErrors = ProjectApiErrorsForTesting.withProjectSpecificData( - singletonList(EXPECTED_ERROR), ProjectSpecificErrorCodeRange.ALLOW_ALL_ERROR_CODES - ); - - @Before - public void beforeMethod() { - HttpServletRequest mockRequest = mock(HttpServletRequest.class); - when(mockRequest.getRequestURI()).thenReturn("/fake/path"); - when(mockRequest.getMethod()).thenReturn("GET"); - when(mockRequest.getQueryString()).thenReturn("queryString"); - - listenerList = new ApiExceptionHandlerListenerList( - Jersey1BackstopperConfigHelper.defaultApiExceptionHandlerListeners(testProjectApiErrors, ApiExceptionHandlerUtils.DEFAULT_IMPL)); - - unhandledSpy = spy(new Jersey1UnhandledExceptionHandler(testProjectApiErrors, ApiExceptionHandlerUtils.DEFAULT_IMPL)); - - handlerSpy = spy(new Jersey1ApiExceptionHandler( - testProjectApiErrors, - listenerList, - ApiExceptionHandlerUtils.DEFAULT_IMPL, unhandledSpy)); - Glassbox.setInternalState(handlerSpy, "request", mockRequest); - Glassbox.setInternalState(handlerSpy, "response", mock(HttpServletResponse.class)); - } - - @Test - public void constructor_throws_IllegalArgumentException_if_jerseyUnhandledExceptionHandler_is_null() { - // when - Throwable ex = catchThrowable(new ThrowableAssert.ThrowingCallable() { - @Override - public void call() throws Throwable { - new Jersey1ApiExceptionHandler(testProjectApiErrors, listenerList, ApiExceptionHandlerUtils.DEFAULT_IMPL, null); - } - }); - - // then - assertThat(ex).isInstanceOf(IllegalArgumentException.class); - } - - @Test - public void toResponseReturnsCorrectResponseForCoreApiErrorException() { - - ApiError expectedError = BarebonesCoreApiErrorForTesting.MALFORMED_REQUEST; - ApiException.Builder exceptionBuilder = ApiException.Builder.newBuilder(); - exceptionBuilder.withExceptionMessage("test this"); - exceptionBuilder.withApiErrors(expectedError); - Response actualResponse = handlerSpy.toResponse(exceptionBuilder.build()); - - Assert.assertEquals(expectedError.getHttpStatusCode(), actualResponse.getStatus()); - } - - @Test - public void toResponseReturnsCorrectResponseForJerseyWebApplicationException() { - - NotFoundException exception = new NotFoundException("uri not found!"); - Response actualResponse = handlerSpy.toResponse(exception); - - Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, actualResponse.getStatus()); - } - - @Test - public void prepareFrameworkRepresentationContainsAppropriateStatusCode() { - - Response.ResponseBuilder response = handlerSpy.prepareFrameworkRepresentation(new DefaultErrorContractDTO(null, null), - HttpServletResponse.SC_OK, null, null, null); - Assert.assertEquals(HttpServletResponse.SC_OK, response.build().getStatus()); - } - - @Test - public void prepareFrameworkRepresentationContainsEntity() { - - Response.ResponseBuilder response = handlerSpy.prepareFrameworkRepresentation(new DefaultErrorContractDTO(null, null), - HttpServletResponse.SC_OK, null, null, null); - Assert.assertNotNull(response.build().getEntity()); - } - - @Test - public void toResponseContainsContentTypeHeaderForHandledException() { - ApiException exception = ApiException.Builder.newBuilder() - .withExceptionMessage("test this") - .withApiErrors(BarebonesCoreApiErrorForTesting.MALFORMED_REQUEST) - .build(); - Response response = handlerSpy.toResponse(exception); - Assert.assertNotNull(response.getMetadata()); - List contentHeaders = response.getMetadata().get("Content-Type"); - Assert.assertNotNull(contentHeaders); - Assert.assertEquals(1, contentHeaders.size()); - String contentHeaderValue = (String) contentHeaders.get(0); - Assert.assertNotNull(contentHeaderValue); - Assert.assertEquals(MediaType.APPLICATION_JSON, contentHeaderValue); - } - - @Test - public void toResponseContainsContentTypeHeaderForUnhandledException() { - Response response = handlerSpy.toResponse(new Exception("not handled")); - Assert.assertNotNull(response.getMetadata()); - List contentHeaders = response.getMetadata().get("Content-Type"); - Assert.assertNotNull(contentHeaders); - Assert.assertEquals(1, contentHeaders.size()); - String contentHeaderValue = (String) contentHeaders.get(0); - Assert.assertNotNull(contentHeaderValue); - Assert.assertEquals(MediaType.APPLICATION_JSON, contentHeaderValue); - } - - @Test - public void toResponse_delegates_to_unhandled_exception_handler_if_maybeHandleException_throws_UnexpectedMajorExceptionHandlingError() - throws UnexpectedMajorExceptionHandlingError { - // given - doThrow(new UnexpectedMajorExceptionHandlingError("foo", null)) - .when(handlerSpy).maybeHandleException(any(Throwable.class), any(HttpServletRequest.class), any(HttpServletResponse.class)); - Exception ex = new Exception("kaboom"); - String uniqueHeader = UUID.randomUUID().toString(); - ErrorResponseInfo unhandledHandlerResponse = - new ErrorResponseInfo<>(500, Response.serverError().header("unique-header", uniqueHeader), null); - doReturn(unhandledHandlerResponse) - .when(unhandledSpy).handleException(any(Throwable.class), any(HttpServletRequest.class), any(HttpServletResponse.class)); - - // when - Response response = handlerSpy.toResponse(ex); - - // then - verify(handlerSpy).maybeHandleException(any(Throwable.class), any(HttpServletRequest.class), any(HttpServletResponse.class)); - verify(unhandledSpy).handleException(any(Throwable.class), any(HttpServletRequest.class), any(HttpServletResponse.class)); - assertThat(response.getMetadata().get("unique-header")).isEqualTo(singletonList(uniqueHeader)); - } - -} diff --git a/backstopper-jersey1/src/test/java/com/nike/backstopper/handler/jersey1/Jersey1UnhandledExceptionHandlerTest.java b/backstopper-jersey1/src/test/java/com/nike/backstopper/handler/jersey1/Jersey1UnhandledExceptionHandlerTest.java deleted file mode 100644 index aff95fe..0000000 --- a/backstopper-jersey1/src/test/java/com/nike/backstopper/handler/jersey1/Jersey1UnhandledExceptionHandlerTest.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.nike.backstopper.handler.jersey1; - -import com.nike.backstopper.apierror.ApiError; -import com.nike.backstopper.apierror.ApiErrorBase; -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.apierror.projectspecificinfo.ProjectSpecificErrorCodeRange; -import com.nike.backstopper.apierror.testutil.ProjectApiErrorsForTesting; -import com.nike.backstopper.handler.ApiExceptionHandlerUtils; -import com.nike.backstopper.handler.ErrorResponseInfo; -import com.nike.backstopper.handler.RequestInfoForLogging; -import com.nike.backstopper.model.DefaultErrorContractDTO; -import com.nike.backstopper.model.util.JsonUtilWithDefaultErrorContractDTOSupport; -import com.nike.internal.util.MapBuilder; - -import org.junit.BeforeClass; -import org.junit.Test; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -import javax.servlet.http.HttpServletResponse; -import javax.ws.rs.core.Response; - -import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - -/** - * Tests the functionality of {@link Jersey1UnhandledExceptionHandler } - * - * Created by dsand7 on 9/25/14. - */ -public class Jersey1UnhandledExceptionHandlerTest { - - private static Jersey1UnhandledExceptionHandler handler; - private static final ApiError EXPECTED_ERROR = new ApiErrorBase("test", 99008, "test", 8); - - private static final ProjectApiErrors testProjectApiErrors = ProjectApiErrorsForTesting.withProjectSpecificData( - singletonList(EXPECTED_ERROR), ProjectSpecificErrorCodeRange.ALLOW_ALL_ERROR_CODES - ); - - @BeforeClass - public static void doBeforeClass() { - handler = new Jersey1UnhandledExceptionHandler(testProjectApiErrors, ApiExceptionHandlerUtils.DEFAULT_IMPL); - } - - @Test - public void prepareFrameworkRepresentationReturnsCorrectStatusCode() { - - int expectedCode = HttpServletResponse.SC_ACCEPTED; - Response.ResponseBuilder actualResponse = handler.prepareFrameworkRepresentation(null, expectedCode, null, null, null); - assertThat(actualResponse.build().getStatus()).isEqualTo(expectedCode); - } - - @Test - public void generateLastDitchFallbackErrorResponseInfo_returns_expected_value() { - // given - Exception ex = new Exception("kaboom"); - RequestInfoForLogging reqMock = mock(RequestInfoForLogging.class); - String errorId = UUID.randomUUID().toString(); - Map> headersMap = MapBuilder.builder("error_uid", singletonList(errorId)).build(); - - ApiError expectedGenericError = testProjectApiErrors.getGenericServiceError(); - int expectedHttpStatusCode = expectedGenericError.getHttpStatusCode(); - Map> expectedHeadersMap = new HashMap<>(headersMap); - String expectedBodyPayload = JsonUtilWithDefaultErrorContractDTOSupport.writeValueAsString( - new DefaultErrorContractDTO(errorId, singletonList(expectedGenericError)) - ); - - // when - ErrorResponseInfo response = handler.generateLastDitchFallbackErrorResponseInfo(ex, reqMock, errorId, headersMap); - - // then - assertThat(response.httpStatusCode).isEqualTo(expectedHttpStatusCode); - assertThat(response.headersToAddToResponse).isEqualTo(expectedHeadersMap); - Response builtFrameworkResponse = response.frameworkRepresentationObj.build(); - assertThat(builtFrameworkResponse.getStatus()).isEqualTo(expectedHttpStatusCode); - assertThat(builtFrameworkResponse.getEntity()).isEqualTo(expectedBodyPayload); - } -} diff --git a/backstopper-jersey1/src/test/java/com/nike/backstopper/handler/jersey1/listener/impl/Jersey1WebApplicationExceptionHandlerListenerTest.java b/backstopper-jersey1/src/test/java/com/nike/backstopper/handler/jersey1/listener/impl/Jersey1WebApplicationExceptionHandlerListenerTest.java deleted file mode 100644 index 838d856..0000000 --- a/backstopper-jersey1/src/test/java/com/nike/backstopper/handler/jersey1/listener/impl/Jersey1WebApplicationExceptionHandlerListenerTest.java +++ /dev/null @@ -1,142 +0,0 @@ -package com.nike.backstopper.handler.jersey1.listener.impl; - -import com.nike.backstopper.apierror.ApiError; -import com.nike.backstopper.apierror.SortedApiErrorSet; -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.apierror.testutil.ProjectApiErrorsForTesting; -import com.nike.backstopper.exception.ApiException; -import com.nike.backstopper.handler.ApiExceptionHandlerUtils; -import com.nike.backstopper.handler.listener.ApiExceptionHandlerListenerResult; -import com.nike.backstopper.handler.listener.impl.ListenerTestBase; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.sun.jersey.api.NotFoundException; -import com.sun.jersey.api.ParamException; -import com.tngtech.java.junit.dataprovider.DataProvider; -import com.tngtech.java.junit.dataprovider.DataProviderRunner; -import com.tngtech.java.junit.dataprovider.UseDataProvider; - -import org.assertj.core.api.Assertions; -import org.assertj.core.api.ThrowableAssert; -import org.junit.BeforeClass; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.util.Collections; - -import javax.servlet.http.HttpServletResponse; -import javax.ws.rs.WebApplicationException; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - -/** - * Tests the functionality of {@link Jersey1WebApplicationExceptionHandlerListener} - * - * Created by dsand7 on 9/25/14. - */ -@RunWith(DataProviderRunner.class) -public class Jersey1WebApplicationExceptionHandlerListenerTest extends ListenerTestBase { - - private static Jersey1WebApplicationExceptionHandlerListener listener; - private static final ProjectApiErrors testProjectApiErrors = ProjectApiErrorsForTesting.withProjectSpecificData(null, null); - private static final ApiExceptionHandlerUtils utils = ApiExceptionHandlerUtils.DEFAULT_IMPL; - - @BeforeClass - public static void setupClass() { - listener = new Jersey1WebApplicationExceptionHandlerListener(testProjectApiErrors, utils); - } - - @Test - public void constructor_sets_fields_to_passed_in_args() { - // given - ProjectApiErrors projectErrorsMock = mock(ProjectApiErrors.class); - ApiExceptionHandlerUtils utilsMock = mock(ApiExceptionHandlerUtils.class); - - // when - Jersey1WebApplicationExceptionHandlerListener - impl = new Jersey1WebApplicationExceptionHandlerListener(projectErrorsMock, utilsMock); - - // then - assertThat(impl.projectApiErrors).isSameAs(projectErrorsMock); - assertThat(impl.utils).isSameAs(utilsMock); - } - - @Test - public void constructor_throws_IllegalArgumentException_if_passed_null_projectApiErrors() { - // when - Throwable ex = Assertions.catchThrowable(new ThrowableAssert.ThrowingCallable() { - @Override - public void call() throws Throwable { - new Jersey1WebApplicationExceptionHandlerListener(null, utils); - } - }); - - // then - assertThat(ex).isInstanceOf(IllegalArgumentException.class); - } - - @Test - public void constructor_throws_IllegalArgumentException_if_passed_null_utils() { - // when - Throwable ex = Assertions.catchThrowable(new ThrowableAssert.ThrowingCallable() { - @Override - public void call() throws Throwable { - new Jersey1WebApplicationExceptionHandlerListener(testProjectApiErrors, null); - } - }); - - // then - assertThat(ex).isInstanceOf(IllegalArgumentException.class); - } - - @Test - public void shouldIgnoreExceptionThatItDoesNotWantToHandle() { - validateResponse(listener.shouldHandleException(new ApiException(testProjectApiErrors.getGenericServiceError())), false, null); - } - - @Test - public void shouldCreateValidationErrorsForWebApplicationException() { - - NotFoundException exception = new NotFoundException("/fake/uri"); - - ApiExceptionHandlerListenerResult result = listener.shouldHandleException(exception); - - validateResponse(result, true, Collections.singletonList(testProjectApiErrors.getNotFoundApiError())); - } - - @Test - public void shouldIgnoreWebApplicationExceptionThatItDoesNotWantToHandle() { - - WebApplicationException exception = new WebApplicationException(); - - ApiExceptionHandlerListenerResult result = listener.shouldHandleException(exception); - - validateResponse(result, false, null); - } - - @DataProvider - public static Object[][] dataProviderForShouldHandleException() { - return new Object[][] { - { new NotFoundException(), testProjectApiErrors.getNotFoundApiError() }, - { mock(ParamException.URIParamException.class), testProjectApiErrors.getNotFoundApiError() }, - { mock(ParamException.class), testProjectApiErrors.getMalformedRequestApiError() }, - { new WebApplicationException(HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE), testProjectApiErrors.getUnsupportedMediaTypeApiError() }, - { new WebApplicationException(HttpServletResponse.SC_METHOD_NOT_ALLOWED), testProjectApiErrors.getMethodNotAllowedApiError() }, - { new WebApplicationException(HttpServletResponse.SC_UNAUTHORIZED), testProjectApiErrors.getUnauthorizedApiError() }, - { new WebApplicationException(HttpServletResponse.SC_NOT_ACCEPTABLE), testProjectApiErrors.getNoAcceptableRepresentationApiError() }, - { mock(JsonProcessingException.class), testProjectApiErrors.getMalformedRequestApiError() } - }; - } - - @UseDataProvider("dataProviderForShouldHandleException") - @Test - public void shouldHandleException_handles_exceptions_it_knows_about(Exception ex, ApiError expectedResultError) { - // when - ApiExceptionHandlerListenerResult result = listener.shouldHandleException(ex); - - // then - assertThat(result.shouldHandleResponse).isTrue(); - assertThat(result.errors).isEqualTo(SortedApiErrorSet.singletonSortedSetOf(expectedResultError)); - } -} diff --git a/backstopper-jersey2/README.md b/backstopper-jersey2/README.md deleted file mode 100644 index f3c5e0b..0000000 --- a/backstopper-jersey2/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# Backstopper - jersey2 - -Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 7 and greater. - -This readme focuses specifically on the Backstopper Jersey 2 integration, which builds upon the JAX-RS integration. If you are looking for a different framework integration check out the [relevant section](../README.md#framework_modules) of the base readme to see if one already exists. The [base project README.md](../README.md) and [User Guide](../USER_GUIDE.md) contain the main bulk of information regarding Backstopper. - -**NOTE: There is a [Jersey 2 sample application](../samples/sample-jersey2/) that provides a simple concrete example of the information covered in this readme.** - -## Backstopper Jersey 2 Setup, Configuration, and Usage - -### Setup - -* Pull in the `com.nike.backstopper:backstopper-jersey2` dependency into your project. -* Register Backstopper components with Jersey 2. Jersey has many ways to configure itself, so this is often a project-specific process. `Jersey2BackstopperConfigHelper` contains some helpers that will be useful. See the [Jersey 2 sample application](../samples/sample-jersey2/)'s `Jersey2SampleResourceConfig` and `Main` classes for a concrete example which should help guide you even if you don't end up registering things the same way in your project. - * This causes `Jersey2ApiExceptionHandler` to be registered with the Jersey 2 error mapping system so that the Backstopper handlers will take care of *all* `Throwable`s. - * Your project's `ProjectApiErrors` will need to be provided when the `Jersey2ApiExceptionHandler` is created. `ProjectApiErrors` creation is discussed in the base Backstopper readme [here](../README.md#quickstart_usage_project_api_errors). -* Setup the reusable unit tests for your project as described in the base Backstopper User Guide [here](../USER_GUIDE.md#reusable_tests) and shown in the sample application. - -### Usage - -The base Backstopper readme covers the [usage basics](../README.md#quickstart_usage). There should be no difference when running in a Jersey 2 environment, other than `Jersey2ApiExceptionHandler` knowing how to handle Jersey 2 framework exceptions properly (this should happen automatically without any effort from you). - -## NOTE - Jersey 2 and Servlet API dependencies required at runtime - -This `backstopper-jersey2` module does not export any transitive Jersey 2 or Servlet API dependencies to prevent runtime -version conflicts with whatever Jersey 2 and Servlet environment you deploy to. - -This should not affect most users since this library is likely to be used in a Jersey 2/Servlet environment where the -required dependencies are already on the classpath at runtime, however if you receive class-not-found errors related to -Jersey 2 or Servlet API classes then you'll need to pull the necessary dependency into your project. - -The dependencies you may need to pull in: - -* Jersey 2: [org.glassfish.jersey.core:jersey-server:\[jersey2-version\]](https://search.maven.org/search?q=g:org.glassfish.jersey.core%20AND%20a:jersey-server) -* Servlet API (choose one of the following, depending on your environment needs): - + Servlet 3+ API: [javax.servlet:javax.servlet-api:\[servlet-api-version\]](https://search.maven.org/search?q=g:javax.servlet%20AND%20a:javax.servlet-api) - + Servlet 2 API: [javax.servlet:servlet-api:\[servlet-2-api-version\]](https://search.maven.org/search?q=g:javax.servlet%20AND%20a:servlet-api) - -## More Info - -See the [base project README.md](../README.md), [User Guide](../USER_GUIDE.md), and Backstopper repository source code and javadocs for all further information. - -## License - -Backstopper is released under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/backstopper-jersey2/build.gradle b/backstopper-jersey2/build.gradle deleted file mode 100644 index ebbbd80..0000000 --- a/backstopper-jersey2/build.gradle +++ /dev/null @@ -1,27 +0,0 @@ -evaluationDependsOn(':') - -compileTestJava { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - -dependencies { - api( - project(":backstopper-jaxrs"), - ) - compileOnly( - "org.glassfish.jersey.core:jersey-server:$jersey2Version", - ) - testImplementation( - project(":backstopper-core").sourceSets.test.output, - "junit:junit:$junitVersion", - "org.mockito:mockito-core:$mockitoVersion", - "ch.qos.logback:logback-classic:$logbackVersion", - "org.assertj:assertj-core:$assertJVersion", - "com.tngtech.java:junit-dataprovider:$junitDataproviderVersion", - "org.hamcrest:hamcrest-all:$hamcrestVersion", - "javax.servlet:javax.servlet-api:$servletApiVersion", - "org.glassfish.jersey.core:jersey-server:$jersey2Version", - "org.glassfish.jersey.media:jersey-media-json-jackson:$jersey2Version", - ) -} diff --git a/backstopper-jersey2/src/main/java/com/nike/backstopper/handler/jersey2/Jersey2ApiExceptionHandler.java b/backstopper-jersey2/src/main/java/com/nike/backstopper/handler/jersey2/Jersey2ApiExceptionHandler.java deleted file mode 100644 index 2eec5a0..0000000 --- a/backstopper-jersey2/src/main/java/com/nike/backstopper/handler/jersey2/Jersey2ApiExceptionHandler.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.nike.backstopper.handler.jersey2; - -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.handler.ApiExceptionHandlerUtils; -import com.nike.backstopper.handler.jaxrs.JaxRsApiExceptionHandler; -import com.nike.backstopper.handler.jaxrs.JaxRsUnhandledExceptionHandler; -import com.nike.backstopper.handler.listener.ApiExceptionHandlerListener; - -import javax.inject.Inject; -import javax.inject.Singleton; -import javax.ws.rs.ext.Provider; - -import static com.nike.backstopper.handler.jersey2.config.Jersey2BackstopperConfigHelper.Jersey2ApiExceptionHandlerListenerList; - -/** - * A {@link JaxRsApiExceptionHandler} extension that overrides the default list of {@link ApiExceptionHandlerListener}s. - * - * @author Michael Irwin - */ -@Provider -@Singleton -public class Jersey2ApiExceptionHandler extends JaxRsApiExceptionHandler { - - @Inject - public Jersey2ApiExceptionHandler(ProjectApiErrors projectApiErrors, - Jersey2ApiExceptionHandlerListenerList apiExceptionHandlerListenerList, - ApiExceptionHandlerUtils apiExceptionHandlerUtils, - JaxRsUnhandledExceptionHandler jaxRsUnhandledExceptionHandler) { - - super(projectApiErrors, apiExceptionHandlerListenerList.listeners, apiExceptionHandlerUtils, jaxRsUnhandledExceptionHandler); - } - -} diff --git a/backstopper-jersey2/src/main/java/com/nike/backstopper/handler/jersey2/config/Jersey2BackstopperConfigHelper.java b/backstopper-jersey2/src/main/java/com/nike/backstopper/handler/jersey2/config/Jersey2BackstopperConfigHelper.java deleted file mode 100644 index 3f7a533..0000000 --- a/backstopper-jersey2/src/main/java/com/nike/backstopper/handler/jersey2/config/Jersey2BackstopperConfigHelper.java +++ /dev/null @@ -1,214 +0,0 @@ -package com.nike.backstopper.handler.jersey2.config; - -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.handler.ApiExceptionHandlerUtils; -import com.nike.backstopper.handler.jaxrs.JaxRsUnhandledExceptionHandler; -import com.nike.backstopper.handler.jaxrs.listener.impl.JaxRsWebApplicationExceptionHandlerListener; -import com.nike.backstopper.handler.jersey2.Jersey2ApiExceptionHandler; -import com.nike.backstopper.handler.jersey2.listener.impl.Jersey2WebApplicationExceptionHandlerListener; -import com.nike.backstopper.handler.listener.ApiExceptionHandlerListener; -import com.nike.backstopper.handler.listener.impl.ClientDataValidationErrorHandlerListener; -import com.nike.backstopper.handler.listener.impl.DownstreamNetworkExceptionHandlerListener; -import com.nike.backstopper.handler.listener.impl.GenericApiExceptionHandlerListener; -import com.nike.backstopper.handler.listener.impl.ServersideValidationErrorHandlerListener; - -import org.glassfish.hk2.api.ServiceHandle; -import org.glassfish.hk2.api.ServiceLocator; -import org.glassfish.hk2.utilities.binding.AbstractBinder; -import org.glassfish.jersey.internal.ExceptionMapperFactory; -import org.glassfish.jersey.server.ResourceConfig; -import org.glassfish.jersey.spi.ExceptionMappers; - -import java.lang.reflect.Field; -import java.util.Arrays; -import java.util.Iterator; -import java.util.List; -import java.util.Set; - -import javax.inject.Inject; -import javax.inject.Singleton; - -/** - * Helper utility class for dealing with setting up a Jersey 2 project with Backstopper. Important methods and inner - * classes in this class: - * - *
    - *
  • - * {@link #setupJersey2ResourceConfigForBackstopperExceptionHandling(ResourceConfig, ProjectApiErrors, - * ApiExceptionHandlerUtils)} - This is the main entry point for setting up a Jersey 2 app. It takes - * a {@link ResourceConfig}, your project's {@link ProjectApiErrors}, and the {@link ApiExceptionHandlerUtils} - * you want to use and generates the {@link Jersey2ApiExceptionHandler} that should be used as the - * one and only exception mapper for your Jersey 2 project. As part of this setup it registers an - * {@link ExceptionMapperFactoryOverrideBinder} that causes the Jersey 2 {@link ExceptionMapperFactory} - * to be limited to *only* {@link Jersey2ApiExceptionHandler}. - *
  • - *
  • - * {@link #generateJerseyApiExceptionHandler(ProjectApiErrors, ApiExceptionHandlerUtils)} - If you want - * to piecemeal your config you can use this method to generate a {@link Jersey2ApiExceptionHandler} - * with the given {@link ProjectApiErrors} and {@link ApiExceptionHandlerUtils}. You'd be responsible - * for registering this with your Jersey 2 app yourself. - *
  • - *
  • - * {@link #defaultApiExceptionHandlerListeners(ProjectApiErrors, ApiExceptionHandlerUtils)} - Use this method - * if you want even further control over creating your {@link Jersey2ApiExceptionHandler} (e.g. for overriding - * method behavior) but still want the default set of {@link ApiExceptionHandlerListener}s. - *
  • - *
  • - * {@link ExceptionMapperFactoryOverrideBinder} - Register this with your {@link ResourceConfig} to force - * your Jersey 2 app to *only* use {@link Jersey2ApiExceptionHandler} for exception mapping. If you don't - * do this then some exceptions will fall through the Backstopper net and be returned to the caller using - * whatever mapper it happened to pick, potentially leading to ugly non-standard error contracts and - * information like stack traces leaking to the caller. - *
  • - *
- * - *

NOTE: There are probably better Jersey 2 idiomatic ways to wire up the dependencies than manually creating - * the objects and passing them to {@link ResourceConfig#register(Object)}. If anyone out there is good with - * Jersey 2 please feel free to submit a pull request. - * - *

ALSO NOTE: The hack we're doing here to override the default {@link ExceptionMapperFactory} in order to make - * sure our {@link Jersey2ApiExceptionHandler} is the only exception mapper that ever gets used is pretty ugly. - * There may or may not be better ways to do this - https://java.net/jira/browse/JERSEY-2437 and - * https://java.net/jira/browse/JERSEY-2722 and seem to be blockers to a clean solution, but if you have a - * better one please feel free to submit a pull request. - * - * @author Nic Munroe - */ -@SuppressWarnings("WeakerAccess") -public class Jersey2BackstopperConfigHelper { - - /** - * Generates a default {@link Jersey2ApiExceptionHandler} (by calling {@link - * #generateJerseyApiExceptionHandler(ProjectApiErrors, ApiExceptionHandlerUtils)}) as well as a {@link - * ExceptionMapperFactoryOverrideBinder} and registers them both with the given resource config using {@link - * ResourceConfig#register(Object)}. - * - * @param resourceConfig The resource config to register with. - * @param projectApiErrors The {@link ProjectApiErrors} for your project. - * @param utils The {@link ApiExceptionHandlerUtils} you want to use with your project. - */ - public static void setupJersey2ResourceConfigForBackstopperExceptionHandling( - ResourceConfig resourceConfig, ProjectApiErrors projectApiErrors, ApiExceptionHandlerUtils utils - ) { - resourceConfig.register(new ExceptionMapperFactoryOverrideBinder()); - resourceConfig.register(generateJerseyApiExceptionHandler(projectApiErrors, utils)); - } - - /** - * @param projectApiErrors The {@link ProjectApiErrors} for your project. - * @param utils The {@link ApiExceptionHandlerUtils} you want to use with your project. - * @return A {@link Jersey2ApiExceptionHandler} that uses the given arguments and contains the default set of - * listeners (generated using {@link #defaultApiExceptionHandlerListeners(ProjectApiErrors, - * ApiExceptionHandlerUtils)}) and a default {@link JaxRsUnhandledExceptionHandler}. - */ - public static Jersey2ApiExceptionHandler generateJerseyApiExceptionHandler(ProjectApiErrors projectApiErrors, - ApiExceptionHandlerUtils utils) { - - Jersey2ApiExceptionHandlerListenerList listeners = new Jersey2ApiExceptionHandlerListenerList( - defaultApiExceptionHandlerListeners(projectApiErrors, utils) - ); - - JaxRsUnhandledExceptionHandler unhandledExceptionHandler = new JaxRsUnhandledExceptionHandler( - projectApiErrors, utils - ); - - return new Jersey2ApiExceptionHandler(projectApiErrors, listeners, utils, unhandledExceptionHandler); - } - - /** - * @return The basic set of handler listeners that are appropriate for most Jersey 2 applications. - */ - public static List defaultApiExceptionHandlerListeners( - ProjectApiErrors projectApiErrors, ApiExceptionHandlerUtils utils - ) { - return Arrays.asList( - new GenericApiExceptionHandlerListener(), - new ServersideValidationErrorHandlerListener(projectApiErrors, utils), - new ClientDataValidationErrorHandlerListener(projectApiErrors, utils), - new DownstreamNetworkExceptionHandlerListener(projectApiErrors), - new Jersey2WebApplicationExceptionHandlerListener(projectApiErrors, utils), - new JaxRsWebApplicationExceptionHandlerListener(projectApiErrors, utils)); - } - - /** - * This wrapper class is necessary because dependency injection of a collection as a bean doesn't work the way you - * think it should. - */ - @Singleton - public static class Jersey2ApiExceptionHandlerListenerList { - public final List listeners; - - @Inject - @SuppressWarnings("unused") - public Jersey2ApiExceptionHandlerListenerList(ProjectApiErrors projectApiErrors, ApiExceptionHandlerUtils utils) { - this(defaultApiExceptionHandlerListeners(projectApiErrors, utils)); - } - - public Jersey2ApiExceptionHandlerListenerList(List listeners) { - this.listeners = listeners; - } - } - - /** - * A special binder that you can register with your Jersey 2 {@link ResourceConfig} to replace the default - * {@link ExceptionMapperFactory} dependency injection binding with one that creates a - * {@link BackstopperOnlyExceptionMapperFactory} instead. This results in your Jersey 2 app only having - * access to a {@link Jersey2ApiExceptionHandler} for mapping exceptions, which in turn guarantees - * the Backstopper error contract is the only one callers will ever see. - *

- * NOTE: Due to the nature of how Jersey 2 starts up, this binder must be registered with the Jersey 2 app's - * {@link ResourceConfig} *before* Jersey 2 initializes. Otherwise the default {@link ExceptionMapperFactory} - * will be used and this binder will have no effect. - */ - public static class ExceptionMapperFactoryOverrideBinder extends AbstractBinder { - @Override - protected void configure() { - bindAsContract(BackstopperOnlyExceptionMapperFactory.class) - .to(ExceptionMappers.class) - .in(Singleton.class) - .ranked(Integer.MAX_VALUE); - } - } - - /** - * A custom {@link ExceptionMapperFactory} that removes all exception handlers from its collection except - * {@link Jersey2ApiExceptionHandler}. You can use {@link ExceptionMapperFactoryOverrideBinder} to override - * Jersey 2's default {@link ExceptionMapperFactory} to use this instead so that it will only ever use - * the Backstopper error handler, which in turn guarantees the Backstopper error contract is the only one callers - * will ever see. - */ - @SuppressWarnings("WeakerAccess") - public static class BackstopperOnlyExceptionMapperFactory extends ExceptionMapperFactory { - - @Inject - public BackstopperOnlyExceptionMapperFactory(ServiceLocator locator) throws NoSuchFieldException, - IllegalAccessException { - super(locator); - hackFixExceptionMappers(); - } - - protected void hackFixExceptionMappers() - throws NoSuchFieldException, IllegalAccessException { - Set exceptionMapperTypes = getFieldObj(ExceptionMapperFactory.class, this, "exceptionMapperTypes"); - Iterator exceptionMapperIterator = exceptionMapperTypes.iterator(); - while (exceptionMapperIterator.hasNext()) { - Object mapperType = exceptionMapperIterator.next(); - ServiceHandle serviceHandle = getFieldObj(mapperType, "mapper"); - if (!(serviceHandle.getService() instanceof Jersey2ApiExceptionHandler)) - exceptionMapperIterator.remove(); - } - } - - protected T getFieldObj(Object obj, String fieldName) throws NoSuchFieldException, IllegalAccessException { - return getFieldObj(obj.getClass(), obj, fieldName); - } - - protected T getFieldObj(Class declaringClass, - Object obj, String fieldName) throws NoSuchFieldException, IllegalAccessException { - Field field = declaringClass.getDeclaredField(fieldName); - field.setAccessible(true); - //noinspection unchecked - return (T)field.get(obj); - } - } -} diff --git a/backstopper-jersey2/src/main/java/com/nike/backstopper/handler/jersey2/listener/impl/Jersey2WebApplicationExceptionHandlerListener.java b/backstopper-jersey2/src/main/java/com/nike/backstopper/handler/jersey2/listener/impl/Jersey2WebApplicationExceptionHandlerListener.java deleted file mode 100644 index bd2933c..0000000 --- a/backstopper-jersey2/src/main/java/com/nike/backstopper/handler/jersey2/listener/impl/Jersey2WebApplicationExceptionHandlerListener.java +++ /dev/null @@ -1,80 +0,0 @@ -package com.nike.backstopper.handler.jersey2.listener.impl; - -import com.nike.backstopper.apierror.ApiError; -import com.nike.backstopper.apierror.SortedApiErrorSet; -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.handler.ApiExceptionHandlerUtils; -import com.nike.backstopper.handler.listener.ApiExceptionHandlerListener; -import com.nike.backstopper.handler.listener.ApiExceptionHandlerListenerResult; -import com.nike.internal.util.Pair; - -import org.glassfish.jersey.server.ParamException; - -import java.util.ArrayList; -import java.util.List; - -import javax.inject.Inject; -import javax.inject.Singleton; - -import static com.nike.backstopper.apierror.SortedApiErrorSet.singletonSortedSetOf; - -/** - * Handles any known errors thrown by the Jersey framework. - * - * @author dsand7 - * @author Michael Irwin - */ -@Singleton -@SuppressWarnings("WeakerAccess") -public class Jersey2WebApplicationExceptionHandlerListener implements ApiExceptionHandlerListener { - - protected final ProjectApiErrors projectApiErrors; - protected final ApiExceptionHandlerUtils utils; - - /** - * @param projectApiErrors The {@link ProjectApiErrors} that should be used by this instance when finding {@link - * ApiError}s. Cannot be null. - * @param utils The {@link ApiExceptionHandlerUtils} that should be used by this instance. - */ - @Inject - public Jersey2WebApplicationExceptionHandlerListener(ProjectApiErrors projectApiErrors, - ApiExceptionHandlerUtils utils) { - if (projectApiErrors == null) - throw new IllegalArgumentException("ProjectApiErrors cannot be null"); - - if (utils == null) - throw new IllegalArgumentException("ApiExceptionHandlerUtils cannot be null"); - - this.projectApiErrors = projectApiErrors; - this.utils = utils; - } - - @Override - public ApiExceptionHandlerListenerResult shouldHandleException(Throwable ex) { - - ApiExceptionHandlerListenerResult result; - SortedApiErrorSet handledErrors = null; - List> extraDetailsForLogging = new ArrayList<>(); - - if (ex instanceof ParamException.UriParamException) { - utils.addBaseExceptionMessageToExtraDetailsForLogging(ex, extraDetailsForLogging); - // Returning a 404 is intentional here. - // The Jersey contract for URIParamException states it should map to a 404. - handledErrors = singletonSortedSetOf(projectApiErrors.getNotFoundApiError()); - } - else if (ex instanceof ParamException) { - utils.addBaseExceptionMessageToExtraDetailsForLogging(ex, extraDetailsForLogging); - handledErrors = singletonSortedSetOf(projectApiErrors.getMalformedRequestApiError()); - } - - // Return an indication that we will handle this exception if handledErrors got set - if (handledErrors != null) { - result = ApiExceptionHandlerListenerResult.handleResponse(handledErrors, extraDetailsForLogging); - } - else { - result = ApiExceptionHandlerListenerResult.ignoreResponse(); - } - - return result; - } -} diff --git a/backstopper-jersey2/src/test/java/com/nike/backstopper/handler/jersey2/Jersey2ApiExceptionHandlerTest.java b/backstopper-jersey2/src/test/java/com/nike/backstopper/handler/jersey2/Jersey2ApiExceptionHandlerTest.java deleted file mode 100644 index 3d6e2d1..0000000 --- a/backstopper-jersey2/src/test/java/com/nike/backstopper/handler/jersey2/Jersey2ApiExceptionHandlerTest.java +++ /dev/null @@ -1,181 +0,0 @@ -package com.nike.backstopper.handler.jersey2; - -import com.nike.backstopper.apierror.ApiError; -import com.nike.backstopper.apierror.ApiErrorBase; -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.apierror.projectspecificinfo.ProjectSpecificErrorCodeRange; -import com.nike.backstopper.apierror.testutil.BarebonesCoreApiErrorForTesting; -import com.nike.backstopper.apierror.testutil.ProjectApiErrorsForTesting; -import com.nike.backstopper.exception.ApiException; -import com.nike.backstopper.handler.ApiExceptionHandlerUtils; -import com.nike.backstopper.handler.ErrorResponseInfo; -import com.nike.backstopper.handler.UnexpectedMajorExceptionHandlingError; -import com.nike.backstopper.handler.jaxrs.JaxRsUnhandledExceptionHandler; -import com.nike.backstopper.handler.jersey2.config.Jersey2BackstopperConfigHelper; -import com.nike.backstopper.handler.jersey2.config.Jersey2BackstopperConfigHelper.Jersey2ApiExceptionHandlerListenerList; -import com.nike.backstopper.model.DefaultErrorContractDTO; -import com.nike.internal.util.testing.Glassbox; - -import org.assertj.core.api.ThrowableAssert; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; - -import java.util.List; -import java.util.UUID; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.ws.rs.NotFoundException; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; - -import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.catchThrowable; -import static org.mockito.Matchers.any; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -/** - * Tests the functionality of {@link Jersey2ApiExceptionHandler } - * - * Created by dsand7 on 9/25/14. - */ -public class Jersey2ApiExceptionHandlerTest { - - private Jersey2ApiExceptionHandler handlerSpy; - private JaxRsUnhandledExceptionHandler unhandledSpy; - private Jersey2ApiExceptionHandlerListenerList listenerList; - private static final ApiError EXPECTED_ERROR = new ApiErrorBase("test", 99008, "test", 8); - - private static final ProjectApiErrors testProjectApiErrors = ProjectApiErrorsForTesting.withProjectSpecificData( - singletonList(EXPECTED_ERROR), ProjectSpecificErrorCodeRange.ALLOW_ALL_ERROR_CODES - ); - - @Before - public void beforeMethod() { - HttpServletRequest mockRequest = mock(HttpServletRequest.class); - when(mockRequest.getRequestURI()).thenReturn("/fake/path"); - when(mockRequest.getMethod()).thenReturn("GET"); - when(mockRequest.getQueryString()).thenReturn("queryString"); - - listenerList = new Jersey2ApiExceptionHandlerListenerList( - Jersey2BackstopperConfigHelper.defaultApiExceptionHandlerListeners(testProjectApiErrors, ApiExceptionHandlerUtils.DEFAULT_IMPL)); - - unhandledSpy = spy(new JaxRsUnhandledExceptionHandler(testProjectApiErrors, ApiExceptionHandlerUtils.DEFAULT_IMPL)); - - handlerSpy = spy(new Jersey2ApiExceptionHandler( - testProjectApiErrors, - listenerList, - ApiExceptionHandlerUtils.DEFAULT_IMPL, unhandledSpy)); - Glassbox.setInternalState(handlerSpy, "request", mockRequest); - Glassbox.setInternalState(handlerSpy, "response", mock(HttpServletResponse.class)); - } - - @Test - public void constructor_throws_IllegalArgumentException_if_jerseyUnhandledExceptionHandler_is_null() { - // when - Throwable ex = catchThrowable(new ThrowableAssert.ThrowingCallable() { - @Override - public void call() throws Throwable { - new Jersey2ApiExceptionHandler(testProjectApiErrors, listenerList, ApiExceptionHandlerUtils.DEFAULT_IMPL, null); - } - }); - - // then - assertThat(ex).isInstanceOf(IllegalArgumentException.class); - } - - @Test - public void toResponseReturnsCorrectResponseForCoreApiErrorException() { - - ApiError expectedError = BarebonesCoreApiErrorForTesting.MALFORMED_REQUEST; - ApiException.Builder exceptionBuilder = ApiException.Builder.newBuilder(); - exceptionBuilder.withExceptionMessage("test this"); - exceptionBuilder.withApiErrors(expectedError); - Response actualResponse = handlerSpy.toResponse(exceptionBuilder.build()); - - Assert.assertEquals(expectedError.getHttpStatusCode(), actualResponse.getStatus()); - } - - @Test - public void toResponseReturnsCorrectResponseForJerseyWebApplicationException() { - - NotFoundException exception = new NotFoundException("uri not found!"); - Response actualResponse = handlerSpy.toResponse(exception); - - Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, actualResponse.getStatus()); - } - - @Test - public void prepareFrameworkRepresentationContainsAppropriateStatusCode() { - - Response.ResponseBuilder response = handlerSpy.prepareFrameworkRepresentation(new DefaultErrorContractDTO(null, null), - HttpServletResponse.SC_OK, null, null, null); - Assert.assertEquals(HttpServletResponse.SC_OK, response.build().getStatus()); - } - - @Test - public void prepareFrameworkRepresentationContainsEntity() { - - Response.ResponseBuilder response = handlerSpy.prepareFrameworkRepresentation(new DefaultErrorContractDTO(null, null), - HttpServletResponse.SC_OK, null, null, null); - Assert.assertNotNull(response.build().getEntity()); - } - - @Test - public void toResponseContainsContentTypeHeaderForHandledException() { - ApiException exception = ApiException.Builder.newBuilder() - .withExceptionMessage("test this") - .withApiErrors(BarebonesCoreApiErrorForTesting.MALFORMED_REQUEST) - .build(); - Response response = handlerSpy.toResponse(exception); - Assert.assertNotNull(response.getMetadata()); - List contentHeaders = response.getMetadata().get("Content-Type"); - Assert.assertNotNull(contentHeaders); - Assert.assertEquals(1, contentHeaders.size()); - String contentHeaderValue = (String) contentHeaders.get(0); - Assert.assertNotNull(contentHeaderValue); - Assert.assertEquals(MediaType.APPLICATION_JSON, contentHeaderValue); - } - - @Test - public void toResponseContainsContentTypeHeaderForUnhandledException() { - Response response = handlerSpy.toResponse(new Exception("not handled")); - Assert.assertNotNull(response.getMetadata()); - List contentHeaders = response.getMetadata().get("Content-Type"); - Assert.assertNotNull(contentHeaders); - Assert.assertEquals(1, contentHeaders.size()); - String contentHeaderValue = (String) contentHeaders.get(0); - Assert.assertNotNull(contentHeaderValue); - Assert.assertEquals(MediaType.APPLICATION_JSON, contentHeaderValue); - } - - @Test - public void toResponse_delegates_to_unhandled_exception_handler_if_maybeHandleException_throws_UnexpectedMajorExceptionHandlingError() - throws UnexpectedMajorExceptionHandlingError { - // given - doThrow(new UnexpectedMajorExceptionHandlingError("foo", null)) - .when(handlerSpy).maybeHandleException(any(Throwable.class), any(HttpServletRequest.class), any(HttpServletResponse.class)); - Exception ex = new Exception("kaboom"); - String uniqueHeader = UUID.randomUUID().toString(); - ErrorResponseInfo unhandledHandlerResponse = - new ErrorResponseInfo<>(500, Response.serverError().header("unique-header", uniqueHeader), null); - doReturn(unhandledHandlerResponse) - .when(unhandledSpy).handleException(any(Throwable.class), any(HttpServletRequest.class), any(HttpServletResponse.class)); - - // when - Response response = handlerSpy.toResponse(ex); - - // then - verify(handlerSpy).maybeHandleException(any(Throwable.class), any(HttpServletRequest.class), any(HttpServletResponse.class)); - verify(unhandledSpy).handleException(any(Throwable.class), any(HttpServletRequest.class), any(HttpServletResponse.class)); - assertThat(response.getMetadata().get("unique-header")).isEqualTo(singletonList(uniqueHeader)); - } - -} diff --git a/backstopper-jersey2/src/test/java/com/nike/backstopper/handler/jersey2/config/Jersey2BackstopperConfigHelperTest.java b/backstopper-jersey2/src/test/java/com/nike/backstopper/handler/jersey2/config/Jersey2BackstopperConfigHelperTest.java deleted file mode 100644 index 6f38457..0000000 --- a/backstopper-jersey2/src/test/java/com/nike/backstopper/handler/jersey2/config/Jersey2BackstopperConfigHelperTest.java +++ /dev/null @@ -1,190 +0,0 @@ -package com.nike.backstopper.handler.jersey2.config; - -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.apierror.testutil.ProjectApiErrorsForTesting; -import com.nike.backstopper.handler.ApiExceptionHandlerUtils; -import com.nike.backstopper.handler.jaxrs.JaxRsUnhandledExceptionHandler; -import com.nike.backstopper.handler.jaxrs.listener.impl.JaxRsWebApplicationExceptionHandlerListener; -import com.nike.backstopper.handler.jersey2.Jersey2ApiExceptionHandler; -import com.nike.backstopper.handler.jersey2.config.Jersey2BackstopperConfigHelper.BackstopperOnlyExceptionMapperFactory; -import com.nike.backstopper.handler.jersey2.config.Jersey2BackstopperConfigHelper.ExceptionMapperFactoryOverrideBinder; -import com.nike.backstopper.handler.jersey2.config.Jersey2BackstopperConfigHelper.Jersey2ApiExceptionHandlerListenerList; -import com.nike.backstopper.handler.jersey2.listener.impl.Jersey2WebApplicationExceptionHandlerListener; -import com.nike.backstopper.handler.listener.ApiExceptionHandlerListener; -import com.nike.backstopper.handler.listener.impl.ClientDataValidationErrorHandlerListener; -import com.nike.backstopper.handler.listener.impl.DownstreamNetworkExceptionHandlerListener; -import com.nike.backstopper.handler.listener.impl.GenericApiExceptionHandlerListener; -import com.nike.backstopper.handler.listener.impl.ServersideValidationErrorHandlerListener; -import com.nike.internal.util.testing.Glassbox; - -import com.fasterxml.jackson.jaxrs.base.JsonMappingExceptionMapper; -import com.fasterxml.jackson.jaxrs.base.JsonParseExceptionMapper; - -import org.glassfish.hk2.api.ServiceHandle; -import org.glassfish.hk2.api.ServiceLocator; -import org.glassfish.hk2.utilities.ServiceLocatorUtilities; -import org.glassfish.hk2.utilities.binding.AbstractBinder; -import org.glassfish.jersey.internal.ExceptionMapperFactory; -import org.glassfish.jersey.server.ResourceConfig; -import org.glassfish.jersey.spi.ExceptionMappers; -import org.junit.Test; -import org.mockito.ArgumentCaptor; - -import java.util.List; -import java.util.Set; - -import javax.inject.Singleton; -import javax.ws.rs.ext.ExceptionMapper; - -import static com.nike.backstopper.handler.jersey2.config.Jersey2BackstopperConfigHelper.defaultApiExceptionHandlerListeners; -import static com.nike.backstopper.handler.jersey2.config.Jersey2BackstopperConfigHelper.generateJerseyApiExceptionHandler; -import static com.nike.backstopper.handler.jersey2.config.Jersey2BackstopperConfigHelper.setupJersey2ResourceConfigForBackstopperExceptionHandling; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -/** - * Tests the functionality of {@link Jersey2BackstopperConfigHelper}. - * - * @author Nic Munroe - */ -public class Jersey2BackstopperConfigHelperTest { - - private ProjectApiErrors projectApiErrors = ProjectApiErrorsForTesting.withProjectSpecificData(null, null); - private ApiExceptionHandlerUtils utils = mock(ApiExceptionHandlerUtils.class); - - private void verifyContainsExpectedField(Object obj, String fieldName, Object expectedValue) { - assertThat(Glassbox.getInternalState(obj, fieldName)).isEqualTo(expectedValue); - } - - private void verifyDefaultListenerList(List listeners) { - assertThat(listeners).hasSize(6); - - assertThat(listeners.get(0)).isInstanceOf(GenericApiExceptionHandlerListener.class); - - assertThat(listeners.get(1)).isInstanceOf(ServersideValidationErrorHandlerListener.class); - verifyContainsExpectedField(listeners.get(1), "projectApiErrors", projectApiErrors); - verifyContainsExpectedField(listeners.get(1), "utils", utils); - - assertThat(listeners.get(2)).isInstanceOf(ClientDataValidationErrorHandlerListener.class); - verifyContainsExpectedField(listeners.get(2), "projectApiErrors", projectApiErrors); - verifyContainsExpectedField(listeners.get(2), "utils", utils); - - assertThat(listeners.get(3)).isInstanceOf(DownstreamNetworkExceptionHandlerListener.class); - verifyContainsExpectedField(listeners.get(3), "projectApiErrors", projectApiErrors); - - assertThat(listeners.get(4)).isInstanceOf(Jersey2WebApplicationExceptionHandlerListener.class); - verifyContainsExpectedField(listeners.get(4), "projectApiErrors", projectApiErrors); - verifyContainsExpectedField(listeners.get(4), "utils", utils); - - assertThat(listeners.get(5)).isInstanceOf(JaxRsWebApplicationExceptionHandlerListener.class); - verifyContainsExpectedField(listeners.get(5), "projectApiErrors", projectApiErrors); - verifyContainsExpectedField(listeners.get(5), "utils", utils); - } - - @Test - public void defaultApiExceptionHandlerListeners_creates_default_list_of_listeners() { - // when - List defaultListeners = defaultApiExceptionHandlerListeners(projectApiErrors, utils); - - // then - verifyDefaultListenerList(defaultListeners); - } - - private void verifyDefaultJersey2ApiExceptionHandler(Jersey2ApiExceptionHandler handler) { - verifyDefaultListenerList( - (List) Glassbox.getInternalState(handler, "apiExceptionHandlerListenerList") - ); - verifyContainsExpectedField(handler, "projectApiErrors", projectApiErrors); - verifyContainsExpectedField(handler, "utils", utils); - JaxRsUnhandledExceptionHandler unhandledHandler = - (JaxRsUnhandledExceptionHandler) Glassbox.getInternalState(handler, "jerseyUnhandledExceptionHandler"); - verifyContainsExpectedField(unhandledHandler, "projectApiErrors", projectApiErrors); - verifyContainsExpectedField(unhandledHandler, "utils", utils); - } - - @Test - public void generateJerseyApiExceptionHandler_creates_default_handler() { - // when - Jersey2ApiExceptionHandler handler = generateJerseyApiExceptionHandler(projectApiErrors, utils); - - // then - verifyDefaultJersey2ApiExceptionHandler(handler); - } - - @Test - public void setupJersey2ResourceConfigForBackstopperExceptionHandling_sets_up_expected_defaults() { - // given - ResourceConfig resourceConfigMock = mock(ResourceConfig.class); - - // when - setupJersey2ResourceConfigForBackstopperExceptionHandling(resourceConfigMock, projectApiErrors, utils); - - // then - ArgumentCaptor registerArgCaptor = ArgumentCaptor.forClass(Object.class); - verify(resourceConfigMock, times(2)).register(registerArgCaptor.capture()); - List registeredResources = registerArgCaptor.getAllValues(); - assertThat(registeredResources).hasSize(2); - assertThat(registeredResources.get(0)).isInstanceOf(ExceptionMapperFactoryOverrideBinder.class); - assertThat(registeredResources.get(1)).isInstanceOf(Jersey2ApiExceptionHandler.class); - Jersey2ApiExceptionHandler registeredHandler = (Jersey2ApiExceptionHandler) registeredResources.get(1); - verifyDefaultJersey2ApiExceptionHandler(registeredHandler); - } - - @Test - public void code_coverage_hoops() { - // jump! - new Jersey2BackstopperConfigHelper(); - } - - @Test - public void apiExceptionHandlerListenerList_injector_constructor_creates_default_listener_list() { - // when - Jersey2ApiExceptionHandlerListenerList listHolder = new Jersey2ApiExceptionHandlerListenerList(projectApiErrors, utils); - - // then - verifyDefaultListenerList(listHolder.listeners); - } - - @Test - public void exceptionMapperFactoryOverrideBinder_configures_ExceptionMappers_override() { - // given - AbstractBinder defaultJersey2ExceptionMapperBinder = new ExceptionMapperFactory.Binder(); - ExceptionMapperFactoryOverrideBinder overrideBinder = new ExceptionMapperFactoryOverrideBinder(); - ServiceLocator locator = ServiceLocatorUtilities.bind(defaultJersey2ExceptionMapperBinder, overrideBinder); - - // when - ExceptionMappers result = locator.getService(ExceptionMappers.class); - - // then - assertThat(result).isInstanceOf(BackstopperOnlyExceptionMapperFactory.class); - } - - @Test - public void backstopperOnlyExceptionMapperFactory_removes_all_exception_mappers_except_Jersey2ApiExceptionHandler() - throws NoSuchFieldException, IllegalAccessException { - // given - AbstractBinder lotsOfExceptionMappersBinder = new AbstractBinder() { - @Override - protected void configure() { - bind(JsonMappingExceptionMapper.class).to(ExceptionMapper.class).in(Singleton.class); - bind(JsonParseExceptionMapper.class).to(ExceptionMapper.class).in(Singleton.class); - bind(generateJerseyApiExceptionHandler(projectApiErrors, utils)).to(ExceptionMapper.class); - } - }; - - ServiceLocator locator = ServiceLocatorUtilities.bind(lotsOfExceptionMappersBinder); - - // when - BackstopperOnlyExceptionMapperFactory overrideExceptionMapper = new BackstopperOnlyExceptionMapperFactory(locator); - - // then - Set emTypesLeft = overrideExceptionMapper.getFieldObj( - ExceptionMapperFactory.class, overrideExceptionMapper, "exceptionMapperTypes" - ); - assertThat(emTypesLeft).hasSize(1); - ServiceHandle serviceHandle = overrideExceptionMapper.getFieldObj(emTypesLeft.iterator().next(), "mapper"); - assertThat(serviceHandle.getService()).isInstanceOf(Jersey2ApiExceptionHandler.class); - } -} \ No newline at end of file diff --git a/backstopper-jersey2/src/test/java/com/nike/backstopper/handler/jersey2/listener/impl/Jersey2WebApplicationExceptionHandlerListenerTest.java b/backstopper-jersey2/src/test/java/com/nike/backstopper/handler/jersey2/listener/impl/Jersey2WebApplicationExceptionHandlerListenerTest.java deleted file mode 100644 index 5f0a203..0000000 --- a/backstopper-jersey2/src/test/java/com/nike/backstopper/handler/jersey2/listener/impl/Jersey2WebApplicationExceptionHandlerListenerTest.java +++ /dev/null @@ -1,121 +0,0 @@ -package com.nike.backstopper.handler.jersey2.listener.impl; - -import com.nike.backstopper.apierror.ApiError; -import com.nike.backstopper.apierror.SortedApiErrorSet; -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.apierror.testutil.ProjectApiErrorsForTesting; -import com.nike.backstopper.exception.ApiException; -import com.nike.backstopper.handler.ApiExceptionHandlerUtils; -import com.nike.backstopper.handler.listener.ApiExceptionHandlerListenerResult; -import com.nike.backstopper.handler.listener.impl.ListenerTestBase; - -import com.tngtech.java.junit.dataprovider.DataProvider; -import com.tngtech.java.junit.dataprovider.DataProviderRunner; -import com.tngtech.java.junit.dataprovider.UseDataProvider; - -import org.assertj.core.api.Assertions; -import org.assertj.core.api.ThrowableAssert; -import org.glassfish.jersey.server.ParamException; -import org.junit.BeforeClass; -import org.junit.Test; -import org.junit.runner.RunWith; - -import javax.ws.rs.WebApplicationException; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; - -/** - * Tests the functionality of {@link Jersey2WebApplicationExceptionHandlerListener} - * - * Created by dsand7 on 9/25/14. - */ -@RunWith(DataProviderRunner.class) -public class Jersey2WebApplicationExceptionHandlerListenerTest extends ListenerTestBase { - - private static Jersey2WebApplicationExceptionHandlerListener listener; - private static final ProjectApiErrors testProjectApiErrors = ProjectApiErrorsForTesting.withProjectSpecificData(null, null); - private static final ApiExceptionHandlerUtils utils = ApiExceptionHandlerUtils.DEFAULT_IMPL; - - @BeforeClass - public static void setupClass() { - listener = new Jersey2WebApplicationExceptionHandlerListener(testProjectApiErrors, utils); - } - - @Test - public void constructor_sets_fields_to_passed_in_args() { - // given - ProjectApiErrors projectErrorsMock = mock(ProjectApiErrors.class); - ApiExceptionHandlerUtils utilsMock = mock(ApiExceptionHandlerUtils.class); - - // when - Jersey2WebApplicationExceptionHandlerListener - impl = new Jersey2WebApplicationExceptionHandlerListener(projectErrorsMock, utilsMock); - - // then - assertThat(impl.projectApiErrors).isSameAs(projectErrorsMock); - assertThat(impl.utils).isSameAs(utilsMock); - } - - @Test - public void constructor_throws_IllegalArgumentException_if_passed_null_projectApiErrors() { - // when - Throwable ex = Assertions.catchThrowable(new ThrowableAssert.ThrowingCallable() { - @Override - public void call() throws Throwable { - new Jersey2WebApplicationExceptionHandlerListener(null, utils); - } - }); - - // then - assertThat(ex).isInstanceOf(IllegalArgumentException.class); - } - - @Test - public void constructor_throws_IllegalArgumentException_if_passed_null_utils() { - // when - Throwable ex = Assertions.catchThrowable(new ThrowableAssert.ThrowingCallable() { - @Override - public void call() throws Throwable { - new Jersey2WebApplicationExceptionHandlerListener(testProjectApiErrors, null); - } - }); - - // then - assertThat(ex).isInstanceOf(IllegalArgumentException.class); - } - - @Test - public void shouldIgnoreExceptionThatItDoesNotWantToHandle() { - validateResponse(listener.shouldHandleException(new ApiException(testProjectApiErrors.getGenericServiceError())), false, null); - } - - @Test - public void shouldIgnoreWebApplicationExceptionThatItDoesNotWantToHandle() { - - WebApplicationException exception = new WebApplicationException(); - - ApiExceptionHandlerListenerResult result = listener.shouldHandleException(exception); - - validateResponse(result, false, null); - } - - @DataProvider - public static Object[][] dataProviderForShouldHandleException() { - return new Object[][] { - { mock(ParamException.UriParamException.class), testProjectApiErrors.getNotFoundApiError() }, - { mock(ParamException.class), testProjectApiErrors.getMalformedRequestApiError() } - }; - } - - @UseDataProvider("dataProviderForShouldHandleException") - @Test - public void shouldHandleException_handles_exceptions_it_knows_about(Exception ex, ApiError expectedResultError) { - // when - ApiExceptionHandlerListenerResult result = listener.shouldHandleException(ex); - - // then - assertThat(result.shouldHandleResponse).isTrue(); - assertThat(result.errors).isEqualTo(SortedApiErrorSet.singletonSortedSetOf(expectedResultError)); - } -} diff --git a/samples/sample-jersey1/README.md b/samples/sample-jersey1/README.md deleted file mode 100644 index fbb914e..0000000 --- a/samples/sample-jersey1/README.md +++ /dev/null @@ -1,42 +0,0 @@ -# Backstopper Sample Application - jersey1 - -Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 7 and greater. - -This submodule contains a sample application based on Jersey 1 that fully integrates Backstopper. - -* Build the sample by running the `./buildSample.sh` script. -* Launch the sample by running the `./runSample.sh` script. It will bind to port 8080 by default. - * You can override the default port by passing in a system property to the run script, e.g. to bind to port 8181: `./runSample.sh -Djersey1Sample.server.port=8181` - -## Things to try - -All examples here assume the sample app is running on port 8080, so you would hit each path by going to `http://localhost:8080/[endpoint-path]`. It's recommended that you use a REST client like [Postman](https://www.getpostman.com/) for making the requests so you can easily specify HTTP method, payloads, headers, etc, and fully inspect the response. - -Also note that all the following things to try are verified in a component test: `VerifyExpectedErrorsAreReturnedComponentTest`. If you prefer to experiment via code you can run, debug, and otherwise explore that test. - -As you are doing the following you should check the logs that are output by the sample application and notice what is included in the log messages. In particular notice how you can search for an `error_id` that came from an error response and go directly to the relevant log message in the logs. Also notice how the `ApiError.getName()` value shows up in the logs for each error represented in a returned error contract (there can be more than one per request). - -* `GET /sample` - Returns the JSON serialization for the `SampleModel` model object. You can copy this into a `POST` call to experiment with triggering errors. -* `POST /sample` with `ContentType: application/json` header - Using the JSON model retrieved by the `GET` call, you can trigger numerous different types of errors, all of which get caught by the Backstopper system and converted into the appropriate error contract. - * Omit the `foo` field. - * Set the value of the `range_0_to_42` field to something outside of the allowed 0-42 range. - * Set the value of the `rgb_color` field to something besides `RED`, `GREEN`, or `BLUE`, or omit it entirely. Note that the validation and deserialization of this enum field is done in a case insensitive manner - i.e. you can pass `red`, `Green`, or `bLuE` if you want and it will not throw an error. - * Set two or more invalid values for `foo`, `range_0_to_42`, and `rgb_color` to invalid values all at once - notice you get back all relevant errors at once in the same error contract. - * Set `throw_manual_error` to true to trigger a manual exception to be thrown inside the normal `POST /sample` endpoint. - * Note the extra response headers that are included when you do this, and how they relate to the `.withExtraResponseHeaders(...)` method call on the builder of the exception that is thrown. - * Pass in an empty JSON payload - you should receive a `"Missing expected content"` error back. - * Pass in a junk payload that is not valid JSON - you should receive a `"Malformed request"` error back. -* `GET /sample/coreErrorWrapper` - Triggers an error to be thrown that appears to the caller like a normal generic service exception, but the `SOME_MEANINGFUL_ERROR_NAME` name from the `ApiError` it represents shows up in the logs to help you disambiguate what the true cause was. -* `GET /sample/triggerUnhandledError` - Triggers an error that is caught by the unhandled exception handler portion of Backstopper and converted to a generic service exception. -* `GET /does-not-exist` - Triggers a framework 404 which Backstopper handles. -* `DELETE /sample` - Triggers a framework 405 which Backstopper handles. -* `GET /sample` with `Accept: application/octet-stream` header - Triggers a framework 406 which Backstopper handles. -* `POST /sample` with `ContentType: text/plain` - Triggers a framework 415 which Backstopper handles. - -## More Info - -See the [base project README.md](../../README.md), [User Guide](../../USER_GUIDE.md), and Backstopper repository source code and javadocs for all further information. - -## License - -Backstopper is released under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/samples/sample-jersey1/build.gradle b/samples/sample-jersey1/build.gradle deleted file mode 100644 index 53e90cc..0000000 --- a/samples/sample-jersey1/build.gradle +++ /dev/null @@ -1,40 +0,0 @@ -evaluationDependsOn(':') - -sourceCompatibility = JavaVersion.VERSION_1_8 -targetCompatibility = JavaVersion.VERSION_1_8 - -test { - useJUnitPlatform() -} - -dependencies { - implementation( - project(":backstopper-jersey1"), - project(":backstopper-custom-validators"), - "com.sun.jersey:jersey-servlet:$jersey1Version", - "com.sun.jersey:jersey-json:$jersey1Version", - "org.glassfish.jersey.media:jersey-media-json-jackson:$jersey2Version", - "ch.qos.logback:logback-classic:$logbackVersion", - "org.hibernate:hibernate-validator:$hibernateValidatorVersion", - "org.eclipse.jetty:jetty-webapp:$jettyVersion" - ) - testImplementation( - project(":backstopper-reusable-tests-junit5"), - "org.junit.jupiter:junit-jupiter-api:$junit5Version", - "org.junit.jupiter:junit-jupiter-engine:$junit5Version", - "org.junit.jupiter:junit-jupiter-params:$junit5Version", - "org.mockito:mockito-core:$mockitoVersion", - "com.fasterxml.jackson.core:jackson-core:$jacksonVersion", - "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion", - "ch.qos.logback:logback-classic:$logbackVersion", - "org.assertj:assertj-core:$assertJVersion", - "io.rest-assured:rest-assured:$restAssuredVersion" - ) -} - -apply plugin: "application" -mainClassName = "com.nike.backstopper.jersey1sample.Main" - -run { - systemProperties = System.getProperties() -} diff --git a/samples/sample-jersey1/buildSample.sh b/samples/sample-jersey1/buildSample.sh deleted file mode 100755 index 2effeba..0000000 --- a/samples/sample-jersey1/buildSample.sh +++ /dev/null @@ -1,2 +0,0 @@ -echo "../../gradlew clean build" -../../gradlew clean build \ No newline at end of file diff --git a/samples/sample-jersey1/runSample.sh b/samples/sample-jersey1/runSample.sh deleted file mode 100755 index 65ac084..0000000 --- a/samples/sample-jersey1/runSample.sh +++ /dev/null @@ -1,3 +0,0 @@ -echo "../../gradlew run" -echo "NOTE: Type ctrl+c to stop" -../../gradlew run $* \ No newline at end of file diff --git a/samples/sample-jersey1/src/main/java/com/nike/backstopper/jersey1sample/Main.java b/samples/sample-jersey1/src/main/java/com/nike/backstopper/jersey1sample/Main.java deleted file mode 100644 index 7d4fff3..0000000 --- a/samples/sample-jersey1/src/main/java/com/nike/backstopper/jersey1sample/Main.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.nike.backstopper.jersey1sample; - -import com.nike.backstopper.jersey1sample.config.Jersey1SampleConfigHelper; - -import com.sun.jersey.api.core.ResourceConfig; -import com.sun.jersey.api.core.servlet.WebAppResourceConfig; -import com.sun.jersey.spi.container.servlet.ServletContainer; - -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHolder; - -import java.io.IOException; -import java.util.Collections; - -/** - * Starts up the Backstopper Jersey 1 Sample server (on port 8080 by default). - * - * @author Nic Munroe - */ -public class Main { - - @SuppressWarnings("WeakerAccess") - public static final String PORT_SYSTEM_PROP_KEY = "jersey1Sample.server.port"; - - public static void main(String[] args) throws Exception { - Server server = createServer(Integer.parseInt(System.getProperty(PORT_SYSTEM_PROP_KEY, "8080"))); - - try { - server.start(); - server.join(); - } - finally { - server.destroy(); - } - } - - public static Server createServer(int port) throws Exception { - Server server = new Server(port); - server.setHandler(generateServletContextHandler()); - - return server; - } - - private static ServletContextHandler generateServletContextHandler() throws IOException { - ServletContextHandler contextHandler = new ServletContextHandler(); - contextHandler.setContextPath("/"); - - ResourceConfig rc = new WebAppResourceConfig(Collections.emptyMap(), contextHandler.getServletContext()); - rc.getSingletons().add(Jersey1SampleConfigHelper.generateJerseyApiExceptionHandler()); - rc.getSingletons().add(Jersey1SampleConfigHelper.generateSampleResource()); - ServletContainer jerseyServletContainer = new ServletContainer(rc); - contextHandler.addServlet(new ServletHolder(jerseyServletContainer), "/*"); - return contextHandler; - } - -} diff --git a/samples/sample-jersey1/src/main/java/com/nike/backstopper/jersey1sample/config/Jersey1SampleConfigHelper.java b/samples/sample-jersey1/src/main/java/com/nike/backstopper/jersey1sample/config/Jersey1SampleConfigHelper.java deleted file mode 100644 index e1f647e..0000000 --- a/samples/sample-jersey1/src/main/java/com/nike/backstopper/jersey1sample/config/Jersey1SampleConfigHelper.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.nike.backstopper.jersey1sample.config; - -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.handler.ApiExceptionHandlerUtils; -import com.nike.backstopper.handler.jersey1.Jersey1ApiExceptionHandler; -import com.nike.backstopper.handler.jersey1.Jersey1UnhandledExceptionHandler; -import com.nike.backstopper.handler.jersey1.config.Jersey1BackstopperConfigHelper; -import com.nike.backstopper.jersey1sample.error.SampleProjectApiErrorsImpl; -import com.nike.backstopper.jersey1sample.resource.SampleResource; -import com.nike.backstopper.service.ClientDataValidationService; - -import javax.validation.Validation; -import javax.validation.Validator; - -/** - * This Jersey 1 sample app is as barebones as we could make it, which means no dependency injection. This class - * is here to provide instances of {@link Jersey1ApiExceptionHandler} and {@link SampleResource} for registration - * with Jersey so that they are picked up and used, but gives us the control over their creation so we - * can manually inject them with the dependencies they need. - * - *

In a normal production Jersey app these might be auto-generated for you using dependency injection, and - * auto-picked-up without having to manually register them. - * - * @author Nic Munroe - */ -public class Jersey1SampleConfigHelper { - - private static final ProjectApiErrors projectApiErrors = new SampleProjectApiErrorsImpl(); - - public static Jersey1ApiExceptionHandler generateJerseyApiExceptionHandler() { - ApiExceptionHandlerUtils utils = ApiExceptionHandlerUtils.DEFAULT_IMPL; - Jersey1BackstopperConfigHelper.ApiExceptionHandlerListenerList - listeners = new Jersey1BackstopperConfigHelper.ApiExceptionHandlerListenerList( - Jersey1BackstopperConfigHelper.defaultApiExceptionHandlerListeners(projectApiErrors, utils) - ); - Jersey1UnhandledExceptionHandler unhandledExceptionHandler = - new Jersey1UnhandledExceptionHandler(projectApiErrors, utils); - - return new Jersey1ApiExceptionHandler( - projectApiErrors, listeners, utils, unhandledExceptionHandler - ); - } - - public static SampleResource generateSampleResource() { - Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); - return new SampleResource(new ClientDataValidationService(validator)); - } - -} diff --git a/samples/sample-jersey1/src/main/java/com/nike/backstopper/jersey1sample/error/SampleProjectApiError.java b/samples/sample-jersey1/src/main/java/com/nike/backstopper/jersey1sample/error/SampleProjectApiError.java deleted file mode 100644 index 21848b2..0000000 --- a/samples/sample-jersey1/src/main/java/com/nike/backstopper/jersey1sample/error/SampleProjectApiError.java +++ /dev/null @@ -1,91 +0,0 @@ -package com.nike.backstopper.jersey1sample.error; - -import com.nike.backstopper.apierror.ApiError; -import com.nike.backstopper.apierror.ApiErrorBase; -import com.nike.backstopper.apierror.ApiErrorWithMetadata; -import com.nike.backstopper.apierror.sample.SampleCoreApiError; -import com.nike.backstopper.jersey1sample.model.RgbColor; -import com.nike.internal.util.MapBuilder; - -import java.util.Arrays; -import java.util.Map; -import java.util.UUID; - -import javax.ws.rs.core.Response.Status; - -/** - * Project-specific error definitions for this sample app. Note that the error codes for errors specified here must - * conform to the range specified in {@link SampleProjectApiErrorsImpl#getProjectSpecificErrorCodeRange()} or an - * exception will be thrown on app startup, and unit tests should fail. The one exception to this rule is a "core - * error wrapper" - an instance that shares the same error code, message, and HTTP status code as a - * {@link SampleProjectApiErrorsImpl#getCoreApiErrors()} instance (in this case that means a wrapper around - * {@link SampleCoreApiError}). - * - * @author Nic Munroe - */ -public enum SampleProjectApiError implements ApiError { - FIELD_CANNOT_BE_NULL_OR_BLANK(99100, "Field cannot be null or empty", Status.BAD_REQUEST.getStatusCode()), - // FOO_STRING_CANNOT_BE_BLANK shows how you can build off a base/generic error and add metadata. - FOO_STRING_CANNOT_BE_BLANK(FIELD_CANNOT_BE_NULL_OR_BLANK, MapBuilder.builder("field", (Object)"foo").build()), - INVALID_RANGE_VALUE(99110, "The range_0_to_42 field must be between 0 and 42 (inclusive)", - Status.BAD_REQUEST.getStatusCode()), - // RGB_COLOR_CANNOT_BE_NULL could build off FIELD_CANNOT_BE_NULL_OR_BLANK like FOO_STRING_CANNOT_BE_BLANK does, - // however this shows how you can make individual field errors with unique code and custom message. - RGB_COLOR_CANNOT_BE_NULL(99120, "The rgb_color field must be defined", Status.BAD_REQUEST.getStatusCode()), - NOT_RGB_COLOR_ENUM(99130, "The rgb_color field value must be one of: " + Arrays.toString(RgbColor.values()), - Status.BAD_REQUEST.getStatusCode()), - MANUALLY_THROWN_ERROR(99140, "You asked for an error to be thrown", Status.INTERNAL_SERVER_ERROR.getStatusCode()), - // This is a wrapper around a core error. It will have the same error code, message, and HTTP status code, - // but will show up in the logs with contributing_errors="SOME_MEANINGFUL_ERROR_NAME", allowing you to - // distinguish the context of the error vs. the core GENERIC_SERVICE_ERROR at a glance. - SOME_MEANINGFUL_ERROR_NAME(SampleCoreApiError.GENERIC_SERVICE_ERROR); - - private final ApiError delegate; - - SampleProjectApiError(ApiError delegate) { - this.delegate = delegate; - } - - SampleProjectApiError(ApiError delegate, Map metadata) { - this(new ApiErrorWithMetadata(delegate, metadata)); - } - - SampleProjectApiError(int errorCode, String message, int httpStatusCode) { - this(new ApiErrorBase( - "delegated-to-enum-wrapper-" + UUID.randomUUID().toString(), errorCode, message, httpStatusCode - )); - } - - @SuppressWarnings("unused") - SampleProjectApiError(int errorCode, String message, int httpStatusCode, Map metadata) { - this(new ApiErrorBase( - "delegated-to-enum-wrapper-" + UUID.randomUUID().toString(), errorCode, message, httpStatusCode, metadata - )); - } - - @Override - public String getName() { - return this.name(); - } - - @Override - public String getErrorCode() { - return delegate.getErrorCode(); - } - - @Override - public String getMessage() { - return delegate.getMessage(); - } - - @Override - public int getHttpStatusCode() { - return delegate.getHttpStatusCode(); - } - - @Override - public Map getMetadata() { - return delegate.getMetadata(); - } - -} \ No newline at end of file diff --git a/samples/sample-jersey1/src/main/java/com/nike/backstopper/jersey1sample/error/SampleProjectApiErrorsImpl.java b/samples/sample-jersey1/src/main/java/com/nike/backstopper/jersey1sample/error/SampleProjectApiErrorsImpl.java deleted file mode 100644 index 0e51192..0000000 --- a/samples/sample-jersey1/src/main/java/com/nike/backstopper/jersey1sample/error/SampleProjectApiErrorsImpl.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.nike.backstopper.jersey1sample.error; - -import com.nike.backstopper.apierror.ApiError; -import com.nike.backstopper.apierror.projectspecificinfo.ProjectSpecificErrorCodeRange; -import com.nike.backstopper.apierror.projectspecificinfo.ProjectSpecificErrorCodeRangeIntegerImpl; -import com.nike.backstopper.apierror.sample.SampleProjectApiErrorsBase; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import javax.inject.Singleton; - -/** - * Returns the project specific errors for this sample application. {@link #getProjectApiErrors()} will return a - * combination of {@link SampleProjectApiErrorsBase#getCoreApiErrors()} and {@link #getProjectSpecificApiErrors()}. - * This means that you have all the enum values of {@link com.nike.backstopper.apierror.sample.SampleCoreApiError} - * and {@link SampleProjectApiError} at your disposal when throwing errors in this sample app. - */ -@Singleton -public class SampleProjectApiErrorsImpl extends SampleProjectApiErrorsBase { - - private static final List projectSpecificApiErrors = - new ArrayList<>(Arrays.asList(SampleProjectApiError.values())); - - // Set the valid range of non-core error codes for this project to be 99100-99200. - private static final ProjectSpecificErrorCodeRange errorCodeRange = new ProjectSpecificErrorCodeRangeIntegerImpl( - 99100, 99200, "SAMPLE_PROJECT_API_ERRORS" - ); - - @Override - protected List getProjectSpecificApiErrors() { - return projectSpecificApiErrors; - } - - @Override - protected ProjectSpecificErrorCodeRange getProjectSpecificErrorCodeRange() { - return errorCodeRange; - } - -} diff --git a/samples/sample-jersey1/src/main/java/com/nike/backstopper/jersey1sample/model/RgbColor.java b/samples/sample-jersey1/src/main/java/com/nike/backstopper/jersey1sample/model/RgbColor.java deleted file mode 100644 index ec7fb06..0000000 --- a/samples/sample-jersey1/src/main/java/com/nike/backstopper/jersey1sample/model/RgbColor.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.nike.backstopper.jersey1sample.model; - -import com.fasterxml.jackson.annotation.JsonCreator; - -/** - * An enum used by {@link SampleModel} for showing how - * {@link com.nike.backstopper.validation.constraints.StringConvertsToClassType} can work with enums. Note - * the {@link #toRgbColor(String)} annotated with {@link JsonCreator}, which allows callers to pass in lower or - * mixed case versions of the enum values and still have them automatically deserialized to the correct enum. - * This special {@link JsonCreator} method is only necessary if you want to support case-insensitive enum validation - * when deserializing. - */ -public enum RgbColor { - RED, GREEN, BLUE; - - @JsonCreator - @SuppressWarnings("unused") - public static RgbColor toRgbColor(String colorString) { - for (RgbColor color : values()) { - if (color.name().equalsIgnoreCase(colorString)) - return color; - } - throw new IllegalArgumentException( - "Cannot convert the string: \"" + colorString + "\" to a valid RgbColor enum value." - ); - } -} diff --git a/samples/sample-jersey1/src/main/java/com/nike/backstopper/jersey1sample/model/SampleModel.java b/samples/sample-jersey1/src/main/java/com/nike/backstopper/jersey1sample/model/SampleModel.java deleted file mode 100644 index 854b6fa..0000000 --- a/samples/sample-jersey1/src/main/java/com/nike/backstopper/jersey1sample/model/SampleModel.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.nike.backstopper.jersey1sample.model; - -import com.nike.backstopper.apierror.ApiError; -import com.nike.backstopper.apierror.sample.SampleCoreApiError; -import com.nike.backstopper.jersey1sample.error.SampleProjectApiError; -import com.nike.backstopper.jersey1sample.error.SampleProjectApiErrorsImpl; -import com.nike.backstopper.validation.constraints.StringConvertsToClassType; - -import org.hibernate.validator.constraints.NotBlank; -import org.hibernate.validator.constraints.Range; - -import javax.validation.constraints.NotNull; - -/** - * Simple model class showing the JSR 303 Bean Validation integration in Backstopper. Each message for a JSR 303 - * annotation must match an {@link ApiError#getName()} from one of the errors returned by this project's - * {@link SampleProjectApiErrorsImpl#getProjectApiErrors()}. In this case that means you can use any of the enum - * names from {@link SampleCoreApiError} or {@link SampleProjectApiError}. - * - *

If you have a typo or forget to add a message that matches an error name then the {@code VerifyJsr303ContractTest} - * unit test will catch your error and the project will fail to build - the test will give you info on exactly which - * classes, fields, and annotations don't conform to the necessary convention. - */ -public class SampleModel { - @NotBlank(message = "FOO_STRING_CANNOT_BE_BLANK") - public final String foo; - - @Range(message = "INVALID_RANGE_VALUE", min = 0, max = 42) - public final String range_0_to_42; - - @NotNull(message = "RGB_COLOR_CANNOT_BE_NULL") - @StringConvertsToClassType( - message = "NOT_RGB_COLOR_ENUM", classType = RgbColor.class, allowCaseInsensitiveEnumMatch = true - ) - public final String rgb_color; - - public final Boolean throw_manual_error; - - @SuppressWarnings("unused") - // Intentionally protected - here for deserialization support. - protected SampleModel() { - this(null, null, null, null); - } - - public SampleModel(String foo, String range_0_to_42, String rgb_color, Boolean throw_manual_error) { - this.foo = foo; - this.range_0_to_42 = range_0_to_42; - this.rgb_color = rgb_color; - this.throw_manual_error = throw_manual_error; - } -} diff --git a/samples/sample-jersey1/src/main/java/com/nike/backstopper/jersey1sample/resource/SampleResource.java b/samples/sample-jersey1/src/main/java/com/nike/backstopper/jersey1sample/resource/SampleResource.java deleted file mode 100644 index 2a3e3bc..0000000 --- a/samples/sample-jersey1/src/main/java/com/nike/backstopper/jersey1sample/resource/SampleResource.java +++ /dev/null @@ -1,123 +0,0 @@ -package com.nike.backstopper.jersey1sample.resource; - -import com.nike.backstopper.apierror.sample.SampleCoreApiError; -import com.nike.backstopper.exception.ApiException; -import com.nike.backstopper.jersey1sample.error.SampleProjectApiError; -import com.nike.backstopper.jersey1sample.model.RgbColor; -import com.nike.backstopper.jersey1sample.model.SampleModel; -import com.nike.backstopper.service.ClientDataValidationService; -import com.nike.internal.util.Pair; - -import com.fasterxml.jackson.core.JsonProcessingException; - -import java.util.Arrays; -import java.util.UUID; - -import javax.ws.rs.Consumes; -import javax.ws.rs.GET; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; - -import static com.nike.backstopper.jersey1sample.resource.SampleResource.SAMPLE_PATH; -import static java.util.Collections.singletonList; - -/** - * Contains some sample endpoints. In particular {@link #postSampleModel(SampleModel)} is useful for showing the - * JSR 303 Bean Validation integration in Backstopper - see that method's source code for more info. - * - *

The {@code VerifyExpectedErrorsAreReturnedComponentTest} component test launches the server and exercises - * all these endpoints in various ways to verify the expected errors are returned using the expected error contract. - * - * @author Nic Munroe - */ -@Path(SAMPLE_PATH) -public class SampleResource { - - public static final String SAMPLE_PATH = "/sample"; - public static final String CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH = "/coreErrorWrapper"; - public static final String WITH_INT_QUERY_PARAM_SUBPATH = "/withIntQueryParam"; - public static final String TRIGGER_UNHANDLED_ERROR_SUBPATH = "/triggerUnhandledError"; - - public static int nextRangeInt(int lowerBound, int upperBound) { - return (int)Math.round(Math.random() * upperBound) + lowerBound; - } - - public static RgbColor nextRandomColor() { - return RgbColor.values()[nextRangeInt(0, 2)]; - } - - private final ClientDataValidationService validationService; - - public SampleResource(ClientDataValidationService validationService) { - if (validationService == null) - throw new IllegalArgumentException("validationService cannot be null"); - - this.validationService = validationService; - } - - @GET - @Produces(MediaType.APPLICATION_JSON) - public SampleModel getSampleModel() throws JsonProcessingException { - return new SampleModel( - UUID.randomUUID().toString(), String.valueOf(nextRangeInt(0, 42)), nextRandomColor().name(), false - ); - } - - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public Response postSampleModel(SampleModel model) throws JsonProcessingException { - // If the caller didn't pass us a payload we throw a MISSING_EXPECTED_CONTENT error. - if (model == null) { - throw ApiException.newBuilder() - .withExceptionMessage("Caller did not pass any request payload") - .withApiErrors(SampleCoreApiError.MISSING_EXPECTED_CONTENT) - .build(); - } - // Run the object through our JSR 303 validation service, which will automatically throw an appropriate error - // if the validation fails that will get translated to the desired error contract. - validationService.validateObjectsFailFast(model); - - // Manually check the throwManualError query param (normally you'd do this with JSR 303 annotations on the - // object, but this shows how you can manually throw exceptions to be picked up by the error handling system). - if (Boolean.TRUE.equals(model.throw_manual_error)) { - throw ApiException.newBuilder() - .withExceptionMessage("Manual error throw was requested") - .withApiErrors(SampleProjectApiError.MANUALLY_THROWN_ERROR) - .withExtraDetailsForLogging(Pair.of("rgb_color_value", model.rgb_color)) - .withExtraResponseHeaders( - Pair.of("rgbColorValue", singletonList(model.rgb_color)), - Pair.of("otherExtraMultivalueHeader", Arrays.asList("foo", "bar")) - ) - .build(); - } - - return Response.status(201).entity(model).build(); - } - - @GET - @Path(CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH) - public String failWithCoreErrorWrapper() { - throw ApiException.newBuilder() - .withExceptionMessage("Throwing error due to 'reasons'") - .withApiErrors(SampleProjectApiError.SOME_MEANINGFUL_ERROR_NAME) - .build(); - } - - @GET - @Path(WITH_INT_QUERY_PARAM_SUBPATH) - @Produces(MediaType.TEXT_PLAIN) - public String withIntQueryParam(@QueryParam("requiredQueryParamValue") Integer someRequiredQueryParam) { - return "You passed in " + someRequiredQueryParam + " for the required query param value"; - } - - @GET - @Path(TRIGGER_UNHANDLED_ERROR_SUBPATH) - public String triggerUnhandledError() { - throw new RuntimeException("This should be handled by Jersey1UnhandledExceptionHandler."); - } -} diff --git a/samples/sample-jersey1/src/main/resources/logback.xml b/samples/sample-jersey1/src/main/resources/logback.xml deleted file mode 100644 index 80adb28..0000000 --- a/samples/sample-jersey1/src/main/resources/logback.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - %d{yyyy-MM-dd_HH:mm:ss.SSS} [%thread] |-%-5level %logger{36} - %msg%n - - - - - - - \ No newline at end of file diff --git a/samples/sample-jersey1/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/ApplicationJsr303AnnotationTroller.java b/samples/sample-jersey1/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/ApplicationJsr303AnnotationTroller.java deleted file mode 100644 index 1445bf0..0000000 --- a/samples/sample-jersey1/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/ApplicationJsr303AnnotationTroller.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.nike.backstopper.apierror.contract.jsr303convention; - -import com.nike.internal.util.Pair; - -import java.lang.annotation.Annotation; -import java.lang.reflect.AnnotatedElement; -import java.util.List; -import java.util.function.Predicate; - -/** - * Extension of {@link ReflectionBasedJsr303AnnotationTrollerBase} for use with this sample project. This is used by JSR - * 303 annotation convention enforcement tests (e.g. {@link VerifyJsr303ContractTest} and {@link - * VerifyStringConvertsToClassTypeAnnotationsAreValidTest}). - * - *

NOTE: If you want to exclude classes or specific JSR 303 annotations from triggering convention violation errors - * in those tests you can do so by populating the return values of the {@link #ignoreAllAnnotationsAssociatedWithTheseProjectClasses()} - * and {@link #specificAnnotationDeclarationExclusionsForProject()} methods. IMPORTANT - this should only be done - * if you *really* know what you're doing. Usually it's only done for unit test classes that are intended to violate the - * convention. It should not be done for production code under normal circumstances. See the javadocs for the super - * class for those methods if you need to use them. - * - * @author Nic Munroe - */ -public final class ApplicationJsr303AnnotationTroller extends ReflectionBasedJsr303AnnotationTrollerBase { - - public static final ApplicationJsr303AnnotationTroller INSTANCE = new ApplicationJsr303AnnotationTroller(); - - public static ApplicationJsr303AnnotationTroller getInstance() { - return INSTANCE; - } - - // Intentionally private - use {@code getInstance()} to retrieve the singleton instance of this class. - private ApplicationJsr303AnnotationTroller() { - super(); - } - - @Override - protected List> ignoreAllAnnotationsAssociatedWithTheseProjectClasses() { - return null; - } - - @Override - protected List>> specificAnnotationDeclarationExclusionsForProject() { - return null; - } -} diff --git a/samples/sample-jersey1/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyJsr303ContractTest.java b/samples/sample-jersey1/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyJsr303ContractTest.java deleted file mode 100644 index 5026614..0000000 --- a/samples/sample-jersey1/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyJsr303ContractTest.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.nike.backstopper.apierror.contract.jsr303convention; - -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.jersey1sample.error.SampleProjectApiErrorsImpl; - -/** - * Verifies that *ALL* non-excluded JSR 303 validation annotations in this project have a message defined that maps to a - * {@link com.nike.backstopper.apierror.ApiError} enum name from this project's {@link ProjectApiErrors}. This is how - * the JSR 303 Bean Validation system is connected to the Backstopper error handling system and you should NOT disable - * these tests. - * - *

You can exclude annotation declarations (e.g. for unit test classes that are intended to violate the naming - * convention) by making sure that the {@link ApplicationJsr303AnnotationTroller#ignoreAllAnnotationsAssociatedWithTheseProjectClasses()} - * and {@link ApplicationJsr303AnnotationTroller#specificAnnotationDeclarationExclusionsForProject()} methods return - * what you need, but you should not exclude any annotations in production code under normal circumstances. - * - * @author Nic Munroe - */ -public class VerifyJsr303ContractTest extends VerifyJsr303ValidationMessagesPointToApiErrorsTest { - - private static final ProjectApiErrors PROJECT_API_ERRORS = new SampleProjectApiErrorsImpl(); - - @Override - protected ReflectionBasedJsr303AnnotationTrollerBase getAnnotationTroller() { - return ApplicationJsr303AnnotationTroller.getInstance(); - } - - @Override - protected ProjectApiErrors getProjectApiErrors() { - return PROJECT_API_ERRORS; - } -} diff --git a/samples/sample-jersey1/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyStringConvertsToClassTypeAnnotationsAreValidTest.java b/samples/sample-jersey1/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyStringConvertsToClassTypeAnnotationsAreValidTest.java deleted file mode 100644 index 3babd32..0000000 --- a/samples/sample-jersey1/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyStringConvertsToClassTypeAnnotationsAreValidTest.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.nike.backstopper.apierror.contract.jsr303convention; - -import com.nike.backstopper.validation.constraints.StringConvertsToClassType; - -/** - * Makes sure that any Enums referenced by {@link StringConvertsToClassType} JSR 303 annotations are case insensitive if - * they are marked with {@link StringConvertsToClassType#allowCaseInsensitiveEnumMatch()} set to true. - * - *

You can exclude annotation declarations (e.g. for unit test classes that are intended to violate the naming - * convention) by making sure that the {@link ApplicationJsr303AnnotationTroller#ignoreAllAnnotationsAssociatedWithTheseProjectClasses()} - * and {@link ApplicationJsr303AnnotationTroller#specificAnnotationDeclarationExclusionsForProject()} methods return - * what you need, but you should not exclude any annotations in production code under normal circumstances. - * - * @author Nic Munroe - */ -public class VerifyStringConvertsToClassTypeAnnotationsAreValidTest - extends VerifyEnumsReferencedByStringConvertsToClassTypeJsr303AnnotationsAreJacksonCaseInsensitiveTest { - - @Override - protected ReflectionBasedJsr303AnnotationTrollerBase getAnnotationTroller() { - return ApplicationJsr303AnnotationTroller.getInstance(); - } -} diff --git a/samples/sample-jersey1/src/test/java/com/nike/backstopper/jersey1sample/componenttest/VerifyExpectedErrorsAreReturnedComponentTest.java b/samples/sample-jersey1/src/test/java/com/nike/backstopper/jersey1sample/componenttest/VerifyExpectedErrorsAreReturnedComponentTest.java deleted file mode 100644 index 296865c..0000000 --- a/samples/sample-jersey1/src/test/java/com/nike/backstopper/jersey1sample/componenttest/VerifyExpectedErrorsAreReturnedComponentTest.java +++ /dev/null @@ -1,424 +0,0 @@ -package com.nike.backstopper.jersey1sample.componenttest; - -import com.nike.backstopper.apierror.ApiError; -import com.nike.backstopper.apierror.ApiErrorWithMetadata; -import com.nike.backstopper.apierror.sample.SampleCoreApiError; -import com.nike.backstopper.jersey1sample.Main; -import com.nike.backstopper.jersey1sample.error.SampleProjectApiError; -import com.nike.backstopper.jersey1sample.model.RgbColor; -import com.nike.backstopper.jersey1sample.model.SampleModel; -import com.nike.backstopper.model.DefaultErrorContractDTO; -import com.nike.backstopper.model.DefaultErrorDTO; -import com.nike.internal.util.MapBuilder; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; - -import org.eclipse.jetty.server.Server; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -import io.restassured.http.ContentType; -import io.restassured.response.ExtractableResponse; - -import static com.nike.backstopper.jersey1sample.error.SampleProjectApiError.INVALID_RANGE_VALUE; -import static com.nike.backstopper.jersey1sample.error.SampleProjectApiError.NOT_RGB_COLOR_ENUM; -import static com.nike.backstopper.jersey1sample.error.SampleProjectApiError.RGB_COLOR_CANNOT_BE_NULL; -import static com.nike.backstopper.jersey1sample.resource.SampleResource.CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH; -import static com.nike.backstopper.jersey1sample.resource.SampleResource.SAMPLE_PATH; -import static com.nike.backstopper.jersey1sample.resource.SampleResource.TRIGGER_UNHANDLED_ERROR_SUBPATH; -import static com.nike.backstopper.jersey1sample.resource.SampleResource.WITH_INT_QUERY_PARAM_SUBPATH; -import static com.nike.backstopper.jersey1sample.resource.SampleResource.nextRandomColor; -import static com.nike.backstopper.jersey1sample.resource.SampleResource.nextRangeInt; -import static com.nike.internal.util.testing.TestUtils.findFreePort; -import static io.restassured.RestAssured.given; -import static java.util.Collections.singleton; -import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Component test that starts up the sample server and hits it with requests that should generate specific errors. - * - * @author Nic Munroe - */ -public class VerifyExpectedErrorsAreReturnedComponentTest { - - private static final int SERVER_PORT = findFreePort(); - private static Server server; - - private static final ObjectMapper objectMapper = new ObjectMapper(); - - @BeforeAll - public static void beforeClass() throws Exception { - server = Main.createServer(SERVER_PORT); - server.start(); - for (int i = 0; i < 100; i++) { - if (server.isStarted()) - return; - Thread.sleep(100); - } - throw new IllegalStateException("Server is not up after waiting 10 seconds. Aborting tests."); - } - - @AfterAll - public static void afterClass() throws Exception { - if (server != null) { - server.stop(); - server.destroy(); - } - } - - private void verifyErrorReceived(ExtractableResponse response, ApiError expectedError) { - verifyErrorReceived(response, singleton(expectedError), expectedError.getHttpStatusCode()); - } - - private DefaultErrorDTO findErrorMatching(DefaultErrorContractDTO errorContract, ApiError desiredError) { - for (DefaultErrorDTO error : errorContract.errors) { - if (error.code.equals(desiredError.getErrorCode()) && error.message.equals(desiredError.getMessage())) - return error; - } - - return null; - } - - private void verifyErrorReceived(ExtractableResponse response, Collection expectedErrors, int expectedHttpStatusCode) { - assertThat(response.statusCode()).isEqualTo(expectedHttpStatusCode); - try { - DefaultErrorContractDTO errorContract = objectMapper.readValue(response.asString(), DefaultErrorContractDTO.class); - assertThat(errorContract.error_id).isNotEmpty(); - assertThat(UUID.fromString(errorContract.error_id)).isNotNull(); - assertThat(errorContract.errors).hasSameSizeAs(expectedErrors); - for (ApiError apiError : expectedErrors) { - DefaultErrorDTO matchingError = findErrorMatching(errorContract, apiError); - assertThat(matchingError).isNotNull(); - assertThat(matchingError.code).isEqualTo(apiError.getErrorCode()); - assertThat(matchingError.message).isEqualTo(apiError.getMessage()); - assertThat(matchingError.metadata).isEqualTo(apiError.getMetadata()); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private SampleModel randomizedSampleModel() { - return new SampleModel(UUID.randomUUID().toString(), String.valueOf(nextRangeInt(0, 42)), nextRandomColor().name(), false); - } - - // *************** SUCCESSFUL (NON ERROR) CALLS ****************** - @Test - public void verify_basic_sample_get() throws IOException { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .basePath(SAMPLE_PATH) - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - assertThat(response.statusCode()).isEqualTo(200); - SampleModel responseBody = objectMapper.readValue(response.asString(), SampleModel.class); - assertThat(responseBody).isNotNull(); - assertThat(responseBody.foo).isNotEmpty(); - assertThat(responseBody.range_0_to_42).isNotEmpty(); - assertThat(Integer.parseInt(responseBody.range_0_to_42)).isBetween(0, 42); - assertThat(responseBody.rgb_color).isNotEmpty(); - assertThat(RgbColor.toRgbColor(responseBody.rgb_color)).isNotNull(); - assertThat(responseBody.throw_manual_error).isFalse(); - } - - @Test - public void verify_basic_sample_post() throws IOException { - SampleModel requestPayload = randomizedSampleModel(); - String requestPayloadAsString = objectMapper.writeValueAsString(requestPayload); - - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .contentType(ContentType.JSON) - .basePath(SAMPLE_PATH) - .body(requestPayloadAsString) - .log().all() - .when() - .post() - .then() - .log().all() - .extract(); - - assertThat(response.statusCode()).isEqualTo(201); - SampleModel responseBody = objectMapper.readValue(response.asString(), SampleModel.class); - assertThat(responseBody).isNotNull(); - assertThat(responseBody.foo).isEqualTo(requestPayload.foo); - assertThat(responseBody.range_0_to_42).isEqualTo(requestPayload.range_0_to_42); - assertThat(responseBody.rgb_color).isEqualTo(requestPayload.rgb_color); - assertThat(responseBody.throw_manual_error).isEqualTo(requestPayload.throw_manual_error); - } - - // *************** JSR 303 AND ENDPOINT ERRORS ****************** - - @CsvSource( - value = { - "null | 42 | GREEN | FOO_STRING_CANNOT_BE_BLANK | 400", - "bar | -1 | GREEN | INVALID_RANGE_VALUE | 400", - "bar | 42 | null | RGB_COLOR_CANNOT_BE_NULL | 400", - "bar | 42 | car | NOT_RGB_COLOR_ENUM | 400", - " | 99 | tree | FOO_STRING_CANNOT_BE_BLANK,INVALID_RANGE_VALUE,NOT_RGB_COLOR_ENUM | 400", - }, - delimiter = '|', - nullValues = { "null" } - ) - @ParameterizedTest - public void verify_jsr303_validation_errors( - String fooString, String rangeString, String rgbColorString, - String expectedErrorsComboString, int expectedResponseHttpStatusCode) throws JsonProcessingException - { - SampleModel requestPayload = new SampleModel(fooString, rangeString, rgbColorString, false); - String requestPayloadAsString = objectMapper.writeValueAsString(requestPayload); - - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .contentType(ContentType.JSON) - .basePath(SAMPLE_PATH) - .body(requestPayloadAsString) - .log().all() - .when() - .post() - .then() - .log().all() - .extract(); - - String[] expectedErrorsArray = expectedErrorsComboString.split(","); - List expectedErrors = new ArrayList<>(); - for (String errorStr : expectedErrorsArray) { - ApiError apiError = SampleProjectApiError.valueOf(errorStr); - String extraMetadataFieldValue = null; - - if (INVALID_RANGE_VALUE.equals(apiError)) - extraMetadataFieldValue = "range_0_to_42"; - else if (RGB_COLOR_CANNOT_BE_NULL.equals(apiError) || NOT_RGB_COLOR_ENUM.equals(apiError)) - extraMetadataFieldValue = "rgb_color"; - - if (extraMetadataFieldValue != null) - apiError = new ApiErrorWithMetadata(apiError, MapBuilder.builder("field", (Object)extraMetadataFieldValue).build()); - - expectedErrors.add(apiError); - } - verifyErrorReceived(response, expectedErrors, expectedResponseHttpStatusCode); - } - - @Test - public void verify_sample_post_fails_with_MISSING_EXPECTED_CONTENT_if_passed_empty_body() { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .basePath(SAMPLE_PATH) - .contentType(ContentType.JSON) - .body("") - .log().all() - .when() - .post() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleCoreApiError.MISSING_EXPECTED_CONTENT); - } - - @Test - public void verify_MANUALLY_THROWN_ERROR_is_thrown_when_requested() throws IOException { - SampleModel requestPayload = new SampleModel("bar", "42", "RED", true); - String requestPayloadAsString = objectMapper.writeValueAsString(requestPayload); - - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .contentType(ContentType.JSON) - .basePath(SAMPLE_PATH) - .body(requestPayloadAsString) - .log().all() - .when() - .post() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleProjectApiError.MANUALLY_THROWN_ERROR); - // This code path also should add some custom headers to the response - assertThat(response.headers().getValues("rgbColorValue")).isEqualTo(singletonList(requestPayload.rgb_color)); - assertThat(response.headers().getValues("otherExtraMultivalueHeader")).isEqualTo(Arrays.asList("foo", "bar")); - } - - @Test - public void verify_SOME_MEANINGFUL_ERROR_NAME_is_thrown_when_correct_endpoint_is_hit() { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .basePath(SAMPLE_PATH + CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH) - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleProjectApiError.SOME_MEANINGFUL_ERROR_NAME); - } - - @Test - public void verify_GENERIC_SERVICE_ERROR_is_thrown_when_correct_endpoint_is_hit() { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .basePath(SAMPLE_PATH + TRIGGER_UNHANDLED_ERROR_SUBPATH) - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleCoreApiError.GENERIC_SERVICE_ERROR); - } - - // *************** FRAMEWORK ERRORS ****************** - - @Test - public void verify_NOT_FOUND_returned_if_unknown_path_is_requested() { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .basePath(UUID.randomUUID().toString()) - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleCoreApiError.NOT_FOUND); - } - - @Test - public void verify_METHOD_NOT_ALLOWED_returned_if_known_path_is_requested_with_invalid_http_method() { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .basePath(SAMPLE_PATH) - .log().all() - .when() - .delete() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleCoreApiError.METHOD_NOT_ALLOWED); - } - - @Test - public void verify_sample_get_fails_with_NO_ACCEPTABLE_REPRESENTATION_if_passed_invalid_accept_header() { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .basePath(SAMPLE_PATH) - .accept(ContentType.BINARY) - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleCoreApiError.NO_ACCEPTABLE_REPRESENTATION); - } - - @Test - public void verify_sample_post_fails_with_UNSUPPORTED_MEDIA_TYPE_if_passed_invalid_content_type() throws IOException { - SampleModel requestPayload = randomizedSampleModel(); - String requestPayloadAsString = objectMapper.writeValueAsString(requestPayload); - - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .basePath(SAMPLE_PATH) - .body(requestPayloadAsString) - .contentType(ContentType.TEXT) - .log().all() - .when() - .post() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleCoreApiError.UNSUPPORTED_MEDIA_TYPE); - } - - @Test - public void verify_NOT_FOUND_is_thrown_when_framework_cannot_convert_query_param_type() { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .basePath(SAMPLE_PATH + WITH_INT_QUERY_PARAM_SUBPATH) - .queryParam("requiredQueryParamValue", "not-an-integer") - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleCoreApiError.NOT_FOUND); - } - - @Test - public void verify_sample_post_fails_with_MALFORMED_REQUEST_if_passed_bad_json_body() throws IOException { - SampleModel originalValidPayloadObj = randomizedSampleModel(); - String originalValidPayloadAsString = objectMapper.writeValueAsString(originalValidPayloadObj); - @SuppressWarnings("unchecked") - Map badRequestPayloadAsMap = objectMapper.readValue(originalValidPayloadAsString, Map.class); - badRequestPayloadAsMap.put("throw_manual_error", "not-a-boolean"); - String badJsonPayloadAsString = objectMapper.writeValueAsString(badRequestPayloadAsMap); - - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .basePath(SAMPLE_PATH) - .contentType(ContentType.JSON) - .body(badJsonPayloadAsString) - .log().all() - .when() - .post() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleCoreApiError.MALFORMED_REQUEST); - } - -} diff --git a/samples/sample-jersey1/src/test/java/com/nike/backstopper/jersey1sample/error/SampleProjectApiErrorsImplTest.java b/samples/sample-jersey1/src/test/java/com/nike/backstopper/jersey1sample/error/SampleProjectApiErrorsImplTest.java deleted file mode 100644 index de9e65c..0000000 --- a/samples/sample-jersey1/src/test/java/com/nike/backstopper/jersey1sample/error/SampleProjectApiErrorsImplTest.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.nike.backstopper.jersey1sample.error; - -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrorsTestBase; - -/** - * Extends {@link ProjectApiErrorsTestBase} in order to inherit tests that will verify the correctness of this - * project's {@link SampleProjectApiErrorsImpl}. - * - * @author Nic Munroe - */ -public class SampleProjectApiErrorsImplTest extends ProjectApiErrorsTestBase { - - ProjectApiErrors projectApiErrors = new SampleProjectApiErrorsImpl(); - - @Override - protected ProjectApiErrors getProjectApiErrors() { - return projectApiErrors; - } -} \ No newline at end of file diff --git a/samples/sample-jersey2/README.md b/samples/sample-jersey2/README.md deleted file mode 100644 index c128a88..0000000 --- a/samples/sample-jersey2/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# Backstopper Sample Application - jersey2 - -Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 7 and greater. - -This submodule contains a sample application based on Jersey 2 that fully integrates Backstopper. - -* Build the sample by running the `./buildSample.sh` script. -* Launch the sample by running the `./runSample.sh` script. It will bind to port 8080 by default. - * You can override the default port by passing in a system property to the run script, e.g. to bind to port 8181: `./runSample.sh -Djersey2Sample.server.port=8181` - -## Things to try - -All examples here assume the sample app is running on port 8080, so you would hit each path by going to `http://localhost:8080/[endpoint-path]`. It's recommended that you use a REST client like [Postman](https://www.getpostman.com/) for making the requests so you can easily specify HTTP method, payloads, headers, etc, and fully inspect the response. - -Also note that all the following things to try are verified in a component test: `VerifyExpectedErrorsAreReturnedComponentTest`. If you prefer to experiment via code you can run, debug, and otherwise explore that test. - -As you are doing the following you should check the logs that are output by the sample application and notice what is included in the log messages. In particular notice how you can search for an `error_id` that came from an error response and go directly to the relevant log message in the logs. Also notice how the `ApiError.getName()` value shows up in the logs for each error represented in a returned error contract (there can be more than one per request). - -* `GET /sample` - Returns the JSON serialization for the `SampleModel` model object. You can copy this into a `POST` call to experiment with triggering errors. -* `POST /sample` with `ContentType: application/json` header - Using the JSON model retrieved by the `GET` call, you can trigger numerous different types of errors, all of which get caught by the Backstopper system and converted into the appropriate error contract. - * Omit the `foo` field. - * Set the value of the `range_0_to_42` field to something outside of the allowed 0-42 range. - * Set the value of the `rgb_color` field to something besides `RED`, `GREEN`, or `BLUE`, or omit it entirely. Note that the validation and deserialization of this enum field is done in a case insensitive manner - i.e. you can pass `red`, `Green`, or `bLuE` if you want and it will not throw an error. - * Set two or more invalid values for `foo`, `range_0_to_42`, and `rgb_color` to invalid values all at once - notice you get back all relevant errors at once in the same error contract. - * Set `throw_manual_error` to true to trigger a manual exception to be thrown inside the normal `POST /sample` endpoint. - * Note the extra response headers that are included when you do this, and how they relate to the `.withExtraResponseHeaders(...)` method call on the builder of the exception that is thrown. - * Pass in an empty JSON payload - you should receive a `"Missing expected content"` error back. - * Pass in a junk payload that is not valid JSON - you should receive a `"Malformed request"` error back. -* `GET /sample/coreErrorWrapper` - Triggers an error to be thrown that appears to the caller like a normal generic service exception, but the `SOME_MEANINGFUL_ERROR_NAME` name from the `ApiError` it represents shows up in the logs to help you disambiguate what the true cause was. -* `GET /sample/triggerUnhandledError` - Triggers an error that is caught by the unhandled exception handler portion of Backstopper and converted to a generic service exception. -* `GET /sample/throwExceptionFromAsyncEndpoint` - Hits an async endpoint (using Jersey's `@Suspended AsyncResponse` -feature) that throws an exception (shows that Backstopper handles async endpoints that throw exceptions). -* `GET /sample/resumeAsyncResponseWithException` - Hits an async endpoint (using Jersey's `@Suspended AsyncResponse` -feature) that resumes the async response via `AsyncResponse.resume(Throwable)` (shows that Backstopper handles async -endpoints that explicitly resume the response with an exception from another thread). -* `GET /does-not-exist` - Triggers a framework 404 which Backstopper handles. -* `DELETE /sample` - Triggers a framework 405 which Backstopper handles. -* `GET /sample` with `Accept: application/octet-stream` header - Triggers a framework 406 which Backstopper handles. -* `POST /sample` with `ContentType: text/plain` - Triggers a framework 415 which Backstopper handles. - -## More Info - -See the [base project README.md](../../README.md), [User Guide](../../USER_GUIDE.md), and Backstopper repository source code and javadocs for all further information. - -## License - -Backstopper is released under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/samples/sample-jersey2/build.gradle b/samples/sample-jersey2/build.gradle deleted file mode 100644 index 9d9213a..0000000 --- a/samples/sample-jersey2/build.gradle +++ /dev/null @@ -1,43 +0,0 @@ -evaluationDependsOn(':') - -sourceCompatibility = JavaVersion.VERSION_1_8 -targetCompatibility = JavaVersion.VERSION_1_8 - -test { - useJUnitPlatform() -} - -dependencies { - implementation( - project(":backstopper-jersey2"), - project(":backstopper-custom-validators"), - "ch.qos.logback:logback-classic:$logbackVersion", - "org.hibernate:hibernate-validator:$hibernateValidatorVersion", - "org.glassfish.jersey.core:jersey-server:$jersey2Version", - "org.glassfish.jersey.containers:jersey-container-servlet:$jersey2Version", - "org.glassfish.jersey.media:jersey-media-json-jackson:$jersey2Version", - "org.eclipse.jetty:jetty-server:$jettyVersion", - "org.eclipse.jetty:jetty-servlet:$jettyVersion" - ) - testImplementation( - project(":backstopper-reusable-tests-junit5"), - "org.junit.jupiter:junit-jupiter-api:$junit5Version", - "org.junit.jupiter:junit-jupiter-engine:$junit5Version", - "org.junit.jupiter:junit-jupiter-params:$junit5Version", - "org.mockito:mockito-core:$mockitoVersion", - "com.fasterxml.jackson.core:jackson-core:$jacksonVersion", - "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion", - "ch.qos.logback:logback-classic:$logbackVersion", - "org.assertj:assertj-core:$assertJVersion", - "io.rest-assured:rest-assured:$restAssuredVersion", - // The jaxb-api is needed for building on the java 11 JDK as these classes were moved out of the Java SE libs. - "javax.xml.bind:jaxb-api:$jaxbApiVersion", - ) -} - -apply plugin: "application" -mainClassName = "com.nike.backstopper.jersey2sample.Main" - -run { - systemProperties = System.getProperties() -} diff --git a/samples/sample-jersey2/buildSample.sh b/samples/sample-jersey2/buildSample.sh deleted file mode 100755 index 2effeba..0000000 --- a/samples/sample-jersey2/buildSample.sh +++ /dev/null @@ -1,2 +0,0 @@ -echo "../../gradlew clean build" -../../gradlew clean build \ No newline at end of file diff --git a/samples/sample-jersey2/runSample.sh b/samples/sample-jersey2/runSample.sh deleted file mode 100755 index 65ac084..0000000 --- a/samples/sample-jersey2/runSample.sh +++ /dev/null @@ -1,3 +0,0 @@ -echo "../../gradlew run" -echo "NOTE: Type ctrl+c to stop" -../../gradlew run $* \ No newline at end of file diff --git a/samples/sample-jersey2/src/main/java/com/nike/backstopper/jersey2sample/Main.java b/samples/sample-jersey2/src/main/java/com/nike/backstopper/jersey2sample/Main.java deleted file mode 100644 index 4c7b7fd..0000000 --- a/samples/sample-jersey2/src/main/java/com/nike/backstopper/jersey2sample/Main.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.nike.backstopper.jersey2sample; - -import com.nike.backstopper.jersey2sample.config.Jersey2SampleResourceConfig; - -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHolder; -import org.glassfish.jersey.servlet.ServletContainer; - -import java.io.IOException; - -/** - * Starts up the Backstopper Jersey 2 Sample server (on port 8080 by default). - * - * @author Nic Munroe - */ -public class Main { - - @SuppressWarnings("WeakerAccess") - public static final String PORT_SYSTEM_PROP_KEY = "jersey2Sample.server.port"; - - public static void main(String[] args) throws Exception { - Server server = createServer(Integer.parseInt(System.getProperty(PORT_SYSTEM_PROP_KEY, "8080"))); - - try { - server.start(); - server.join(); - } - finally { - server.destroy(); - } - } - - public static Server createServer(int port) throws Exception { - Server server = new Server(port); - server.setHandler(generateServletContextHandler()); - - return server; - } - - private static ServletContextHandler generateServletContextHandler() throws IOException { - ServletContextHandler contextHandler = new ServletContextHandler(); - contextHandler.setContextPath("/"); - - ServletContainer jerseyServletContainer = new ServletContainer(new Jersey2SampleResourceConfig()); - contextHandler.addServlet(new ServletHolder(jerseyServletContainer), "/*"); - return contextHandler; - } -} diff --git a/samples/sample-jersey2/src/main/java/com/nike/backstopper/jersey2sample/config/Jersey2SampleResourceConfig.java b/samples/sample-jersey2/src/main/java/com/nike/backstopper/jersey2sample/config/Jersey2SampleResourceConfig.java deleted file mode 100644 index 449be3e..0000000 --- a/samples/sample-jersey2/src/main/java/com/nike/backstopper/jersey2sample/config/Jersey2SampleResourceConfig.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.nike.backstopper.jersey2sample.config; - -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.handler.ApiExceptionHandlerUtils; -import com.nike.backstopper.handler.jersey2.Jersey2ApiExceptionHandler; -import com.nike.backstopper.handler.jersey2.config.Jersey2BackstopperConfigHelper; -import com.nike.backstopper.jersey2sample.error.SampleProjectApiErrorsImpl; -import com.nike.backstopper.jersey2sample.resource.SampleResource; -import com.nike.backstopper.service.ClientDataValidationService; - -import org.glassfish.jersey.internal.ExceptionMapperFactory; -import org.glassfish.jersey.server.ResourceConfig; - -import javax.validation.Validation; -import javax.validation.Validator; - -/** - * This {@link ResourceConfig} will setup the Jersey 2 Sample App with a {@link Jersey2ApiExceptionHandler} for handling - * all errors, and {@link SampleResource} for handling requests. - * - *

NOTE: There are probably better Jersey 2 idiomatic ways to wire up the dependencies than manually creating the - * objects and passing them to {@link ResourceConfig#register(Object)}. If anyone out there is good with Jersey 2 please - * feel free to submit a pull request. - * - *

ALSO NOTE: The hack we're doing in {@link - * Jersey2BackstopperConfigHelper#setupJersey2ResourceConfigForBackstopperExceptionHandling(ResourceConfig, - * ProjectApiErrors, ApiExceptionHandlerUtils)} to override the default {@link ExceptionMapperFactory} in order to make - * sure our {@link Jersey2ApiExceptionHandler} is the only exception mapper that ever gets used is pretty ugly. There - * may or may not be better ways to do this - https://java.net/jira/browse/JERSEY-2437 and - * https://java.net/jira/browse/JERSEY-2722 and seem to be blockers to a clean solution, but if you have a better one - * please feel free to submit a pull request. - * - * @author Nic Munroe - */ -@SuppressWarnings("WeakerAccess") -public class Jersey2SampleResourceConfig extends ResourceConfig { - - protected static final ProjectApiErrors projectApiErrors = new SampleProjectApiErrorsImpl(); - - public Jersey2SampleResourceConfig() { - Jersey2BackstopperConfigHelper.setupJersey2ResourceConfigForBackstopperExceptionHandling( - this, projectApiErrors, ApiExceptionHandlerUtils.DEFAULT_IMPL - ); - register(generateSampleResource()); - } - - protected SampleResource generateSampleResource() { - Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); - return new SampleResource(new ClientDataValidationService(validator)); - } - -} diff --git a/samples/sample-jersey2/src/main/java/com/nike/backstopper/jersey2sample/error/SampleProjectApiError.java b/samples/sample-jersey2/src/main/java/com/nike/backstopper/jersey2sample/error/SampleProjectApiError.java deleted file mode 100644 index 65490bc..0000000 --- a/samples/sample-jersey2/src/main/java/com/nike/backstopper/jersey2sample/error/SampleProjectApiError.java +++ /dev/null @@ -1,91 +0,0 @@ -package com.nike.backstopper.jersey2sample.error; - -import com.nike.backstopper.apierror.ApiError; -import com.nike.backstopper.apierror.ApiErrorBase; -import com.nike.backstopper.apierror.ApiErrorWithMetadata; -import com.nike.backstopper.apierror.sample.SampleCoreApiError; -import com.nike.backstopper.jersey2sample.model.RgbColor; -import com.nike.internal.util.MapBuilder; - -import java.util.Arrays; -import java.util.Map; -import java.util.UUID; - -import javax.ws.rs.core.Response.Status; - -/** - * Project-specific error definitions for this sample app. Note that the error codes for errors specified here must - * conform to the range specified in {@link SampleProjectApiErrorsImpl#getProjectSpecificErrorCodeRange()} or an - * exception will be thrown on app startup, and unit tests should fail. The one exception to this rule is a "core - * error wrapper" - an instance that shares the same error code, message, and HTTP status code as a - * {@link SampleProjectApiErrorsImpl#getCoreApiErrors()} instance (in this case that means a wrapper around - * {@link SampleCoreApiError}). - * - * @author Nic Munroe - */ -public enum SampleProjectApiError implements ApiError { - FIELD_CANNOT_BE_NULL_OR_BLANK(99100, "Field cannot be null or empty", Status.BAD_REQUEST.getStatusCode()), - // FOO_STRING_CANNOT_BE_BLANK shows how you can build off a base/generic error and add metadata. - FOO_STRING_CANNOT_BE_BLANK(FIELD_CANNOT_BE_NULL_OR_BLANK, MapBuilder.builder("field", (Object)"foo").build()), - INVALID_RANGE_VALUE(99110, "The range_0_to_42 field must be between 0 and 42 (inclusive)", - Status.BAD_REQUEST.getStatusCode()), - // RGB_COLOR_CANNOT_BE_NULL could build off FIELD_CANNOT_BE_NULL_OR_BLANK like FOO_STRING_CANNOT_BE_BLANK does, - // however this shows how you can make individual field errors with unique code and custom message. - RGB_COLOR_CANNOT_BE_NULL(99120, "The rgb_color field must be defined", Status.BAD_REQUEST.getStatusCode()), - NOT_RGB_COLOR_ENUM(99130, "The rgb_color field value must be one of: " + Arrays.toString(RgbColor.values()), - Status.BAD_REQUEST.getStatusCode()), - MANUALLY_THROWN_ERROR(99140, "You asked for an error to be thrown", Status.INTERNAL_SERVER_ERROR.getStatusCode()), - // This is a wrapper around a core error. It will have the same error code, message, and HTTP status code, - // but will show up in the logs with contributing_errors="SOME_MEANINGFUL_ERROR_NAME", allowing you to - // distinguish the context of the error vs. the core GENERIC_SERVICE_ERROR at a glance. - SOME_MEANINGFUL_ERROR_NAME(SampleCoreApiError.GENERIC_SERVICE_ERROR); - - private final ApiError delegate; - - SampleProjectApiError(ApiError delegate) { - this.delegate = delegate; - } - - SampleProjectApiError(ApiError delegate, Map metadata) { - this(new ApiErrorWithMetadata(delegate, metadata)); - } - - SampleProjectApiError(int errorCode, String message, int httpStatusCode) { - this(new ApiErrorBase( - "delegated-to-enum-wrapper-" + UUID.randomUUID().toString(), errorCode, message, httpStatusCode - )); - } - - @SuppressWarnings("unused") - SampleProjectApiError(int errorCode, String message, int httpStatusCode, Map metadata) { - this(new ApiErrorBase( - "delegated-to-enum-wrapper-" + UUID.randomUUID().toString(), errorCode, message, httpStatusCode, metadata - )); - } - - @Override - public String getName() { - return this.name(); - } - - @Override - public String getErrorCode() { - return delegate.getErrorCode(); - } - - @Override - public String getMessage() { - return delegate.getMessage(); - } - - @Override - public int getHttpStatusCode() { - return delegate.getHttpStatusCode(); - } - - @Override - public Map getMetadata() { - return delegate.getMetadata(); - } - -} \ No newline at end of file diff --git a/samples/sample-jersey2/src/main/java/com/nike/backstopper/jersey2sample/error/SampleProjectApiErrorsImpl.java b/samples/sample-jersey2/src/main/java/com/nike/backstopper/jersey2sample/error/SampleProjectApiErrorsImpl.java deleted file mode 100644 index d379b5b..0000000 --- a/samples/sample-jersey2/src/main/java/com/nike/backstopper/jersey2sample/error/SampleProjectApiErrorsImpl.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.nike.backstopper.jersey2sample.error; - -import com.nike.backstopper.apierror.ApiError; -import com.nike.backstopper.apierror.projectspecificinfo.ProjectSpecificErrorCodeRange; -import com.nike.backstopper.apierror.projectspecificinfo.ProjectSpecificErrorCodeRangeIntegerImpl; -import com.nike.backstopper.apierror.sample.SampleProjectApiErrorsBase; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import javax.inject.Singleton; - -/** - * Returns the project specific errors for this sample application. {@link #getProjectApiErrors()} will return a - * combination of {@link SampleProjectApiErrorsBase#getCoreApiErrors()} and {@link #getProjectSpecificApiErrors()}. - * This means that you have all the enum values of {@link com.nike.backstopper.apierror.sample.SampleCoreApiError} - * and {@link SampleProjectApiError} at your disposal when throwing errors in this sample app. - */ -@Singleton -public class SampleProjectApiErrorsImpl extends SampleProjectApiErrorsBase { - - private static final List projectSpecificApiErrors = - new ArrayList<>(Arrays.asList(SampleProjectApiError.values())); - - // Set the valid range of non-core error codes for this project to be 99100-99200. - private static final ProjectSpecificErrorCodeRange errorCodeRange = new ProjectSpecificErrorCodeRangeIntegerImpl( - 99100, 99200, "SAMPLE_PROJECT_API_ERRORS" - ); - - @Override - protected List getProjectSpecificApiErrors() { - return projectSpecificApiErrors; - } - - @Override - protected ProjectSpecificErrorCodeRange getProjectSpecificErrorCodeRange() { - return errorCodeRange; - } - -} diff --git a/samples/sample-jersey2/src/main/java/com/nike/backstopper/jersey2sample/model/RgbColor.java b/samples/sample-jersey2/src/main/java/com/nike/backstopper/jersey2sample/model/RgbColor.java deleted file mode 100644 index b327e61..0000000 --- a/samples/sample-jersey2/src/main/java/com/nike/backstopper/jersey2sample/model/RgbColor.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.nike.backstopper.jersey2sample.model; - -import com.fasterxml.jackson.annotation.JsonCreator; - -/** - * An enum used by {@link SampleModel} for showing how - * {@link com.nike.backstopper.validation.constraints.StringConvertsToClassType} can work with enums. Note - * the {@link #toRgbColor(String)} annotated with {@link JsonCreator}, which allows callers to pass in lower or - * mixed case versions of the enum values and still have them automatically deserialized to the correct enum. - * This special {@link JsonCreator} method is only necessary if you want to support case-insensitive enum validation - * when deserializing. - */ -public enum RgbColor { - RED, GREEN, BLUE; - - @JsonCreator - @SuppressWarnings("unused") - public static RgbColor toRgbColor(String colorString) { - for (RgbColor color : values()) { - if (color.name().equalsIgnoreCase(colorString)) - return color; - } - throw new IllegalArgumentException( - "Cannot convert the string: \"" + colorString + "\" to a valid RgbColor enum value." - ); - } -} diff --git a/samples/sample-jersey2/src/main/java/com/nike/backstopper/jersey2sample/model/SampleModel.java b/samples/sample-jersey2/src/main/java/com/nike/backstopper/jersey2sample/model/SampleModel.java deleted file mode 100644 index a12e102..0000000 --- a/samples/sample-jersey2/src/main/java/com/nike/backstopper/jersey2sample/model/SampleModel.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.nike.backstopper.jersey2sample.model; - -import com.nike.backstopper.apierror.ApiError; -import com.nike.backstopper.apierror.sample.SampleCoreApiError; -import com.nike.backstopper.jersey2sample.error.SampleProjectApiError; -import com.nike.backstopper.jersey2sample.error.SampleProjectApiErrorsImpl; -import com.nike.backstopper.validation.constraints.StringConvertsToClassType; - -import org.hibernate.validator.constraints.NotBlank; -import org.hibernate.validator.constraints.Range; - -import javax.validation.constraints.NotNull; - -/** - * Simple model class showing the JSR 303 Bean Validation integration in Backstopper. Each message for a JSR 303 - * annotation must match an {@link ApiError#getName()} from one of the errors returned by this project's - * {@link SampleProjectApiErrorsImpl#getProjectApiErrors()}. In this case that means you can use any of the enum - * names from {@link SampleCoreApiError} or {@link SampleProjectApiError}. - * - *

If you have a typo or forget to add a message that matches an error name then the {@code VerifyJsr303ContractTest} - * unit test will catch your error and the project will fail to build - the test will give you info on exactly which - * classes, fields, and annotations don't conform to the necessary convention. - */ -public class SampleModel { - @NotBlank(message = "FOO_STRING_CANNOT_BE_BLANK") - public final String foo; - - @Range(message = "INVALID_RANGE_VALUE", min = 0, max = 42) - public final String range_0_to_42; - - @NotNull(message = "RGB_COLOR_CANNOT_BE_NULL") - @StringConvertsToClassType( - message = "NOT_RGB_COLOR_ENUM", classType = RgbColor.class, allowCaseInsensitiveEnumMatch = true - ) - public final String rgb_color; - - public final Boolean throw_manual_error; - - @SuppressWarnings("unused") - // Intentionally protected - here for deserialization support. - protected SampleModel() { - this(null, null, null, null); - } - - public SampleModel(String foo, String range_0_to_42, String rgb_color, Boolean throw_manual_error) { - this.foo = foo; - this.range_0_to_42 = range_0_to_42; - this.rgb_color = rgb_color; - this.throw_manual_error = throw_manual_error; - } -} diff --git a/samples/sample-jersey2/src/main/java/com/nike/backstopper/jersey2sample/resource/SampleResource.java b/samples/sample-jersey2/src/main/java/com/nike/backstopper/jersey2sample/resource/SampleResource.java deleted file mode 100644 index a99125b..0000000 --- a/samples/sample-jersey2/src/main/java/com/nike/backstopper/jersey2sample/resource/SampleResource.java +++ /dev/null @@ -1,154 +0,0 @@ -package com.nike.backstopper.jersey2sample.resource; - -import com.nike.backstopper.apierror.sample.SampleCoreApiError; -import com.nike.backstopper.exception.ApiException; -import com.nike.backstopper.jersey2sample.error.SampleProjectApiError; -import com.nike.backstopper.jersey2sample.model.RgbColor; -import com.nike.backstopper.jersey2sample.model.SampleModel; -import com.nike.backstopper.service.ClientDataValidationService; -import com.nike.internal.util.Pair; - -import com.fasterxml.jackson.core.JsonProcessingException; - -import java.util.Arrays; -import java.util.UUID; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import javax.ws.rs.Consumes; -import javax.ws.rs.GET; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; -import javax.ws.rs.container.AsyncResponse; -import javax.ws.rs.container.Suspended; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; - -import static com.nike.backstopper.jersey2sample.resource.SampleResource.SAMPLE_PATH; -import static java.util.Collections.singletonList; - -/** - * Contains some sample endpoints. In particular {@link #postSampleModel(SampleModel)} is useful for showing the - * JSR 303 Bean Validation integration in Backstopper - see that method's source code for more info. - * - *

The {@code VerifyExpectedErrorsAreReturnedComponentTest} component test launches the server and exercises - * all these endpoints in various ways to verify the expected errors are returned using the expected error contract. - * - * @author Nic Munroe - */ -@Path(SAMPLE_PATH) -public class SampleResource { - - public static final String SAMPLE_PATH = "/sample"; - public static final String CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH = "/coreErrorWrapper"; - public static final String WITH_INT_QUERY_PARAM_SUBPATH = "/withIntQueryParam"; - public static final String TRIGGER_UNHANDLED_ERROR_SUBPATH = "/triggerUnhandledError"; - public static final String THROW_EXCEPTION_FROM_ASYNC_ENDPOINT_SUBPATH = "/throwExceptionFromAsyncEndpoint"; - public static final String RESUME_ASYNC_RESPONSE_WITH_EXCEPTION_SUBPATH = "/resumeAsyncResponseWithException"; - - private static final ExecutorService executor = Executors.newCachedThreadPool(); - - public static int nextRangeInt(int lowerBound, int upperBound) { - return (int)Math.round(Math.random() * upperBound) + lowerBound; - } - - public static RgbColor nextRandomColor() { - return RgbColor.values()[nextRangeInt(0, 2)]; - } - - private final ClientDataValidationService validationService; - - public SampleResource(ClientDataValidationService validationService) { - if (validationService == null) - throw new IllegalArgumentException("validationService cannot be null"); - - this.validationService = validationService; - } - - @GET - @Produces(MediaType.APPLICATION_JSON) - public SampleModel getSampleModel() throws JsonProcessingException { - return new SampleModel( - UUID.randomUUID().toString(), String.valueOf(nextRangeInt(0, 42)), nextRandomColor().name(), false - ); - } - - @POST - @Produces(MediaType.APPLICATION_JSON) - @Consumes(MediaType.APPLICATION_JSON) - public Response postSampleModel(SampleModel model) throws JsonProcessingException { - // If the caller didn't pass us a payload we throw a MISSING_EXPECTED_CONTENT error. - if (model == null) { - throw ApiException.newBuilder() - .withExceptionMessage("Caller did not pass any request payload") - .withApiErrors(SampleCoreApiError.MISSING_EXPECTED_CONTENT) - .build(); - } - // Run the object through our JSR 303 validation service, which will automatically throw an appropriate error - // if the validation fails that will get translated to the desired error contract. - validationService.validateObjectsFailFast(model); - - // Manually check the throwManualError query param (normally you'd do this with JSR 303 annotations on the - // object, but this shows how you can manually throw exceptions to be picked up by the error handling system). - if (Boolean.TRUE.equals(model.throw_manual_error)) { - throw ApiException.newBuilder() - .withExceptionMessage("Manual error throw was requested") - .withApiErrors(SampleProjectApiError.MANUALLY_THROWN_ERROR) - .withExtraDetailsForLogging(Pair.of("rgb_color_value", model.rgb_color)) - .withExtraResponseHeaders( - Pair.of("rgbColorValue", singletonList(model.rgb_color)), - Pair.of("otherExtraMultivalueHeader", Arrays.asList("foo", "bar")) - ) - .build(); - } - - return Response.status(201).entity(model).build(); - } - - @GET - @Path(CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH) - public String failWithCoreErrorWrapper() { - throw ApiException.newBuilder() - .withExceptionMessage("Throwing error due to 'reasons'") - .withApiErrors(SampleProjectApiError.SOME_MEANINGFUL_ERROR_NAME) - .build(); - } - - @GET - @Path(WITH_INT_QUERY_PARAM_SUBPATH) - @Produces(MediaType.TEXT_PLAIN) - public String withIntQueryParam(@QueryParam("requiredQueryParamValue") Integer someRequiredQueryParam) { - return "You passed in " + someRequiredQueryParam + " for the required query param value"; - } - - @GET - @Path(TRIGGER_UNHANDLED_ERROR_SUBPATH) - public String triggerUnhandledError() { - throw new RuntimeException("This should be handled by Jersey2UnhandledExceptionHandler."); - } - - @GET - @Path(THROW_EXCEPTION_FROM_ASYNC_ENDPOINT_SUBPATH) - public void throwExceptionFromAsyncEndpoint(@Suspended final AsyncResponse ar) { - throw ApiException.newBuilder() - .withApiErrors(SampleProjectApiError.MANUALLY_THROWN_ERROR) - .build(); - } - - @GET - @Path(RESUME_ASYNC_RESPONSE_WITH_EXCEPTION_SUBPATH) - public void resumeAsyncResponseWithException(@Suspended final AsyncResponse ar) { - executor.execute(new Runnable() { - @Override - public void run() { - ar.resume( - ApiException.newBuilder() - .withApiErrors(SampleProjectApiError.MANUALLY_THROWN_ERROR) - .build() - ); - } - }); - } -} diff --git a/samples/sample-jersey2/src/main/resources/logback.xml b/samples/sample-jersey2/src/main/resources/logback.xml deleted file mode 100644 index 80adb28..0000000 --- a/samples/sample-jersey2/src/main/resources/logback.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - %d{yyyy-MM-dd_HH:mm:ss.SSS} [%thread] |-%-5level %logger{36} - %msg%n - - - - - - - \ No newline at end of file diff --git a/samples/sample-jersey2/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/ApplicationJsr303AnnotationTroller.java b/samples/sample-jersey2/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/ApplicationJsr303AnnotationTroller.java deleted file mode 100644 index 1445bf0..0000000 --- a/samples/sample-jersey2/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/ApplicationJsr303AnnotationTroller.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.nike.backstopper.apierror.contract.jsr303convention; - -import com.nike.internal.util.Pair; - -import java.lang.annotation.Annotation; -import java.lang.reflect.AnnotatedElement; -import java.util.List; -import java.util.function.Predicate; - -/** - * Extension of {@link ReflectionBasedJsr303AnnotationTrollerBase} for use with this sample project. This is used by JSR - * 303 annotation convention enforcement tests (e.g. {@link VerifyJsr303ContractTest} and {@link - * VerifyStringConvertsToClassTypeAnnotationsAreValidTest}). - * - *

NOTE: If you want to exclude classes or specific JSR 303 annotations from triggering convention violation errors - * in those tests you can do so by populating the return values of the {@link #ignoreAllAnnotationsAssociatedWithTheseProjectClasses()} - * and {@link #specificAnnotationDeclarationExclusionsForProject()} methods. IMPORTANT - this should only be done - * if you *really* know what you're doing. Usually it's only done for unit test classes that are intended to violate the - * convention. It should not be done for production code under normal circumstances. See the javadocs for the super - * class for those methods if you need to use them. - * - * @author Nic Munroe - */ -public final class ApplicationJsr303AnnotationTroller extends ReflectionBasedJsr303AnnotationTrollerBase { - - public static final ApplicationJsr303AnnotationTroller INSTANCE = new ApplicationJsr303AnnotationTroller(); - - public static ApplicationJsr303AnnotationTroller getInstance() { - return INSTANCE; - } - - // Intentionally private - use {@code getInstance()} to retrieve the singleton instance of this class. - private ApplicationJsr303AnnotationTroller() { - super(); - } - - @Override - protected List> ignoreAllAnnotationsAssociatedWithTheseProjectClasses() { - return null; - } - - @Override - protected List>> specificAnnotationDeclarationExclusionsForProject() { - return null; - } -} diff --git a/samples/sample-jersey2/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyJsr303ContractTest.java b/samples/sample-jersey2/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyJsr303ContractTest.java deleted file mode 100644 index 3b45ad9..0000000 --- a/samples/sample-jersey2/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyJsr303ContractTest.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.nike.backstopper.apierror.contract.jsr303convention; - -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.jersey2sample.error.SampleProjectApiErrorsImpl; - -/** - * Verifies that *ALL* non-excluded JSR 303 validation annotations in this project have a message defined that maps to a - * {@link com.nike.backstopper.apierror.ApiError} enum name from this project's {@link ProjectApiErrors}. This is how - * the JSR 303 Bean Validation system is connected to the Backstopper error handling system and you should NOT disable - * these tests. - * - *

You can exclude annotation declarations (e.g. for unit test classes that are intended to violate the naming - * convention) by making sure that the {@link ApplicationJsr303AnnotationTroller#ignoreAllAnnotationsAssociatedWithTheseProjectClasses()} - * and {@link ApplicationJsr303AnnotationTroller#specificAnnotationDeclarationExclusionsForProject()} methods return - * what you need, but you should not exclude any annotations in production code under normal circumstances. - * - * @author Nic Munroe - */ -public class VerifyJsr303ContractTest extends VerifyJsr303ValidationMessagesPointToApiErrorsTest { - - private static final ProjectApiErrors PROJECT_API_ERRORS = new SampleProjectApiErrorsImpl(); - - @Override - protected ReflectionBasedJsr303AnnotationTrollerBase getAnnotationTroller() { - return ApplicationJsr303AnnotationTroller.getInstance(); - } - - @Override - protected ProjectApiErrors getProjectApiErrors() { - return PROJECT_API_ERRORS; - } -} diff --git a/samples/sample-jersey2/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyStringConvertsToClassTypeAnnotationsAreValidTest.java b/samples/sample-jersey2/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyStringConvertsToClassTypeAnnotationsAreValidTest.java deleted file mode 100644 index 3babd32..0000000 --- a/samples/sample-jersey2/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyStringConvertsToClassTypeAnnotationsAreValidTest.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.nike.backstopper.apierror.contract.jsr303convention; - -import com.nike.backstopper.validation.constraints.StringConvertsToClassType; - -/** - * Makes sure that any Enums referenced by {@link StringConvertsToClassType} JSR 303 annotations are case insensitive if - * they are marked with {@link StringConvertsToClassType#allowCaseInsensitiveEnumMatch()} set to true. - * - *

You can exclude annotation declarations (e.g. for unit test classes that are intended to violate the naming - * convention) by making sure that the {@link ApplicationJsr303AnnotationTroller#ignoreAllAnnotationsAssociatedWithTheseProjectClasses()} - * and {@link ApplicationJsr303AnnotationTroller#specificAnnotationDeclarationExclusionsForProject()} methods return - * what you need, but you should not exclude any annotations in production code under normal circumstances. - * - * @author Nic Munroe - */ -public class VerifyStringConvertsToClassTypeAnnotationsAreValidTest - extends VerifyEnumsReferencedByStringConvertsToClassTypeJsr303AnnotationsAreJacksonCaseInsensitiveTest { - - @Override - protected ReflectionBasedJsr303AnnotationTrollerBase getAnnotationTroller() { - return ApplicationJsr303AnnotationTroller.getInstance(); - } -} diff --git a/samples/sample-jersey2/src/test/java/com/nike/backstopper/jersey2sample/componenttest/VerifyExpectedErrorsAreReturnedComponentTest.java b/samples/sample-jersey2/src/test/java/com/nike/backstopper/jersey2sample/componenttest/VerifyExpectedErrorsAreReturnedComponentTest.java deleted file mode 100644 index 37c0164..0000000 --- a/samples/sample-jersey2/src/test/java/com/nike/backstopper/jersey2sample/componenttest/VerifyExpectedErrorsAreReturnedComponentTest.java +++ /dev/null @@ -1,460 +0,0 @@ -package com.nike.backstopper.jersey2sample.componenttest; - -import com.nike.backstopper.apierror.ApiError; -import com.nike.backstopper.apierror.ApiErrorWithMetadata; -import com.nike.backstopper.apierror.sample.SampleCoreApiError; -import com.nike.backstopper.jersey2sample.Main; -import com.nike.backstopper.jersey2sample.error.SampleProjectApiError; -import com.nike.backstopper.jersey2sample.model.RgbColor; -import com.nike.backstopper.jersey2sample.model.SampleModel; -import com.nike.backstopper.model.DefaultErrorContractDTO; -import com.nike.backstopper.model.DefaultErrorDTO; -import com.nike.internal.util.MapBuilder; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; - -import org.eclipse.jetty.server.Server; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -import io.restassured.http.ContentType; -import io.restassured.response.ExtractableResponse; - -import static com.nike.backstopper.jersey2sample.error.SampleProjectApiError.INVALID_RANGE_VALUE; -import static com.nike.backstopper.jersey2sample.error.SampleProjectApiError.NOT_RGB_COLOR_ENUM; -import static com.nike.backstopper.jersey2sample.error.SampleProjectApiError.RGB_COLOR_CANNOT_BE_NULL; -import static com.nike.backstopper.jersey2sample.resource.SampleResource.CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH; -import static com.nike.backstopper.jersey2sample.resource.SampleResource.RESUME_ASYNC_RESPONSE_WITH_EXCEPTION_SUBPATH; -import static com.nike.backstopper.jersey2sample.resource.SampleResource.SAMPLE_PATH; -import static com.nike.backstopper.jersey2sample.resource.SampleResource.THROW_EXCEPTION_FROM_ASYNC_ENDPOINT_SUBPATH; -import static com.nike.backstopper.jersey2sample.resource.SampleResource.TRIGGER_UNHANDLED_ERROR_SUBPATH; -import static com.nike.backstopper.jersey2sample.resource.SampleResource.WITH_INT_QUERY_PARAM_SUBPATH; -import static com.nike.backstopper.jersey2sample.resource.SampleResource.nextRandomColor; -import static com.nike.backstopper.jersey2sample.resource.SampleResource.nextRangeInt; -import static com.nike.internal.util.testing.TestUtils.findFreePort; -import static io.restassured.RestAssured.given; -import static java.util.Collections.singleton; -import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Component test that starts up the sample server and hits it with requests that should generate specific errors. - * - * @author Nic Munroe - */ -public class VerifyExpectedErrorsAreReturnedComponentTest { - - private static final int SERVER_PORT = findFreePort(); - private static Server server; - - private static final ObjectMapper objectMapper = new ObjectMapper(); - - @BeforeAll - public static void beforeClass() throws Exception { - server = Main.createServer(SERVER_PORT); - server.start(); - for (int i = 0; i < 100; i++) { - if (server.isStarted()) - return; - Thread.sleep(100); - } - throw new IllegalStateException("Server is not up after waiting 10 seconds. Aborting tests."); - } - - @AfterAll - public static void afterClass() throws Exception { - if (server != null) { - server.stop(); - server.destroy(); - } - } - - private void verifyErrorReceived(ExtractableResponse response, ApiError expectedError) { - verifyErrorReceived(response, singleton(expectedError), expectedError.getHttpStatusCode()); - } - - private DefaultErrorDTO findErrorMatching(DefaultErrorContractDTO errorContract, ApiError desiredError) { - for (DefaultErrorDTO error : errorContract.errors) { - if (error.code.equals(desiredError.getErrorCode()) && error.message.equals(desiredError.getMessage())) - return error; - } - - return null; - } - - private void verifyErrorReceived(ExtractableResponse response, Collection expectedErrors, int expectedHttpStatusCode) { - assertThat(response.statusCode()).isEqualTo(expectedHttpStatusCode); - try { - DefaultErrorContractDTO errorContract = objectMapper.readValue(response.asString(), DefaultErrorContractDTO.class); - assertThat(errorContract.error_id).isNotEmpty(); - assertThat(UUID.fromString(errorContract.error_id)).isNotNull(); - assertThat(errorContract.errors).hasSameSizeAs(expectedErrors); - for (ApiError apiError : expectedErrors) { - DefaultErrorDTO matchingError = findErrorMatching(errorContract, apiError); - assertThat(matchingError).isNotNull(); - assertThat(matchingError.code).isEqualTo(apiError.getErrorCode()); - assertThat(matchingError.message).isEqualTo(apiError.getMessage()); - assertThat(matchingError.metadata).isEqualTo(apiError.getMetadata()); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private SampleModel randomizedSampleModel() { - return new SampleModel(UUID.randomUUID().toString(), String.valueOf(nextRangeInt(0, 42)), nextRandomColor().name(), false); - } - - // *************** SUCCESSFUL (NON ERROR) CALLS ****************** - @Test - public void verify_basic_sample_get() throws IOException { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .basePath(SAMPLE_PATH) - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - assertThat(response.statusCode()).isEqualTo(200); - SampleModel responseBody = objectMapper.readValue(response.asString(), SampleModel.class); - assertThat(responseBody).isNotNull(); - assertThat(responseBody.foo).isNotEmpty(); - assertThat(responseBody.range_0_to_42).isNotEmpty(); - assertThat(Integer.parseInt(responseBody.range_0_to_42)).isBetween(0, 42); - assertThat(responseBody.rgb_color).isNotEmpty(); - assertThat(RgbColor.toRgbColor(responseBody.rgb_color)).isNotNull(); - assertThat(responseBody.throw_manual_error).isFalse(); - } - - @Test - public void verify_basic_sample_post() throws IOException { - SampleModel requestPayload = randomizedSampleModel(); - String requestPayloadAsString = objectMapper.writeValueAsString(requestPayload); - - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .contentType(ContentType.JSON) - .basePath(SAMPLE_PATH) - .body(requestPayloadAsString) - .log().all() - .when() - .post() - .then() - .log().all() - .extract(); - - assertThat(response.statusCode()).isEqualTo(201); - SampleModel responseBody = objectMapper.readValue(response.asString(), SampleModel.class); - assertThat(responseBody).isNotNull(); - assertThat(responseBody.foo).isEqualTo(requestPayload.foo); - assertThat(responseBody.range_0_to_42).isEqualTo(requestPayload.range_0_to_42); - assertThat(responseBody.rgb_color).isEqualTo(requestPayload.rgb_color); - assertThat(responseBody.throw_manual_error).isEqualTo(requestPayload.throw_manual_error); - } - - // *************** JSR 303 AND ENDPOINT ERRORS ****************** - - @CsvSource( - value = { - "null | 42 | GREEN | FOO_STRING_CANNOT_BE_BLANK | 400", - "bar | -1 | GREEN | INVALID_RANGE_VALUE | 400", - "bar | 42 | null | RGB_COLOR_CANNOT_BE_NULL | 400", - "bar | 42 | car | NOT_RGB_COLOR_ENUM | 400", - " | 99 | tree | FOO_STRING_CANNOT_BE_BLANK,INVALID_RANGE_VALUE,NOT_RGB_COLOR_ENUM | 400", - }, - delimiter = '|', - nullValues = { "null" } - ) - @ParameterizedTest - public void verify_jsr303_validation_errors( - String fooString, String rangeString, String rgbColorString, - String expectedErrorsComboString, int expectedResponseHttpStatusCode) throws JsonProcessingException - { - SampleModel requestPayload = new SampleModel(fooString, rangeString, rgbColorString, false); - String requestPayloadAsString = objectMapper.writeValueAsString(requestPayload); - - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .contentType(ContentType.JSON) - .basePath(SAMPLE_PATH) - .body(requestPayloadAsString) - .log().all() - .when() - .post() - .then() - .log().all() - .extract(); - - String[] expectedErrorsArray = expectedErrorsComboString.split(","); - List expectedErrors = new ArrayList<>(); - for (String errorStr : expectedErrorsArray) { - ApiError apiError = SampleProjectApiError.valueOf(errorStr); - String extraMetadataFieldValue = null; - - if (INVALID_RANGE_VALUE.equals(apiError)) - extraMetadataFieldValue = "range_0_to_42"; - else if (RGB_COLOR_CANNOT_BE_NULL.equals(apiError) || NOT_RGB_COLOR_ENUM.equals(apiError)) - extraMetadataFieldValue = "rgb_color"; - - if (extraMetadataFieldValue != null) - apiError = new ApiErrorWithMetadata(apiError, MapBuilder.builder("field", (Object)extraMetadataFieldValue).build()); - - expectedErrors.add(apiError); - } - verifyErrorReceived(response, expectedErrors, expectedResponseHttpStatusCode); - } - - @Test - public void verify_sample_post_fails_with_MISSING_EXPECTED_CONTENT_if_passed_empty_body() { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .basePath(SAMPLE_PATH) - .contentType(ContentType.JSON) - .body("") - .log().all() - .when() - .post() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleCoreApiError.MISSING_EXPECTED_CONTENT); - } - - @Test - public void verify_MANUALLY_THROWN_ERROR_is_thrown_when_requested() throws IOException { - SampleModel requestPayload = new SampleModel("bar", "42", "RED", true); - String requestPayloadAsString = objectMapper.writeValueAsString(requestPayload); - - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .contentType(ContentType.JSON) - .basePath(SAMPLE_PATH) - .body(requestPayloadAsString) - .log().all() - .when() - .post() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleProjectApiError.MANUALLY_THROWN_ERROR); - // This code path also should add some custom headers to the response - assertThat(response.headers().getValues("rgbColorValue")).isEqualTo(singletonList(requestPayload.rgb_color)); - assertThat(response.headers().getValues("otherExtraMultivalueHeader")).isEqualTo(Arrays.asList("foo", "bar")); - } - - @Test - public void verify_SOME_MEANINGFUL_ERROR_NAME_is_thrown_when_correct_endpoint_is_hit() { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .basePath(SAMPLE_PATH + CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH) - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleProjectApiError.SOME_MEANINGFUL_ERROR_NAME); - } - - @Test - public void verify_GENERIC_SERVICE_ERROR_is_thrown_when_correct_endpoint_is_hit() { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .basePath(SAMPLE_PATH + TRIGGER_UNHANDLED_ERROR_SUBPATH) - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleCoreApiError.GENERIC_SERVICE_ERROR); - } - - @Test - public void verify_exception_thrown_from_async_endpoint_is_handled_by_backstopper() { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .basePath(SAMPLE_PATH + THROW_EXCEPTION_FROM_ASYNC_ENDPOINT_SUBPATH) - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleProjectApiError.MANUALLY_THROWN_ERROR); - } - - @Test - public void verify_async_response_resumed_with_exception_is_handled_by_backstopper() { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .basePath(SAMPLE_PATH + RESUME_ASYNC_RESPONSE_WITH_EXCEPTION_SUBPATH) - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleProjectApiError.MANUALLY_THROWN_ERROR); - } - - // *************** FRAMEWORK ERRORS ****************** - - @Test - public void verify_NOT_FOUND_returned_if_unknown_path_is_requested() { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .basePath(UUID.randomUUID().toString()) - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleCoreApiError.NOT_FOUND); - } - - @Test - public void verify_METHOD_NOT_ALLOWED_returned_if_known_path_is_requested_with_invalid_http_method() { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .basePath(SAMPLE_PATH) - .log().all() - .when() - .delete() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleCoreApiError.METHOD_NOT_ALLOWED); - } - - @Test - public void verify_sample_get_fails_with_NO_ACCEPTABLE_REPRESENTATION_if_passed_invalid_accept_header() { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .basePath(SAMPLE_PATH) - .accept(ContentType.BINARY) - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleCoreApiError.NO_ACCEPTABLE_REPRESENTATION); - } - - @Test - public void verify_sample_post_fails_with_UNSUPPORTED_MEDIA_TYPE_if_passed_invalid_content_type() throws IOException { - SampleModel requestPayload = randomizedSampleModel(); - String requestPayloadAsString = objectMapper.writeValueAsString(requestPayload); - - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .basePath(SAMPLE_PATH) - .body(requestPayloadAsString) - .contentType(ContentType.TEXT) - .log().all() - .when() - .post() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleCoreApiError.UNSUPPORTED_MEDIA_TYPE); - } - - @Test - public void verify_NOT_FOUND_is_thrown_when_framework_cannot_convert_query_param_type() { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .basePath(SAMPLE_PATH + WITH_INT_QUERY_PARAM_SUBPATH) - .queryParam("requiredQueryParamValue", "not-an-integer") - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleCoreApiError.NOT_FOUND); - } - - @Test - public void verify_sample_post_fails_with_MALFORMED_REQUEST_if_passed_bad_json_body() throws IOException { - SampleModel originalValidPayloadObj = randomizedSampleModel(); - String originalValidPayloadAsString = objectMapper.writeValueAsString(originalValidPayloadObj); - @SuppressWarnings("unchecked") - Map badRequestPayloadAsMap = objectMapper.readValue(originalValidPayloadAsString, Map.class); - badRequestPayloadAsMap.put("throw_manual_error", "not-a-boolean"); - String badJsonPayloadAsString = objectMapper.writeValueAsString(badRequestPayloadAsMap); - - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .basePath(SAMPLE_PATH) - .contentType(ContentType.JSON) - .body(badJsonPayloadAsString) - .log().all() - .when() - .post() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleCoreApiError.MALFORMED_REQUEST); - } - -} diff --git a/samples/sample-jersey2/src/test/java/com/nike/backstopper/jersey2sample/error/SampleProjectApiErrorsImplTest.java b/samples/sample-jersey2/src/test/java/com/nike/backstopper/jersey2sample/error/SampleProjectApiErrorsImplTest.java deleted file mode 100644 index 2287b82..0000000 --- a/samples/sample-jersey2/src/test/java/com/nike/backstopper/jersey2sample/error/SampleProjectApiErrorsImplTest.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.nike.backstopper.jersey2sample.error; - -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrorsTestBase; - -/** - * Extends {@link ProjectApiErrorsTestBase} in order to inherit tests that will verify the correctness of this - * project's {@link SampleProjectApiErrorsImpl}. - * - * @author Nic Munroe - */ -public class SampleProjectApiErrorsImplTest extends ProjectApiErrorsTestBase { - - ProjectApiErrors projectApiErrors = new SampleProjectApiErrorsImpl(); - - @Override - protected ProjectApiErrors getProjectApiErrors() { - return projectApiErrors; - } -} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 16166a7..b2e65a2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -12,9 +12,6 @@ include "nike-internal-util", // "backstopper-spring-web-flux", // "backstopper-spring-boot1", // "backstopper-spring-boot2-webmvc", -// "backstopper-jaxrs", -// "backstopper-jersey1", -// "backstopper-jersey2", // // Test-only modules (not published) // "testonly:testonly-spring-reusable-test-support", // "testonly:testonly-spring4-webmvc", @@ -26,6 +23,4 @@ include "nike-internal-util", // "samples:sample-spring-web-mvc", // "samples:sample-spring-boot1", // "samples:sample-spring-boot2-webmvc", -// "samples:sample-spring-boot2-webflux", -// "samples:sample-jersey1", -// "samples:sample-jersey2" \ No newline at end of file +// "samples:sample-spring-boot2-webflux", \ No newline at end of file From 3a2bd732b1afc09a0ce829da8d58fbd1508ef4ad Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Mon, 9 Sep 2024 19:27:50 -0700 Subject: [PATCH 12/42] Fix for spring-web --- ...mmonFrameworkExceptionHandlerListener.java | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListener.java b/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListener.java index 153d25f..3650ea5 100644 --- a/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListener.java +++ b/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListener.java @@ -198,22 +198,30 @@ protected ApiExceptionHandlerListenerResult handleHttpMessageConversionException protected boolean isMissingExpectedContentCase(HttpMessageConversionException ex) { if (ex instanceof HttpMessageNotReadableException) { - // Different versions of Spring Web MVC can have different ways of expressing missing content. + // Different versions of Spring Web MVC and underlying deserializers (e.g. Jackson) can have different ways of expressing missing content. - // More common case + // A common case. if (ex.getMessage().startsWith("Required request body is missing")) { return true; } - // An older/more unusual case. Unfortunately there's a lot of manual digging that we have to do to determine - // that we've reached this case. + // Underlying Jackson cases. Unfortunately there's a lot of manual digging that we have to do to determine + // that we've reached these cases. Throwable cause = ex.getCause(); //noinspection RedundantIfStatement - if (cause != null - && "com.fasterxml.jackson.databind.JsonMappingException".equals(cause.getClass().getName()) - && nullSafeStringContains(cause.getMessage(), "No content to map due to end-of-input") - ) { - return true; + if (cause != null) { + String causeClassName = cause.getClass().getName(); + if ("com.fasterxml.jackson.databind.exc.InvalidFormatException".equals(causeClassName) + && nullSafeStringContains(cause.getMessage(), "Cannot coerce empty String") + ) { + return true; + } + + if ("com.fasterxml.jackson.databind.JsonMappingException".equals(causeClassName) + && nullSafeStringContains(cause.getMessage(), "No content to map due to end-of-input") + ) { + return true; + } } } From f0390dbddb398056f165ca90183776c490d7b46b Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Mon, 9 Sep 2024 19:29:38 -0700 Subject: [PATCH 13/42] Add back spring webmvc sample app --- build.gradle | 4 +- samples/sample-spring-web-mvc/README.md | 69 ++++++++++++------- samples/sample-spring-web-mvc/build.gradle | 10 +-- .../nike/backstopper/springsample/Main.java | 17 ++--- .../config/SampleWebMvcConfig.java | 10 +-- .../controller/SampleController.java | 2 +- .../error/SampleProjectApiError.java | 6 +- .../error/SampleProjectApiErrorsImpl.java | 2 +- .../springsample/model/SampleModel.java | 4 +- settings.gradle | 4 +- 10 files changed, 75 insertions(+), 53 deletions(-) diff --git a/build.gradle b/build.gradle index 3086ed4..ffc0945 100644 --- a/build.gradle +++ b/build.gradle @@ -111,8 +111,8 @@ ext { orgReflectionsVersion = '0.9.11' javassistVersion = '3.23.2-GA' - jettyVersion = '9.2.19.v20160908' - restAssuredVersion = '3.0.1' + jettyVersion = '11.0.24' + restAssuredVersion = '5.5.0' // JACOCO PROPERTIES jacocoToolVersion = '0.8.12' diff --git a/samples/sample-spring-web-mvc/README.md b/samples/sample-spring-web-mvc/README.md index 271ea7f..91e5785 100644 --- a/samples/sample-spring-web-mvc/README.md +++ b/samples/sample-spring-web-mvc/README.md @@ -3,43 +3,66 @@ Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 7 and greater. This submodule contains a sample application based on Spring Web MVC that fully integrates Backstopper. - + * Build the sample by running the `./buildSample.sh` script. -* Launch the sample by running the `./runSample.sh` script. It will bind to port 8080 by default. - * You can override the default port by passing in a system property to the run script, e.g. to bind to port 8181: `./runSample.sh -DspringSample.server.port=8181` - +* Launch the sample by running the `./runSample.sh` script. It will bind to port 8080 by default. + * You can override the default port by passing in a system property to the run script, e.g. to bind to port 8181: + `./runSample.sh -DspringSample.server.port=8181` + ## Things to try - -All examples here assume the sample app is running on port 8080, so you would hit each path by going to `http://localhost:8080/[endpoint-path]`. It's recommended that you use a REST client like [Postman](https://www.getpostman.com/) for making the requests so you can easily specify HTTP method, payloads, headers, etc, and fully inspect the response. -Also note that all the following things to try are verified in a component test: `VerifyExpectedErrorsAreReturnedComponentTest`. If you prefer to experiment via code you can run, debug, and otherwise explore that test. +All examples here assume the sample app is running on port 8080, so you would hit each path by going to +`http://localhost:8080/[endpoint-path]`. It's recommended that you use a REST client +like [Postman](https://www.getpostman.com/) for making the requests so you can easily specify HTTP method, payloads, +headers, etc, and fully inspect the response. + +Also note that all the following things to try are verified in a component test: +`VerifyExpectedErrorsAreReturnedComponentTest`. If you prefer to experiment via code you can run, debug, and otherwise +explore that test. + +As you are doing the following you should check the logs that are output by the sample application and notice what is +included in the log messages. In particular notice how you can search for an `error_id` that came from an error response +and go directly to the relevant log message in the logs. Also notice how the `ApiError.getName()` value shows up in the +logs for each error represented in a returned error contract (there can be more than one per request). -As you are doing the following you should check the logs that are output by the sample application and notice what is included in the log messages. In particular notice how you can search for an `error_id` that came from an error response and go directly to the relevant log message in the logs. Also notice how the `ApiError.getName()` value shows up in the logs for each error represented in a returned error contract (there can be more than one per request). - -* `GET /sample` - Returns the JSON serialization for the `SampleModel` model object. You can copy this into a `POST` call to experiment with triggering errors. -* `POST /sample` with `ContentType: application/json` header - Using the JSON model retrieved by the `GET` call, you can trigger numerous different types of errors, all of which get caught by the Backstopper system and converted into the appropriate error contract. +* `GET /sample` - Returns the JSON serialization for the `SampleModel` model object. You can copy this into a `POST` + call to experiment with triggering errors. +* `POST /sample` with `Content-Type: application/json` header - Using the JSON model retrieved by the `GET` call, you can + trigger numerous different types of errors, all of which get caught by the Backstopper system and converted into the + appropriate error contract. * Omit the `foo` field. * Set the value of the `range_0_to_42` field to something outside of the allowed 0-42 range. - * Set the value of the `rgb_color` field to something besides `RED`, `GREEN`, or `BLUE`, or omit it entirely. Note that the validation and deserialization of this enum field is done in a case insensitive manner - i.e. you can pass `red`, `Green`, or `bLuE` if you want and it will not throw an error. - * Set two or more invalid values for `foo`, `range_0_to_42`, and `rgb_color` to invalid values all at once - notice you get back all relevant errors at once in the same error contract. - * Set `throw_manual_error` to true to trigger a manual exception to be thrown inside the normal `POST /sample` endpoint. - * Note the extra response headers that are included when you do this, and how they relate to the `.withExtraResponseHeaders(...)` method call on the builder of the exception that is thrown. + * Set the value of the `rgb_color` field to something besides `RED`, `GREEN`, or `BLUE`, or omit it entirely. Note + that the validation and deserialization of this enum field is done in a case insensitive manner - i.e. you can + pass `red`, `Green`, or `bLuE` if you want and it will not throw an error. + * Set two or more invalid values for `foo`, `range_0_to_42`, and `rgb_color` to invalid values all at once - notice + you get back all relevant errors at once in the same error contract. + * Set `throw_manual_error` to true to trigger a manual exception to be thrown inside the normal `POST /sample` + endpoint. + * Note the extra response headers that are included when you do this, and how they relate to the + `.withExtraResponseHeaders(...)` method call on the builder of the exception that is thrown. * Pass in an empty JSON payload - you should receive a `"Missing expected content"` error back. * Pass in a junk payload that is not valid JSON - you should receive a `"Malformed request"` error back. -* `GET /sample/coreErrorWrapper` - Triggers an error to be thrown that appears to the caller like a normal generic service exception, but the `SOME_MEANINGFUL_ERROR_NAME` name from the `ApiError` it represents shows up in the logs to help you disambiguate what the true cause was. -* `GET /sample/triggerUnhandledError` - Triggers an error that is caught by the unhandled exception handler portion of Backstopper and converted to a generic service exception. -* `GET /sample/withRequiredQueryParam?requiredQueryParamValue=not-an-int` - Triggers an error in the Spring Web MVC framework when it cannot coerce the query param value to the required type (an integer), which results in a Backstopper `"Type conversion error"`. +* `GET /sample/coreErrorWrapper` - Triggers an error to be thrown that appears to the caller like a normal generic + service exception, but the `SOME_MEANINGFUL_ERROR_NAME` name from the `ApiError` it represents shows up in the logs to + help you disambiguate what the true cause was. +* `GET /sample/triggerUnhandledError` - Triggers an error that is caught by the unhandled exception handler portion of + Backstopper and converted to a generic service exception. +* `GET /sample/withRequiredQueryParam?requiredQueryParamValue=not-an-int` - Triggers an error in the Spring Web MVC + framework when it cannot coerce the query param value to the required type (an integer), which results in a + Backstopper `"Type conversion error"`. * `GET /does-not-exist` - Triggers a framework 404 which Backstopper handles. -* `DELETE /sample` - Triggers a framework 405 which Backstopper handles. +* `DELETE /sample` - Triggers a framework 405 which Backstopper handles. * `GET /sample` with `Accept: application/octet-stream` header - Triggers a framework 406 which Backstopper handles. -* `POST /sample` with `ContentType: text/plain` - Triggers a framework 415 which Backstopper handles. +* `POST /sample` with `Content-Type: text/plain` - Triggers a framework 415 which Backstopper handles. * Any request with a `throw-servlet-filter-exception` header set to `true`. This will trigger an exception in a -Servlet filter before the request ever hits Spring. The Jetty container for this sample app is configured (in -`Main.java`) to forward these kinds of errors back into Spring, where it is handled by Backstopper. + Servlet filter before the request ever hits Spring. The Jetty container for this sample app is configured (in + `Main.java`) to forward these kinds of errors back into Spring, where it is handled by Backstopper. ## More Info -See the [base project README.md](../../README.md), [User Guide](../../USER_GUIDE.md), and Backstopper repository source code and javadocs for all further information. +See the [base project README.md](../../README.md), [User Guide](../../USER_GUIDE.md), and Backstopper repository source +code and javadocs for all further information. ## License diff --git a/samples/sample-spring-web-mvc/build.gradle b/samples/sample-spring-web-mvc/build.gradle index 6b3cb67..87173cb 100644 --- a/samples/sample-spring-web-mvc/build.gradle +++ b/samples/sample-spring-web-mvc/build.gradle @@ -1,8 +1,5 @@ evaluationDependsOn(':') -sourceCompatibility = JavaVersion.VERSION_1_8 -targetCompatibility = JavaVersion.VERSION_1_8 - test { useJUnitPlatform() } @@ -11,9 +8,10 @@ dependencies { implementation( project(":backstopper-spring-web-mvc"), project(":backstopper-custom-validators"), - "org.springframework:spring-webmvc:$spring4Version", + "org.springframework:spring-webmvc:$spring6Version", "ch.qos.logback:logback-classic:$logbackVersion", - "org.hibernate:hibernate-validator:$hibernateValidatorVersion", + "org.hibernate.validator:hibernate-validator:$hibernateValidatorVersion", + "org.glassfish.expressly:expressly:$glassfishExpresslyVersion", "org.eclipse.jetty:jetty-webapp:$jettyVersion" ) compileOnly( @@ -30,8 +28,6 @@ dependencies { "ch.qos.logback:logback-classic:$logbackVersion", "org.assertj:assertj-core:$assertJVersion", "io.rest-assured:rest-assured:$restAssuredVersion", - // The jaxb-api is needed for building on the java 11 JDK as these classes were moved out of the Java SE libs. - "javax.xml.bind:jaxb-api:$jaxbApiVersion", ) } diff --git a/samples/sample-spring-web-mvc/src/main/java/com/nike/backstopper/springsample/Main.java b/samples/sample-spring-web-mvc/src/main/java/com/nike/backstopper/springsample/Main.java index bcdad24..0cd3c61 100644 --- a/samples/sample-spring-web-mvc/src/main/java/com/nike/backstopper/springsample/Main.java +++ b/samples/sample-spring-web-mvc/src/main/java/com/nike/backstopper/springsample/Main.java @@ -8,6 +8,7 @@ import org.eclipse.jetty.servlet.ErrorPageErrorHandler; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHolder; +import org.jetbrains.annotations.NotNull; import org.springframework.web.context.ContextLoaderListener; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; @@ -17,11 +18,11 @@ import java.io.IOException; import java.util.EnumSet; -import javax.servlet.DispatcherType; -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.DispatcherType; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; /** * Starts up the Backstopper Spring Web MVC Sample server (on port 8080 by default). @@ -44,14 +45,14 @@ public static void main(String[] args) throws Exception { } } - public static Server createServer(int port) throws Exception { + public static Server createServer(int port) { Server server = new Server(port); server.setHandler(generateServletContextHandler(generateWebAppContext())); return server; } - private static ServletContextHandler generateServletContextHandler(WebApplicationContext context) throws IOException { + private static ServletContextHandler generateServletContextHandler(WebApplicationContext context) { ServletContextHandler contextHandler = new ServletContextHandler(); contextHandler.setErrorHandler(generateErrorHandler()); contextHandler.setContextPath("/"); @@ -88,7 +89,7 @@ public static class ExplodingFilter extends OncePerRequestFilter { @Override protected void doFilterInternal( - HttpServletRequest request, HttpServletResponse response, FilterChain filterChain + HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain ) throws ServletException, IOException { if ("true".equals(request.getHeader("throw-servlet-filter-exception"))) { throw ApiException diff --git a/samples/sample-spring-web-mvc/src/main/java/com/nike/backstopper/springsample/config/SampleWebMvcConfig.java b/samples/sample-spring-web-mvc/src/main/java/com/nike/backstopper/springsample/config/SampleWebMvcConfig.java index 8f59932..3e0f51a 100644 --- a/samples/sample-spring-web-mvc/src/main/java/com/nike/backstopper/springsample/config/SampleWebMvcConfig.java +++ b/samples/sample-spring-web-mvc/src/main/java/com/nike/backstopper/springsample/config/SampleWebMvcConfig.java @@ -9,10 +9,10 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.web.servlet.config.annotation.EnableWebMvc; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import javax.validation.Validation; -import javax.validation.Validator; +import jakarta.validation.Validation; +import jakarta.validation.Validator; /** * Simple Spring Web MVC config for the sample app. The {@link ProjectApiErrors} and {@link Validator} beans defined @@ -34,7 +34,8 @@ SampleController.class }) @EnableWebMvc -public class SampleWebMvcConfig extends WebMvcConfigurerAdapter { +@SuppressWarnings("unused") +public class SampleWebMvcConfig implements WebMvcConfigurer { /** * @return The {@link ProjectApiErrors} to use for this sample app. @@ -54,6 +55,7 @@ public ProjectApiErrors getProjectApiErrors() { * implementation dependency into your project. */ @Bean + @SuppressWarnings("resource") public Validator getJsr303Validator() { return Validation.buildDefaultValidatorFactory().getValidator(); } diff --git a/samples/sample-spring-web-mvc/src/main/java/com/nike/backstopper/springsample/controller/SampleController.java b/samples/sample-spring-web-mvc/src/main/java/com/nike/backstopper/springsample/controller/SampleController.java index 9328bc7..e5907f7 100644 --- a/samples/sample-spring-web-mvc/src/main/java/com/nike/backstopper/springsample/controller/SampleController.java +++ b/samples/sample-spring-web-mvc/src/main/java/com/nike/backstopper/springsample/controller/SampleController.java @@ -20,7 +20,7 @@ import java.util.Arrays; import java.util.UUID; -import javax.validation.Valid; +import jakarta.validation.Valid; import static com.nike.backstopper.springsample.controller.SampleController.SAMPLE_PATH; import static java.util.Collections.singletonList; diff --git a/samples/sample-spring-web-mvc/src/main/java/com/nike/backstopper/springsample/error/SampleProjectApiError.java b/samples/sample-spring-web-mvc/src/main/java/com/nike/backstopper/springsample/error/SampleProjectApiError.java index 8090624..229279b 100644 --- a/samples/sample-spring-web-mvc/src/main/java/com/nike/backstopper/springsample/error/SampleProjectApiError.java +++ b/samples/sample-spring-web-mvc/src/main/java/com/nike/backstopper/springsample/error/SampleProjectApiError.java @@ -18,7 +18,7 @@ * conform to the range specified in {@link SampleProjectApiErrorsImpl#getProjectSpecificErrorCodeRange()} or an * exception will be thrown on app startup, and unit tests should fail. The one exception to this rule is a "core * error wrapper" - an instance that shares the same error code, message, and HTTP status code as a - * {@link SampleProjectApiErrorsImpl#getCoreApiErrors()} instance (in this case that means a wrapper around + * {@code SampleProjectApiErrorsImpl.getCoreApiErrors()} instance (in this case that means a wrapper around * {@link com.nike.backstopper.apierror.sample.SampleCoreApiError}). * * @author Nic Munroe @@ -55,14 +55,14 @@ public enum SampleProjectApiError implements ApiError { SampleProjectApiError(int errorCode, String message, int httpStatusCode) { this(new ApiErrorBase( - "delegated-to-enum-wrapper-" + UUID.randomUUID().toString(), errorCode, message, httpStatusCode + "delegated-to-enum-wrapper-" + UUID.randomUUID(), errorCode, message, httpStatusCode )); } @SuppressWarnings("unused") SampleProjectApiError(int errorCode, String message, int httpStatusCode, Map metadata) { this(new ApiErrorBase( - "delegated-to-enum-wrapper-" + UUID.randomUUID().toString(), errorCode, message, httpStatusCode, metadata + "delegated-to-enum-wrapper-" + UUID.randomUUID(), errorCode, message, httpStatusCode, metadata )); } diff --git a/samples/sample-spring-web-mvc/src/main/java/com/nike/backstopper/springsample/error/SampleProjectApiErrorsImpl.java b/samples/sample-spring-web-mvc/src/main/java/com/nike/backstopper/springsample/error/SampleProjectApiErrorsImpl.java index 6495fde..a4c5045 100644 --- a/samples/sample-spring-web-mvc/src/main/java/com/nike/backstopper/springsample/error/SampleProjectApiErrorsImpl.java +++ b/samples/sample-spring-web-mvc/src/main/java/com/nike/backstopper/springsample/error/SampleProjectApiErrorsImpl.java @@ -9,7 +9,7 @@ import java.util.Arrays; import java.util.List; -import javax.inject.Singleton; +import jakarta.inject.Singleton; /** * Returns the project specific errors for this sample application. {@link #getProjectApiErrors()} will return a diff --git a/samples/sample-spring-web-mvc/src/main/java/com/nike/backstopper/springsample/model/SampleModel.java b/samples/sample-spring-web-mvc/src/main/java/com/nike/backstopper/springsample/model/SampleModel.java index 09398f9..3e3fbd6 100644 --- a/samples/sample-spring-web-mvc/src/main/java/com/nike/backstopper/springsample/model/SampleModel.java +++ b/samples/sample-spring-web-mvc/src/main/java/com/nike/backstopper/springsample/model/SampleModel.java @@ -6,10 +6,10 @@ import com.nike.backstopper.springsample.error.SampleProjectApiErrorsImpl; import com.nike.backstopper.validation.constraints.StringConvertsToClassType; -import org.hibernate.validator.constraints.NotBlank; import org.hibernate.validator.constraints.Range; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; /** * Simple model class showing the JSR 303 Bean Validation integration in Backstopper. Each message for a JSR 303 diff --git a/settings.gradle b/settings.gradle index b2e65a2..1cb6618 100644 --- a/settings.gradle +++ b/settings.gradle @@ -8,7 +8,7 @@ include "nike-internal-util", "backstopper-jackson", "backstopper-servlet-api", "backstopper-spring-web", - "backstopper-spring-web-mvc" + "backstopper-spring-web-mvc", // "backstopper-spring-web-flux", // "backstopper-spring-boot1", // "backstopper-spring-boot2-webmvc", @@ -20,7 +20,7 @@ include "nike-internal-util", // "testonly:testonly-springboot2-webmvc", // "testonly:testonly-springboot2-webflux", // // Sample modules (not published) -// "samples:sample-spring-web-mvc", + "samples:sample-spring-web-mvc" // "samples:sample-spring-boot1", // "samples:sample-spring-boot2-webmvc", // "samples:sample-spring-boot2-webflux", \ No newline at end of file From 7621ee6c05fdc94dff6c131dd30163bcb3abf2f0 Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Tue, 10 Sep 2024 17:21:49 -0700 Subject: [PATCH 14/42] Migrate backstopper-spring-web-flux from javax to jakarta and move common stuff back to backstopper-spring-web --- backstopper-spring-web-flux/README.md | 14 +- backstopper-spring-web-flux/build.gradle | 18 +- .../SpringWebfluxApiExceptionHandler.java | 6 +- ...SpringWebfluxApiExceptionHandlerUtils.java | 4 +- ...pringWebfluxUnhandledExceptionHandler.java | 6 +- .../BackstopperSpringWebFluxConfig.java | 2 +- ...ebFluxApiExceptionHandlerListenerList.java | 6 +- ...FluxFrameworkExceptionHandlerListener.java | 217 +---- .../SpringWebfluxApiExceptionHandlerTest.java | 4 +- ...ngWebfluxApiExceptionHandlerUtilsTest.java | 4 +- ...gWebfluxUnhandledExceptionHandlerTest.java | 4 +- ...BackstopperSpringWebFluxComponentTest.java | 114 ++- .../componenttest/model/SampleModel.java | 4 +- ...FrameworkExceptionHandlerListenerTest.java | 628 --------------- backstopper-spring-web-mvc/README.md | 2 +- backstopper-spring-web/README.md | 2 +- ...mmonFrameworkExceptionHandlerListener.java | 291 ++++++- ...FrameworkExceptionHandlerListenerTest.java | 756 +++++++++++++++++- build.gradle | 3 +- settings.gradle | 2 +- 20 files changed, 1166 insertions(+), 921 deletions(-) diff --git a/backstopper-spring-web-flux/README.md b/backstopper-spring-web-flux/README.md index 64e306e..caae3f0 100644 --- a/backstopper-spring-web-flux/README.md +++ b/backstopper-spring-web-flux/README.md @@ -1,14 +1,17 @@ # Backstopper - spring-web-flux -Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 7 and greater -(although for this Backstopper+Spring WebFlux module, Java 8 is required since Spring WebFlux requires Java 8). +Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 17 and greater. + +(NOTE: The [Backstopper 1.x branch](https://github.com/Nike-Inc/backstopper/tree/v1.x) contains a version of +Backstopper for Java 7+, and for the `javax` ecosystem. The current Backstopper supports Java 17+ and the `jakarta` +ecosystem. The Backstopper 1.x releases also contain support for Spring 4 and 5, and Springboot 1 and 2.) This readme focuses specifically on the Backstopper Spring WebFlux integration. If you are looking for a different framework integration check out the [relevant section](../README.md#framework_modules) of the base readme to see if one already exists. The [base project README.md](../README.md) and [User Guide](../USER_GUIDE.md) contain the main bulk of information regarding Backstopper. -**NOTE: There is a [Spring Boot 2 WebFlux sample application](../samples/sample-spring-boot2-webflux/) that provides +**NOTE: There is a [Spring Boot 3 WebFlux sample application](../samples/sample-spring-boot3-webflux/) that provides a simple concrete example of the information covered in this readme.** _ALSO NOTE: **This library does not cover Spring Web MVC (Servlet) applications.** Spring Web MVC and Spring WebFlux @@ -16,8 +19,7 @@ _ALSO NOTE: **This library does not cover Spring Web MVC (Servlet) applications. If you're looking for a Spring Web MVC based integration, then you should see the following Backstopper libraries depending on your application:_ -* [backstopper-spring-boot1](../backstopper-spring-boot1) - For Spring Boot 1 + Spring MVC applications. -* [backstopper-spring-boot2-webmvc](../backstopper-spring-boot2-webmvc) - For Spring Boot 2 + Spring MVC applications. +* [backstopper-spring-boot3-webmvc](../backstopper-spring-boot3-webmvc) - For Spring Boot 3 + Spring MVC applications. * [backstopper-spring-web-mvc](../backstopper-spring-web-mvc) - For Spring Web MVC applications that are not Spring Boot. @@ -35,7 +37,7 @@ related details. default list of `ApiExceptionHandlerListener` listeners that should be sufficient for most projects. You can override that list of listeners (and/or many other Backstopper components) if needed in your project's Spring config. -* Expose your project's `ProjectApiErrors` and a JSR 303 `javax.validation.Validator` implementation in your Spring +* Expose your project's `ProjectApiErrors` and a JSR 303 `jakarta.validation.Validator` implementation in your Spring dependency injection config. * `ProjectApiErrors` creation is discussed in the base Backstopper readme [here](../README.md#quickstart_usage_project_api_errors). diff --git a/backstopper-spring-web-flux/build.gradle b/backstopper-spring-web-flux/build.gradle index b3413a7..2ce61e8 100644 --- a/backstopper-spring-web-flux/build.gradle +++ b/backstopper-spring-web-flux/build.gradle @@ -1,8 +1,5 @@ evaluationDependsOn(':') -sourceCompatibility = JavaVersion.VERSION_1_8 -targetCompatibility = JavaVersion.VERSION_1_8 - dependencies { api( project(":backstopper-jackson"), @@ -10,8 +7,8 @@ dependencies { ) compileOnly( "org.jetbrains:annotations:$jetbrainsAnnotationsVersion", - "org.springframework:spring-webflux:$spring5Version", - "org.springframework:spring-context:$spring5Version", + "org.springframework:spring-webflux:$spring6Version", + "org.springframework:spring-context:$spring6Version", ) testImplementation( project(":backstopper-core").sourceSets.test.output, @@ -23,12 +20,9 @@ dependencies { "org.assertj:assertj-core:$assertJVersion", "com.tngtech.java:junit-dataprovider:$junitDataproviderVersion", "io.rest-assured:rest-assured:$restAssuredVersion", - "org.springframework.boot:spring-boot-starter-webflux:$springboot2Version", - "javax.validation:validation-api:$javaxValidationVersionForNewerSpring", - "org.hibernate:hibernate-validator:$hibernateValidatorVersionForNewerSpring", - "javax.el:javax.el-api:$elApiVersion", // The el-api and el-impl are needed for the JSR 303 validation - "org.glassfish:javax.el:$elImplVersion", - // The jaxb-api is needed for building on the java 11 JDK as these classes were moved out of the Java SE libs. - "javax.xml.bind:jaxb-api:$jaxbApiVersion", + "org.springframework.boot:spring-boot-starter-webflux:$springboot3Version", + "jakarta.validation:jakarta.validation-api:$jakartaValidationVersion", + "org.hibernate.validator:hibernate-validator:$hibernateValidatorVersion", + "org.glassfish.expressly:expressly:$glassfishExpresslyVersion", ) } diff --git a/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandler.java b/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandler.java index c8506f4..2d18b8d 100644 --- a/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandler.java +++ b/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandler.java @@ -35,9 +35,9 @@ import java.util.Set; import java.util.stream.Collectors; -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Singleton; import reactor.core.publisher.Mono; diff --git a/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandlerUtils.java b/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandlerUtils.java index ea61063..1402c23 100644 --- a/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandlerUtils.java +++ b/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandlerUtils.java @@ -10,8 +10,8 @@ import java.util.Collection; -import javax.inject.Named; -import javax.inject.Singleton; +import jakarta.inject.Named; +import jakarta.inject.Singleton; import reactor.core.publisher.Mono; diff --git a/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxUnhandledExceptionHandler.java b/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxUnhandledExceptionHandler.java index fc03ad2..b476bd6 100644 --- a/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxUnhandledExceptionHandler.java +++ b/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxUnhandledExceptionHandler.java @@ -30,9 +30,9 @@ import java.util.Set; import java.util.stream.Collectors; -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Singleton; import reactor.core.publisher.Mono; diff --git a/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/config/BackstopperSpringWebFluxConfig.java b/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/config/BackstopperSpringWebFluxConfig.java index cb2dc51..590175d 100644 --- a/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/config/BackstopperSpringWebFluxConfig.java +++ b/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/config/BackstopperSpringWebFluxConfig.java @@ -16,7 +16,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -import javax.validation.Validator; +import jakarta.validation.Validator; /** * This Spring WebFlux configuration is an alternative to simply scanning all of {@code com.nike.backstopper}. You can diff --git a/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/listener/SpringWebFluxApiExceptionHandlerListenerList.java b/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/listener/SpringWebFluxApiExceptionHandlerListenerList.java index 937458d..b57a450 100644 --- a/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/listener/SpringWebFluxApiExceptionHandlerListenerList.java +++ b/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/listener/SpringWebFluxApiExceptionHandlerListenerList.java @@ -12,9 +12,9 @@ import java.util.Arrays; import java.util.List; -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Singleton; /** * Specifies the list of {@link ApiExceptionHandlerListener}s that should be available to diff --git a/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/listener/impl/OneOffSpringWebFluxFrameworkExceptionHandlerListener.java b/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/listener/impl/OneOffSpringWebFluxFrameworkExceptionHandlerListener.java index 5751bdc..70ebbea 100644 --- a/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/listener/impl/OneOffSpringWebFluxFrameworkExceptionHandlerListener.java +++ b/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/listener/impl/OneOffSpringWebFluxFrameworkExceptionHandlerListener.java @@ -1,35 +1,16 @@ package com.nike.backstopper.handler.spring.webflux.listener.impl; import com.nike.backstopper.apierror.ApiError; -import com.nike.backstopper.apierror.ApiErrorBase; -import com.nike.backstopper.apierror.ApiErrorWithMetadata; import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; import com.nike.backstopper.handler.ApiExceptionHandlerUtils; import com.nike.backstopper.handler.listener.ApiExceptionHandlerListenerResult; import com.nike.backstopper.handler.spring.listener.impl.OneOffSpringCommonFrameworkExceptionHandlerListener; -import com.nike.internal.util.Pair; import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import org.springframework.beans.ConversionNotSupportedException; -import org.springframework.beans.TypeMismatchException; -import org.springframework.core.codec.DecodingException; -import org.springframework.web.server.MediaTypeNotSupportedStatusException; -import org.springframework.web.server.MethodNotAllowedException; -import org.springframework.web.server.NotAcceptableStatusException; -import org.springframework.web.server.ResponseStatusException; -import org.springframework.web.server.ServerErrorException; -import org.springframework.web.server.ServerWebInputException; -import org.springframework.web.server.UnsupportedMediaTypeStatusException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.stream.Collectors; - -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Singleton; /** * An extension and concrete implementation of {@link OneOffSpringCommonFrameworkExceptionHandlerListener} that @@ -59,199 +40,9 @@ public OneOffSpringWebFluxFrameworkExceptionHandlerListener(ProjectApiErrors pro protected @NotNull ApiExceptionHandlerListenerResult handleSpringMvcOrWebfluxSpecificFrameworkExceptions( @NotNull Throwable ex ) { - if (ex instanceof ResponseStatusException) { - return handleResponseStatusException((ResponseStatusException)ex); - } + // If/when we get webflux specific exceptions, they would be handled here. // This exception is not handled here. return ApiExceptionHandlerListenerResult.ignoreResponse(); } - - protected @NotNull ApiExceptionHandlerListenerResult handleResponseStatusException( - @NotNull ResponseStatusException ex - ) { - // ResponseStatusException is technically in spring-web, so it could be handled in backstopper-spring-web's - // OneOffSpringCommonFrameworkExceptionHandlerListener, except that it's also spring 5 so we'd have to - // have yet another module for backstopper-spring-web5. Or we'd have to do a bunch of obnoxious reflection. - // Since Spring WebFlux seems to be the only place ResponseStatusException is used, we'll just shove this - // logic here for now. It can be moved later if needed. - int statusCode = ex.getStatus().value(); - List> extraDetailsForLogging = new ArrayList<>(); - utils.addBaseExceptionMessageToExtraDetailsForLogging(ex, extraDetailsForLogging); - addExtraDetailsForLoggingForResponseStatusException(ex, extraDetailsForLogging); - - // Search for a more specific way to handle this based on the cause. - Throwable exCause = ex.getCause(); - if (exCause instanceof TypeMismatchException) { - // If the cause is a TypeMismatchException and status code is acceptable, then we can have the - // handleTypeMismatchException(...) method deal with it for a more specific response. - - // For safety make sure the status code is one we expect. - TypeMismatchException tmeCause = (TypeMismatchException) ex.getCause(); - int expectedStatusCode = (tmeCause instanceof ConversionNotSupportedException) ? 500 : 400; - if (statusCode == expectedStatusCode) { - // The specific cause exception type and the status code match, - // so we can use handleTypeMismatchException(...). - return handleTypeMismatchException(tmeCause, extraDetailsForLogging, false); - } - } - else if (exCause instanceof DecodingException && statusCode == 400) { - return handleError(projectApiErrors.getMalformedRequestApiError(), extraDetailsForLogging); - } - - // Exception cause didn't help. Try parsing the reason message. - String exReason = (ex.getReason() == null) ? "" : ex.getReason(); - String[] exReasonWords = exReason.split(" "); - - RequiredParamData missingRequiredParam = parseExReasonForMissingRequiredParam(exReasonWords, exReason); - if (missingRequiredParam != null && statusCode == 400) { - return handleError( - new ApiErrorWithMetadata( - projectApiErrors.getMalformedRequestApiError(), - Pair.of("missing_param_name", missingRequiredParam.paramName), - Pair.of("missing_param_type", missingRequiredParam.paramType) - ), - extraDetailsForLogging - ); - } - else if (exReason.startsWith("Request body is missing") && statusCode == 400) { - return handleError(projectApiErrors.getMissingExpectedContentApiError(), extraDetailsForLogging); - } - - // For any other ResponseStatusException we'll search for an appropriate ApiError by status code. - return handleError( - determineApiErrorToUseForGenericResponseStatusCode(statusCode), - extraDetailsForLogging - ); - } - - protected void addExtraDetailsForLoggingForResponseStatusException( - @NotNull ResponseStatusException ex, - @NotNull List> extraDetailsForLogging - ) { - if (ex instanceof MediaTypeNotSupportedStatusException) { - MediaTypeNotSupportedStatusException detailsEx = (MediaTypeNotSupportedStatusException)ex; - extraDetailsForLogging.add( - Pair.of("supported_media_types", concatenateCollectionToString(detailsEx.getSupportedMediaTypes())) - ); - } - - if (ex instanceof MethodNotAllowedException) { - MethodNotAllowedException detailsEx = (MethodNotAllowedException)ex; - extraDetailsForLogging.add( - Pair.of("supported_methods", concatenateCollectionToString(detailsEx.getSupportedMethods())) - ); - } - - if (ex instanceof NotAcceptableStatusException) { - NotAcceptableStatusException detailsEx = (NotAcceptableStatusException)ex; - extraDetailsForLogging.add( - Pair.of("supported_media_types", concatenateCollectionToString(detailsEx.getSupportedMediaTypes())) - ); - } - - if (ex instanceof ServerErrorException) { - ServerErrorException detailsEx = (ServerErrorException)ex; - extraDetailsForLogging.add( - Pair.of("method_parameter", String.valueOf(detailsEx.getMethodParameter())) - ); - extraDetailsForLogging.add( - Pair.of("handler_method", String.valueOf(detailsEx.getHandlerMethod())) - ); - } - - if (ex instanceof ServerWebInputException) { - ServerWebInputException detailsEx = (ServerWebInputException)ex; - extraDetailsForLogging.add( - Pair.of("method_parameter", String.valueOf(detailsEx.getMethodParameter())) - ); - } - - if (ex instanceof UnsupportedMediaTypeStatusException) { - UnsupportedMediaTypeStatusException detailsEx = (UnsupportedMediaTypeStatusException)ex; - extraDetailsForLogging.add( - Pair.of("supported_media_types", concatenateCollectionToString(detailsEx.getSupportedMediaTypes())) - ); - extraDetailsForLogging.add(Pair.of("java_body_type", String.valueOf(detailsEx.getBodyType()))); - } - } - - protected @NotNull String concatenateCollectionToString(@Nullable Collection collection) { - if (collection == null) { - return ""; - } - return collection.stream().map(Object::toString).collect(Collectors.joining(",")); - } - - protected @NotNull ApiError determineApiErrorToUseForGenericResponseStatusCode(int statusCode) { - switch (statusCode) { - case 400: - return projectApiErrors.getGenericBadRequestApiError(); - case 401: - return projectApiErrors.getUnauthorizedApiError(); - case 403: - return projectApiErrors.getForbiddenApiError(); - case 404: - return projectApiErrors.getNotFoundApiError(); - case 405: - return projectApiErrors.getMethodNotAllowedApiError(); - case 406: - return projectApiErrors.getNoAcceptableRepresentationApiError(); - case 415: - return projectApiErrors.getUnsupportedMediaTypeApiError(); - case 429: - return projectApiErrors.getTooManyRequestsApiError(); - case 500: - return projectApiErrors.getGenericServiceError(); - case 503: - return projectApiErrors.getTemporaryServiceProblemApiError(); - } - - // If we reach here then it wasn't a status code where we have a common ApiError in ProjectApiErrors. - // Generate a generic ApiError to cover it. - return generateGenericApiErrorForResponseStatusCode(statusCode); - } - - protected @NotNull ApiError generateGenericApiErrorForResponseStatusCode(int statusCode) { - // Reuse the error code for the generic bad request ApiError, unless the status code is greater than or equal - // to 500. If status code >= 500, then use the generic service error status code instead. - String errorCodeToUse = projectApiErrors.getGenericBadRequestApiError().getErrorCode(); - if (statusCode >= 500) { - errorCodeToUse = projectApiErrors.getGenericServiceError().getErrorCode(); - } - - return new ApiErrorBase( - "GENERIC_API_ERROR_FOR_RESPONSE_STATUS_CODE_" + statusCode, - errorCodeToUse, - "An error occurred that resulted in response status code " + statusCode, - statusCode - ); - } - - protected @Nullable RequiredParamData parseExReasonForMissingRequiredParam( - @NotNull String[] exReasonWords, @NotNull String exReason - ) { - if (exReasonWords.length != 7) { - return null; - } - - if ("Required".equals(exReasonWords[0]) - && "parameter".equals(exReasonWords[2]) - && exReason.endsWith("is not present") - ) { - return new RequiredParamData(exReasonWords[3].replace("'", ""), exReasonWords[1]); - } - - return null; - } - - protected static class RequiredParamData { - public final String paramName; - public final String paramType; - - public RequiredParamData(String paramName, String paramType) { - this.paramName = paramName; - this.paramType = paramType; - } - } } diff --git a/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandlerTest.java b/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandlerTest.java index 19fc54a..cc286f2 100644 --- a/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandlerTest.java +++ b/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandlerTest.java @@ -50,8 +50,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; diff --git a/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandlerUtilsTest.java b/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandlerUtilsTest.java index 9490950..0a3d839 100644 --- a/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandlerUtilsTest.java +++ b/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandlerUtilsTest.java @@ -29,7 +29,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; /** * Tests the functionality of {@link SpringWebfluxApiExceptionHandlerUtils}. @@ -135,7 +135,7 @@ public void getErrorResponseContentType_returns_APPLICATION_JSON_UTF8_by_default // then assertThat(result).isEqualTo(MediaType.APPLICATION_JSON_UTF8); - verifyZeroInteractions(errorContractDtoMock, errors, ex, requestMock); + verifyNoMoreInteractions(errorContractDtoMock, errors, ex, requestMock); } } diff --git a/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxUnhandledExceptionHandlerTest.java b/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxUnhandledExceptionHandlerTest.java index 84fbaab..20d73bf 100644 --- a/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxUnhandledExceptionHandlerTest.java +++ b/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxUnhandledExceptionHandlerTest.java @@ -44,8 +44,8 @@ import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; diff --git a/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/componenttest/BackstopperSpringWebFluxComponentTest.java b/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/componenttest/BackstopperSpringWebFluxComponentTest.java index 66877ec..4de192f 100644 --- a/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/componenttest/BackstopperSpringWebFluxComponentTest.java +++ b/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/componenttest/BackstopperSpringWebFluxComponentTest.java @@ -55,6 +55,7 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; @@ -82,10 +83,10 @@ import java.util.Map; import java.util.UUID; -import javax.inject.Singleton; -import javax.validation.Valid; -import javax.validation.Validation; -import javax.validation.Validator; +import jakarta.inject.Singleton; +import jakarta.validation.Valid; +import jakarta.validation.Validation; +import jakarta.validation.Validator; import io.restassured.http.ContentType; import io.restassured.response.ExtractableResponse; @@ -97,6 +98,7 @@ import static com.nike.backstopper.handler.spring.webflux.componenttest.BackstopperSpringWebFluxComponentTest.ComponentTestController.FLUX_ENDPOINT_PATH; import static com.nike.backstopper.handler.spring.webflux.componenttest.BackstopperSpringWebFluxComponentTest.ComponentTestController.FLUX_RESPONSE_PAYLOAD; import static com.nike.backstopper.handler.spring.webflux.componenttest.BackstopperSpringWebFluxComponentTest.ComponentTestController.GET_SAMPLE_MODEL_ENDPOINT; +import static com.nike.backstopper.handler.spring.webflux.componenttest.BackstopperSpringWebFluxComponentTest.ComponentTestController.INT_HEADER_REQUIRED_ENDPOINT; import static com.nike.backstopper.handler.spring.webflux.componenttest.BackstopperSpringWebFluxComponentTest.ComponentTestController.INT_QUERY_PARAM_REQUIRED_ENDPOINT; import static com.nike.backstopper.handler.spring.webflux.componenttest.BackstopperSpringWebFluxComponentTest.ComponentTestController.MONO_ENDPOINT_PATH; import static com.nike.backstopper.handler.spring.webflux.componenttest.BackstopperSpringWebFluxComponentTest.ComponentTestController.MONO_RESPONSE_PAYLOAD; @@ -398,7 +400,7 @@ public void verify_sample_post_fails_with_UNSUPPORTED_MEDIA_TYPE_if_passed_inval } @Test - public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert_type() { + public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert_type_for_query_param() { ExtractableResponse response = given() .baseUri("http://localhost") @@ -415,7 +417,49 @@ public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert SampleCoreApiError.TYPE_CONVERSION_ERROR, // We can't expect the bad_property_name=requiredQueryParamValue metadata like we do in Spring Web MVC, // because Spring WebFlux doesn't add it to the TypeMismatchException cause. - MapBuilder.builder("bad_property_value", (Object) "not-an-integer") + MapBuilder.builder("bad_property_name", (Object) "requiredQueryParamValue") + .put("bad_property_value", "not-an-integer") + .put("required_location", "query_param") + .put("required_type", "int") + .build() + ); + + verifyErrorReceived(response, expectedApiError); + ServerWebInputException ex = verifyResponseStatusExceptionSeenByBackstopper( + ServerWebInputException.class, 400 + ); + TypeMismatchException tme = verifyExceptionHasCauseOfType(ex, TypeMismatchException.class); + verifyHandlingResult( + expectedApiError, + Pair.of("exception_message", quotesToApostrophes(ex.getMessage())), + Pair.of("method_parameter", ex.getMethodParameter().toString()), + Pair.of("bad_property_name", tme.getPropertyName()), + Pair.of("bad_property_value", tme.getValue().toString()), + Pair.of("required_type", tme.getRequiredType().toString()) + ); + } + + @Test + public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert_type_for_header() { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(SERVER_PORT) + .log().all() + .when() + .header("requiredHeaderValue", "not-an-integer") + .get(INT_HEADER_REQUIRED_ENDPOINT) + .then() + .log().all() + .extract(); + + ApiError expectedApiError = new ApiErrorWithMetadata( + SampleCoreApiError.TYPE_CONVERSION_ERROR, + // We can't expect the bad_property_name=requiredQueryParamValue metadata like we do in Spring Web MVC, + // because Spring WebFlux doesn't add it to the TypeMismatchException cause. + MapBuilder.builder("bad_property_name", (Object) "requiredHeaderValue") + .put("bad_property_value", "not-an-integer") + .put("required_location", "header") .put("required_type", "int") .build() ); @@ -463,7 +507,7 @@ public void verify_ResponseStatusException_with_TypeMismatchException_is_handled } @Test - public void verify_MALFORMED_REQUEST_is_thrown_when_required_data_is_missing_and_error_metadata_must_be_extracted_from_ex_reason() { + public void verify_MALFORMED_REQUEST_is_thrown_when_required_query_param_is_missing_and_error_metadata_must_be_extracted_from_ex_reason() { ExtractableResponse response = given() .baseUri("http://localhost") @@ -478,7 +522,8 @@ public void verify_MALFORMED_REQUEST_is_thrown_when_required_data_is_missing_and ApiError expectedApiError = new ApiErrorWithMetadata( SampleCoreApiError.MALFORMED_REQUEST, Pair.of("missing_param_type", "int"), - Pair.of("missing_param_name", "requiredQueryParamValue") + Pair.of("missing_param_name", "requiredQueryParamValue"), + Pair.of("required_location", "query_param") ); verifyErrorReceived(response, expectedApiError); @@ -492,7 +537,41 @@ public void verify_MALFORMED_REQUEST_is_thrown_when_required_data_is_missing_and ); // Verify no cause, leaving the exception `reason` as the only way we could have gotten the metadata. assertThat(ex).hasNoCause(); - assertThat(ex.getReason()).isEqualTo("Required int parameter 'requiredQueryParamValue' is not present"); + assertThat(ex.getReason()).isEqualTo("Required query parameter 'requiredQueryParamValue' is not present."); + } + + @Test + public void verify_MALFORMED_REQUEST_is_thrown_when_required_header_is_missing_and_error_metadata_must_be_extracted_from_ex_reason() { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(SERVER_PORT) + .log().all() + .when() + .get(INT_HEADER_REQUIRED_ENDPOINT) + .then() + .log().all() + .extract(); + + ApiError expectedApiError = new ApiErrorWithMetadata( + SampleCoreApiError.MALFORMED_REQUEST, + Pair.of("missing_param_type", "int"), + Pair.of("missing_param_name", "requiredHeaderValue"), + Pair.of("required_location", "header") + ); + + verifyErrorReceived(response, expectedApiError); + ServerWebInputException ex = verifyResponseStatusExceptionSeenByBackstopper( + ServerWebInputException.class, 400 + ); + verifyHandlingResult( + expectedApiError, + Pair.of("exception_message", quotesToApostrophes(ex.getMessage())), + Pair.of("method_parameter", ex.getMethodParameter().toString()) + ); + // Verify no cause, leaving the exception `reason` as the only way we could have gotten the metadata. + assertThat(ex).hasNoCause(); + assertThat(ex.getReason()).isEqualTo("Required header 'requiredHeaderValue' is not present."); } @Test @@ -567,9 +646,9 @@ public void verify_sample_post_fails_with_MISSING_EXPECTED_CONTENT_if_passed_emp Pair.of("exception_message", quotesToApostrophes(ex.getMessage())), Pair.of("method_parameter", ex.getMethodParameter().toString()) ); - // Verify no cause, leaving the exception `reason` as the only way we could have determined this case. - assertThat(ex).hasNoCause(); - assertThat(ex.getReason()).startsWith("Request body is missing"); + // Verify DecodingException as the cause, leaving the exception `reason` as the only way we could have determined this case. + assertThat(ex).hasCauseInstanceOf(DecodingException.class); + assertThat(ex.getReason()).isEqualTo("No request body"); } @Test @@ -824,7 +903,7 @@ private T verifyResponseStatusExceptionSeenB ) { T ex = verifyExceptionSeenByBackstopper(expectedClassType); - int actualStatusCode = ex.getStatus().value(); + int actualStatusCode = ex.getStatusCode().value(); assertThat(actualStatusCode).isEqualTo(expectedStatusCode); return ex; @@ -934,6 +1013,7 @@ static class ComponentTestController { static final String CONVERSION_NOT_SUPPORTED_EXCEPTION_ENDPOINT_PATH = "/triggerConversionNotSupportedException"; static final String INT_QUERY_PARAM_REQUIRED_ENDPOINT = "/intQueryParamRequiredEndpoint"; + static final String INT_HEADER_REQUIRED_ENDPOINT = "/intHeaderRequiredEndpoint"; static final String TYPE_MISMATCH_WITH_UNEXPECTED_STATUS_ENDPOINT = "/typeMismatchWithUnexpectedStatusEndpoint"; static final String GET_SAMPLE_MODEL_ENDPOINT = "/getSampleModel"; static final String POST_SAMPLE_MODEL_ENDPOINT_WITH_JSR_303_VALIDATION = @@ -1046,6 +1126,14 @@ public String intQueryParamRequiredEndpoint( return "You passed in " + someRequiredQueryParam + " for the required query param value"; } + @GetMapping(path = INT_HEADER_REQUIRED_ENDPOINT) + @ResponseBody + public String intHeaderRequiredEndpoint( + @RequestHeader(name = "requiredHeaderValue") int someRequiredHeader + ) { + return "You passed in " + someRequiredHeader + " for the required header value"; + } + // Mismatch between the path param {foo} and the name we gave the @PathVariable triggers a ServerErrorException. @GetMapping(path = SERVER_ERROR_EXCEPTION_ENDPOINT_PATH) @ResponseBody diff --git a/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/componenttest/model/SampleModel.java b/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/componenttest/model/SampleModel.java index 554d1bf..70683c6 100644 --- a/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/componenttest/model/SampleModel.java +++ b/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/componenttest/model/SampleModel.java @@ -4,8 +4,8 @@ import org.hibernate.validator.constraints.Range; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; public class SampleModel { @NotBlank(message = "FOO_STRING_CANNOT_BE_BLANK") diff --git a/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/listener/impl/OneOffSpringWebFluxFrameworkExceptionHandlerListenerTest.java b/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/listener/impl/OneOffSpringWebFluxFrameworkExceptionHandlerListenerTest.java index f7dab46..a434159 100644 --- a/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/listener/impl/OneOffSpringWebFluxFrameworkExceptionHandlerListenerTest.java +++ b/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/listener/impl/OneOffSpringWebFluxFrameworkExceptionHandlerListenerTest.java @@ -1,10 +1,7 @@ package com.nike.backstopper.handler.spring.webflux.listener.impl; import com.nike.backstopper.apierror.ApiError; -import com.nike.backstopper.apierror.ApiErrorBase; -import com.nike.backstopper.apierror.ApiErrorWithMetadata; import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.apierror.testutil.BarebonesCoreApiErrorForTesting; import com.nike.backstopper.apierror.testutil.ProjectApiErrorsForTesting; import com.nike.backstopper.exception.ApiException; import com.nike.backstopper.handler.ApiExceptionHandlerUtils; @@ -12,41 +9,17 @@ import com.nike.internal.util.Pair; import com.nike.internal.util.testing.Glassbox; -import com.tngtech.java.junit.dataprovider.DataProvider; import com.tngtech.java.junit.dataprovider.DataProviderRunner; -import com.tngtech.java.junit.dataprovider.UseDataProvider; import org.assertj.core.api.Assertions; import org.junit.Test; import org.junit.runner.RunWith; -import org.springframework.beans.ConversionNotSupportedException; -import org.springframework.beans.TypeMismatchException; -import org.springframework.core.MethodParameter; -import org.springframework.core.ResolvableType; -import org.springframework.core.codec.DecodingException; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.web.server.MediaTypeNotSupportedStatusException; -import org.springframework.web.server.MethodNotAllowedException; -import org.springframework.web.server.NotAcceptableStatusException; -import org.springframework.web.server.ResponseStatusException; -import org.springframework.web.server.ServerErrorException; -import org.springframework.web.server.ServerWebInputException; -import org.springframework.web.server.UnsupportedMediaTypeStatusException; -import java.beans.PropertyChangeEvent; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Optional; -import java.util.UUID; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import static java.util.Collections.singleton; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -143,605 +116,4 @@ public void handleFluxExceptions_returns_ignoreResponse_if_passed_exception_it_d validateResponse(result, false, null); } - private enum TypeMismatchExceptionScenario { - CONVERSION_NOT_SUPPORTED_500( - HttpStatus.resolve(500), - new ConversionNotSupportedException( - new PropertyChangeEvent("doesNotMatter", "somePropertyName", "oldValue", "newValue"), - Integer.class, - null - ), - testProjectApiErrors.getGenericServiceError(), - Arrays.asList( - Pair.of("bad_property_name", "somePropertyName"), - Pair.of("bad_property_value", "newValue"), - Pair.of("required_type", Integer.class.toString()) - ) - ), - GENERIC_TYPE_MISMATCH_EXCEPTION_400( - HttpStatus.resolve(400), - new TypeMismatchException( - new PropertyChangeEvent("doesNotMatter", "somePropertyName", "oldValue", "newValue"), - Integer.class - ), - new ApiErrorWithMetadata( - testProjectApiErrors.getTypeConversionApiError(), - Pair.of("bad_property_name", "somePropertyName"), - Pair.of("bad_property_value", "newValue"), - Pair.of("required_type", "int") - ), - Arrays.asList( - Pair.of("bad_property_name", "somePropertyName"), - Pair.of("bad_property_value", "newValue"), - Pair.of("required_type", Integer.class.toString()) - ) - ), - UNEXPECTED_4XX_STATUS_CODE( - HttpStatus.resolve(403), - new TypeMismatchException("doesNotMatter", Integer.class), - testProjectApiErrors.getForbiddenApiError(), - Collections.emptyList() - ), - UNEXPECTED_5XX_STATUS_CODE( - HttpStatus.resolve(503), - new TypeMismatchException("doesNotMatter", Integer.class), - testProjectApiErrors.getTemporaryServiceProblemApiError(), - Collections.emptyList() - ), - UNKNOWN_4XX_STATUS_CODE( - HttpStatus.resolve(418), - new TypeMismatchException("doesNotMatter", Integer.class), - new ApiErrorBase( - "GENERIC_API_ERROR_FOR_RESPONSE_STATUS_CODE_418", - testProjectApiErrors.getGenericBadRequestApiError().getErrorCode(), - "An error occurred that resulted in response status code 418", - 418 - ), - Collections.emptyList() - ), - UNKNOWN_5XX_STATUS_CODE( - HttpStatus.resolve(509), - new TypeMismatchException("doesNotMatter", Integer.class), - new ApiErrorBase( - "GENERIC_API_ERROR_FOR_RESPONSE_STATUS_CODE_509", - testProjectApiErrors.getGenericServiceError().getErrorCode(), - "An error occurred that resulted in response status code 509", - 509 - ), - Collections.emptyList() - ); - - public final HttpStatus status; - public final TypeMismatchException tmeCause; - public final ApiError expectedApiError; - public final List> expectedExtraDetailsForLogging; - - TypeMismatchExceptionScenario( - HttpStatus status, TypeMismatchException tmeCause, ApiError expectedApiError, - List> expectedExtraDetailsForLogging - ) { - this.status = status; - this.tmeCause = tmeCause; - this.expectedApiError = expectedApiError; - this.expectedExtraDetailsForLogging = expectedExtraDetailsForLogging; - } - } - - @DataProvider - public static List> typeMismatchExceptionScenarioDataProvider() { - return Stream.of(TypeMismatchExceptionScenario.values()) - .map(Collections::singletonList) - .collect(Collectors.toList()); - } - - @UseDataProvider("typeMismatchExceptionScenarioDataProvider") - @Test - public void handleFluxExceptions_handles_ResponseStatusException_with_TypeMismatchException_cause_as_expected( - TypeMismatchExceptionScenario scenario - ) { - // given - ResponseStatusException ex = new ResponseStatusException( - scenario.status, "Some ResponseStatusException reason", scenario.tmeCause - ); - List> expectedExtraDetailsForLogging = new ArrayList<>(); - ApiExceptionHandlerUtils.DEFAULT_IMPL.addBaseExceptionMessageToExtraDetailsForLogging( - ex, expectedExtraDetailsForLogging - ); - expectedExtraDetailsForLogging.addAll(scenario.expectedExtraDetailsForLogging); - - // when - ApiExceptionHandlerListenerResult result = listener.handleSpringMvcOrWebfluxSpecificFrameworkExceptions(ex); - - // then - validateResponse( - result, - true, - singleton(scenario.expectedApiError), - expectedExtraDetailsForLogging - ); - } - - @DataProvider(value = { - "400 | MALFORMED_REQUEST", - "401 | UNAUTHORIZED" - }, splitBy = "\\|") - @Test - public void handleFluxExceptions_returns_MALFORMED_REQUEST_for_ResponseStatusException_with_DecodingException_cause_only_if_status_is_400( - int statusCode, BarebonesCoreApiErrorForTesting expectedError - ) { - // given - ResponseStatusException ex = new ResponseStatusException( - HttpStatus.resolve(statusCode), - "Some ResponseStatusException reason", - new DecodingException("Some decoding ex") - ); - List> expectedExtraDetailsForLogging = new ArrayList<>(); - ApiExceptionHandlerUtils.DEFAULT_IMPL.addBaseExceptionMessageToExtraDetailsForLogging( - ex, expectedExtraDetailsForLogging - ); - - // when - ApiExceptionHandlerListenerResult result = listener.handleSpringMvcOrWebfluxSpecificFrameworkExceptions(ex); - - // then - validateResponse( - result, - true, - singleton(expectedError), - expectedExtraDetailsForLogging - ); - } - - @DataProvider(value = { - "400 | Required foo parameter 'bar' is not present | foo | bar | MALFORMED_REQUEST", - "401 | Required foo parameter 'bar' is not present | null | null | UNAUTHORIZED", - "400 | Required parameter 'bar' is not present | null | null | GENERIC_BAD_REQUEST", - "400 | Required foo parameter is not present | null | null | GENERIC_BAD_REQUEST", - "400 | Blah foo parameter 'bar' is not present | null | null | GENERIC_BAD_REQUEST", - "400 | Required foo blah 'bar' is not present | null | null | GENERIC_BAD_REQUEST", - "400 | Required foo parameter 'bar' is not blah | null | null | GENERIC_BAD_REQUEST", - "400 | Some random reason | null | null | GENERIC_BAD_REQUEST", - }, splitBy = "\\|") - @Test - public void handleFluxExceptions_returns_MALFORMED_REQUEST_for_ResponseStatusException_with_special_required_param_reason_string( - int statusCode, - String exReasonString, - String expectedMissingParamType, - String expectedMissingParamName, - BarebonesCoreApiErrorForTesting expectedBaseError - ) { - // given - ResponseStatusException ex = new ResponseStatusException(HttpStatus.resolve(statusCode), exReasonString); - List> expectedExtraDetailsForLogging = new ArrayList<>(); - ApiExceptionHandlerUtils.DEFAULT_IMPL.addBaseExceptionMessageToExtraDetailsForLogging( - ex, expectedExtraDetailsForLogging - ); - - ApiError expectedError = expectedBaseError; - if (expectedMissingParamName != null && expectedMissingParamType != null) { - expectedError = new ApiErrorWithMetadata( - expectedBaseError, - Pair.of("missing_param_name", expectedMissingParamName), - Pair.of("missing_param_type", expectedMissingParamType) - ); - } - - // when - ApiExceptionHandlerListenerResult result = listener.handleSpringMvcOrWebfluxSpecificFrameworkExceptions(ex); - - // then - validateResponse( - result, - true, - singleton(expectedError), - expectedExtraDetailsForLogging - ); - } - - @DataProvider(value = { - "400 | Request body is missing | MISSING_EXPECTED_CONTENT", - "400 | Request body is missing blahblah | MISSING_EXPECTED_CONTENT", - "401 | Request body is missing | UNAUTHORIZED", - "400 | Request body is | GENERIC_BAD_REQUEST", - "400 | Some random reason | GENERIC_BAD_REQUEST", - }, splitBy = "\\|") - @Test - public void handleFluxExceptions_returns_MISSING_EXPECTED_CONTENT_for_ResponseStatusException_with_special_reason_string_beginning( - int statusCode, String exReasonString, BarebonesCoreApiErrorForTesting expectedError - ) { - // given - ResponseStatusException ex = new ResponseStatusException(HttpStatus.resolve(statusCode), exReasonString); - List> expectedExtraDetailsForLogging = new ArrayList<>(); - ApiExceptionHandlerUtils.DEFAULT_IMPL.addBaseExceptionMessageToExtraDetailsForLogging( - ex, expectedExtraDetailsForLogging - ); - - // when - ApiExceptionHandlerListenerResult result = listener.handleSpringMvcOrWebfluxSpecificFrameworkExceptions(ex); - - // then - validateResponse( - result, - true, - singleton(expectedError), - expectedExtraDetailsForLogging - ); - } - - @DataProvider(value = { - "400 | GENERIC_BAD_REQUEST", - "401 | UNAUTHORIZED", - "403 | FORBIDDEN", - "404 | NOT_FOUND", - "405 | METHOD_NOT_ALLOWED", - "406 | NO_ACCEPTABLE_REPRESENTATION", - "415 | UNSUPPORTED_MEDIA_TYPE", - "429 | TOO_MANY_REQUESTS", - "500 | GENERIC_SERVICE_ERROR", - "503 | TEMPORARY_SERVICE_PROBLEM", - }, splitBy = "\\|") - @Test - public void handleFluxExceptions_handles_generic_ResponseStatusException_by_returning_ApiError_from_project_if_status_code_is_known( - int desiredStatusCode, BarebonesCoreApiErrorForTesting expectedError - ) { - // given - ResponseStatusException ex = new ResponseStatusException( - HttpStatus.resolve(desiredStatusCode), "Some ResponseStatusException reason" - ); - List> expectedExtraDetailsForLogging = new ArrayList<>(); - ApiExceptionHandlerUtils.DEFAULT_IMPL.addBaseExceptionMessageToExtraDetailsForLogging( - ex, expectedExtraDetailsForLogging - ); - - // when - ApiExceptionHandlerListenerResult result = listener.handleSpringMvcOrWebfluxSpecificFrameworkExceptions(ex); - - // then - validateResponse( - result, - true, - singleton(expectedError), - expectedExtraDetailsForLogging - ); - } - - @DataProvider(value = { - "418", - "509" - }) - @Test - public void handleFluxExceptions_handles_generic_ResponseStatusException_by_returning_synthetic_ApiError_if_status_code_is_unknown( - int desiredStatusCode - ) { - // given - ResponseStatusException ex = new ResponseStatusException( - HttpStatus.resolve(desiredStatusCode), "Some ResponseStatusException reason" - ); - List> expectedExtraDetailsForLogging = new ArrayList<>(); - ApiExceptionHandlerUtils.DEFAULT_IMPL.addBaseExceptionMessageToExtraDetailsForLogging( - ex, expectedExtraDetailsForLogging - ); - - String expectedErrorCode = (desiredStatusCode >= 500) - ? testProjectApiErrors.getGenericServiceError().getErrorCode() - : testProjectApiErrors.getGenericBadRequestApiError().getErrorCode(); - - ApiError expectedError = new ApiErrorBase( - "GENERIC_API_ERROR_FOR_RESPONSE_STATUS_CODE_" + desiredStatusCode, - expectedErrorCode, - "An error occurred that resulted in response status code " + desiredStatusCode, - desiredStatusCode - ); - - // when - ApiExceptionHandlerListenerResult result = listener.handleSpringMvcOrWebfluxSpecificFrameworkExceptions(ex); - - // then - validateResponse( - result, - true, - singleton(expectedError), - expectedExtraDetailsForLogging - ); - } - - @DataProvider(value = { - "true", - "false" - }) - @Test - public void handleFluxExceptions_handles_MediaTypeNotSupportedStatusException_as_expected( - boolean includesSupportedMediaTypes - ) { - // given - List supportedMediaTypes = Arrays.asList( - MediaType.APPLICATION_JSON, - MediaType.IMAGE_JPEG - ); - MediaTypeNotSupportedStatusException ex = - (includesSupportedMediaTypes) - ? new MediaTypeNotSupportedStatusException(supportedMediaTypes) - : new MediaTypeNotSupportedStatusException("Some reason"); - - List> expectedExtraDetailsForLogging = new ArrayList<>(); - ApiExceptionHandlerUtils.DEFAULT_IMPL.addBaseExceptionMessageToExtraDetailsForLogging( - ex, expectedExtraDetailsForLogging - ); - - String expectedSupportedMediaTypesValueStr = - (includesSupportedMediaTypes) - ? supportedMediaTypes.stream().map(Object::toString).collect(Collectors.joining(",")) - : ""; - - expectedExtraDetailsForLogging.add(Pair.of("supported_media_types", expectedSupportedMediaTypesValueStr)); - - // when - ApiExceptionHandlerListenerResult result = listener.handleSpringMvcOrWebfluxSpecificFrameworkExceptions(ex); - - // then - validateResponse( - result, - true, - singleton(testProjectApiErrors.getUnsupportedMediaTypeApiError()), - expectedExtraDetailsForLogging - ); - } - - @DataProvider(value = { - "true", - "false" - }) - @Test - public void handleFluxExceptions_handles_MethodNotAllowedException_as_expected( - boolean supportedMethodsIsEmpty - ) { - // given - String actualMethod = UUID.randomUUID().toString(); - List supportedMethods = - (supportedMethodsIsEmpty) - ? Collections.emptyList() - : Arrays.asList( - HttpMethod.GET, - HttpMethod.POST - ); - - MethodNotAllowedException ex = new MethodNotAllowedException(actualMethod, supportedMethods); - - List> expectedExtraDetailsForLogging = new ArrayList<>(); - ApiExceptionHandlerUtils.DEFAULT_IMPL.addBaseExceptionMessageToExtraDetailsForLogging( - ex, expectedExtraDetailsForLogging - ); - - // when - ApiExceptionHandlerListenerResult result = listener.handleSpringMvcOrWebfluxSpecificFrameworkExceptions(ex); - - // then - // They throw the supported methods into a plain HashSet so we can't rely on the ordering. - // Verify it another way. - Optional supportedMethodsLoggingDetailsValue = result.extraDetailsForLogging - .stream() - .filter(p -> p.getKey().equals("supported_methods")) - .map(Pair::getValue) - .findAny(); - assertThat(supportedMethodsLoggingDetailsValue).isPresent(); - List actualLoggingDetailsMethods = supportedMethodsLoggingDetailsValue - .map(s -> { - if (s.equals("")) { - return Collections.emptyList(); - } - return Arrays.stream(s.split(",")).map(HttpMethod::valueOf).collect(Collectors.toList()); - }) - .orElse(Collections.emptyList()); - - assertThat(actualLoggingDetailsMethods).containsExactlyInAnyOrderElementsOf(supportedMethods); - - expectedExtraDetailsForLogging.add(Pair.of("supported_methods", supportedMethodsLoggingDetailsValue.get())); - - validateResponse( - result, - true, - singleton(testProjectApiErrors.getMethodNotAllowedApiError()), - expectedExtraDetailsForLogging - ); - } - - @DataProvider(value = { - "true", - "false" - }) - @Test - public void handleFluxExceptions_handles_NotAcceptableStatusException_as_expected( - boolean includesSupportedMediaTypes - ) { - // given - List supportedMediaTypes = Arrays.asList( - MediaType.APPLICATION_JSON, - MediaType.IMAGE_JPEG - ); - NotAcceptableStatusException ex = - (includesSupportedMediaTypes) - ? new NotAcceptableStatusException(supportedMediaTypes) - : new NotAcceptableStatusException("Some reason"); - - List> expectedExtraDetailsForLogging = new ArrayList<>(); - ApiExceptionHandlerUtils.DEFAULT_IMPL.addBaseExceptionMessageToExtraDetailsForLogging( - ex, expectedExtraDetailsForLogging - ); - - String expectedSupportedMediaTypesValueStr = - (includesSupportedMediaTypes) - ? supportedMediaTypes.stream().map(Object::toString).collect(Collectors.joining(",")) - : ""; - - expectedExtraDetailsForLogging.add(Pair.of("supported_media_types", expectedSupportedMediaTypesValueStr)); - - // when - ApiExceptionHandlerListenerResult result = listener.handleSpringMvcOrWebfluxSpecificFrameworkExceptions(ex); - - // then - validateResponse( - result, - true, - singleton(testProjectApiErrors.getNoAcceptableRepresentationApiError()), - expectedExtraDetailsForLogging - ); - } - - @DataProvider(value = { - "true", - "false" - }) - @Test - public void handleFluxExceptions_handles_ServerErrorException_as_expected( - boolean nullDetails - ) throws NoSuchMethodException { - // given - MethodParameter details = new MethodParameter(String.class.getDeclaredMethod("length"), -1); - - ServerErrorException ex = - (nullDetails) - ? new ServerErrorException("Some reason", (Throwable) null) - : new ServerErrorException("Some reason", details, null); - - List> expectedExtraDetailsForLogging = new ArrayList<>(); - ApiExceptionHandlerUtils.DEFAULT_IMPL.addBaseExceptionMessageToExtraDetailsForLogging( - ex, expectedExtraDetailsForLogging - ); - - expectedExtraDetailsForLogging.add( - Pair.of("method_parameter", String.valueOf(ex.getMethodParameter())) - ); - expectedExtraDetailsForLogging.add( - Pair.of("handler_method", String.valueOf(ex.getHandlerMethod())) - ); - - // when - ApiExceptionHandlerListenerResult result = listener.handleSpringMvcOrWebfluxSpecificFrameworkExceptions(ex); - - // then - validateResponse( - result, - true, - singleton(testProjectApiErrors.getGenericServiceError()), - expectedExtraDetailsForLogging - ); - } - - @DataProvider(value = { - "true", - "false" - }) - @Test - public void handleFluxExceptions_handles_ServerWebInputException_as_expected( - boolean nullDetails - ) throws NoSuchMethodException { - // given - MethodParameter details = new MethodParameter(String.class.getDeclaredMethod("length"), -1); - - ServerWebInputException ex = - (nullDetails) - ? new ServerWebInputException("Some reason") - : new ServerWebInputException("Some reason", details); - - List> expectedExtraDetailsForLogging = new ArrayList<>(); - ApiExceptionHandlerUtils.DEFAULT_IMPL.addBaseExceptionMessageToExtraDetailsForLogging( - ex, expectedExtraDetailsForLogging - ); - - expectedExtraDetailsForLogging.add( - Pair.of("method_parameter", String.valueOf(ex.getMethodParameter())) - ); - - // when - ApiExceptionHandlerListenerResult result = listener.handleSpringMvcOrWebfluxSpecificFrameworkExceptions(ex); - - // then - validateResponse( - result, - true, - singleton(testProjectApiErrors.getGenericBadRequestApiError()), - expectedExtraDetailsForLogging - ); - } - - @DataProvider(value = { - "true", - "false" - }) - @Test - public void handleFluxExceptions_handles_UnsupportedMediaTypeStatusException_as_expected( - boolean includeDetails - ) { - // given - MediaType actualMediaType = MediaType.TEXT_PLAIN; - List supportedMediaTypes = Arrays.asList( - MediaType.APPLICATION_JSON, - MediaType.IMAGE_JPEG - ); - ResolvableType javaBodyType = ResolvableType.forClass(Integer.class); - UnsupportedMediaTypeStatusException ex = - (includeDetails) - ? new UnsupportedMediaTypeStatusException(actualMediaType, supportedMediaTypes, javaBodyType) - : new UnsupportedMediaTypeStatusException("Some reason"); - - List> expectedExtraDetailsForLogging = new ArrayList<>(); - ApiExceptionHandlerUtils.DEFAULT_IMPL.addBaseExceptionMessageToExtraDetailsForLogging( - ex, expectedExtraDetailsForLogging - ); - - String expectedSupportedMediaTypesValueStr = - (includeDetails) - ? supportedMediaTypes.stream().map(Object::toString).collect(Collectors.joining(",")) - : ""; - String expectedJavaBodyTypeValueStr = - (includeDetails) - ? javaBodyType.toString() - : "null"; - - expectedExtraDetailsForLogging.add(Pair.of("supported_media_types", expectedSupportedMediaTypesValueStr)); - expectedExtraDetailsForLogging.add(Pair.of("java_body_type", expectedJavaBodyTypeValueStr)); - - // when - ApiExceptionHandlerListenerResult result = listener.handleSpringMvcOrWebfluxSpecificFrameworkExceptions(ex); - - // then - validateResponse( - result, - true, - singleton(testProjectApiErrors.getUnsupportedMediaTypeApiError()), - expectedExtraDetailsForLogging - ); - } - - private enum ConcatenateCollectionToStringScenario { - NULL_COLLECTION(null, ""), - EMPTY_COLLECTION(Collections.emptyList(), ""), - SINGLE_ITEM(Collections.singleton("foo"), "foo"), - MULTIPLE_ITEMS(Arrays.asList("foo", "bar"), "foo,bar"); - - public final Collection collection; - public final String expectedResult; - - ConcatenateCollectionToStringScenario(Collection collection, String expectedResult) { - this.collection = collection; - this.expectedResult = expectedResult; - } - } - - @DataProvider - public static List> concatenateCollectionToStringScenarioDataProvider() { - return Stream.of(ConcatenateCollectionToStringScenario.values()) - .map(Collections::singletonList) - .collect(Collectors.toList()); - } - - @UseDataProvider("concatenateCollectionToStringScenarioDataProvider") - @Test - public void concatenateCollectionToString_works_as_expected(ConcatenateCollectionToStringScenario scenario) { - // when - String result = listener.concatenateCollectionToString(scenario.collection); - - // then - assertThat(result).isEqualTo(scenario.expectedResult); - } } \ No newline at end of file diff --git a/backstopper-spring-web-mvc/README.md b/backstopper-spring-web-mvc/README.md index 0db37b5..676036e 100644 --- a/backstopper-spring-web-mvc/README.md +++ b/backstopper-spring-web-mvc/README.md @@ -4,7 +4,7 @@ Backstopper is a framework-agnostic API error handling and (optional) model vali (NOTE: The [Backstopper 1.x branch](https://github.com/Nike-Inc/backstopper/tree/v1.x) contains a version of Backstopper for Java 7+, and for the `javax` ecosystem. The current Backstopper supports Java 17+ and the `jakarta` -ecosystem. It also contains support for Spring 4 and 5, and Springboot 1 and 2.) +ecosystem. The Backstopper 1.x releases also contain support for Spring 4 and 5, and Springboot 1 and 2.) This readme focuses specifically on the Backstopper Spring Web MVC integration. If you are looking for a different framework integration check out the [relevant section](../README.md#framework_modules) of the base readme to see if one diff --git a/backstopper-spring-web/README.md b/backstopper-spring-web/README.md index 5006e96..baa0704 100644 --- a/backstopper-spring-web/README.md +++ b/backstopper-spring-web/README.md @@ -4,7 +4,7 @@ Backstopper is a framework-agnostic API error handling and (optional) model vali (NOTE: The [Backstopper 1.x branch](https://github.com/Nike-Inc/backstopper/tree/v1.x) contains a version of Backstopper for Java 7+, and for the `javax` ecosystem. The current Backstopper supports Java 17+ and the `jakarta` -ecosystem. It also contains support for Spring 4 and 5, and Springboot 1 and 2.) +ecosystem. The Backstopper 1.x releases also contain support for Spring 4 and 5, and Springboot 1 and 2.) This `backstopper-spring-web` module is not meant to be used standalone. It is here to provide common code for any `spring-web*` based application, including both Spring Web MVC and Spring WebFlux applications. But this module diff --git a/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListener.java b/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListener.java index 3650ea5..9797a3c 100644 --- a/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListener.java +++ b/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListener.java @@ -1,6 +1,7 @@ package com.nike.backstopper.handler.spring.listener.impl; import com.nike.backstopper.apierror.ApiError; +import com.nike.backstopper.apierror.ApiErrorBase; import com.nike.backstopper.apierror.ApiErrorWithMetadata; import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; import com.nike.backstopper.handler.ApiExceptionHandlerUtils; @@ -9,21 +10,37 @@ import com.nike.internal.util.Pair; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.springframework.beans.ConversionNotSupportedException; import org.springframework.beans.TypeMismatchException; +import org.springframework.core.MethodParameter; +import org.springframework.core.codec.DecodingException; import org.springframework.http.converter.HttpMessageConversionException; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.method.annotation.MethodArgumentConversionNotSupportedException; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.server.MethodNotAllowedException; +import org.springframework.web.server.MissingRequestValueException; +import org.springframework.web.server.NotAcceptableStatusException; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.web.server.ServerErrorException; +import org.springframework.web.server.ServerWebInputException; +import org.springframework.web.server.UnsupportedMediaTypeStatusException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; import static com.nike.backstopper.apierror.SortedApiErrorSet.singletonSortedSetOf; import static java.util.Collections.singleton; @@ -128,6 +145,10 @@ public ApiExceptionHandlerListenerResult shouldHandleException(Throwable ex) { // Not a Spring MVC or WebFlux specific exception. See if it's an exception common to both. List> extraDetailsForLogging = new ArrayList<>(); + if (ex instanceof ResponseStatusException) { + return handleResponseStatusException((ResponseStatusException)ex); + } + String exClassname = ex.getClass().getName(); if (isA404NotFoundExceptionClassname(exClassname)) { @@ -135,7 +156,7 @@ public ApiExceptionHandlerListenerResult shouldHandleException(Throwable ex) { } if (ex instanceof TypeMismatchException) { - return handleTypeMismatchException((TypeMismatchException)ex, extraDetailsForLogging, true); + return handleTypeMismatchException((TypeMismatchException)ex, extraDetailsForLogging, true, null); } if (ex instanceof HttpMessageConversionException) { @@ -208,7 +229,6 @@ protected boolean isMissingExpectedContentCase(HttpMessageConversionException ex // Underlying Jackson cases. Unfortunately there's a lot of manual digging that we have to do to determine // that we've reached these cases. Throwable cause = ex.getCause(); - //noinspection RedundantIfStatement if (cause != null) { String causeClassName = cause.getClass().getName(); if ("com.fasterxml.jackson.databind.exc.InvalidFormatException".equals(causeClassName) @@ -217,6 +237,7 @@ && nullSafeStringContains(cause.getMessage(), "Cannot coerce empty String") return true; } + //noinspection RedundantIfStatement if ("com.fasterxml.jackson.databind.JsonMappingException".equals(causeClassName) && nullSafeStringContains(cause.getMessage(), "No content to map due to end-of-input") ) { @@ -239,10 +260,18 @@ protected boolean nullSafeStringContains(String strToCheck, String snippet) { protected ApiExceptionHandlerListenerResult handleTypeMismatchException( TypeMismatchException ex, List> extraDetailsForLogging, - boolean addBaseExceptionMessageToLoggingDetails + boolean addBaseExceptionMessageToLoggingDetails, + @Nullable List> extraMetadata ) { // The metadata will only be used if it's a 400 error. Map metadata = new LinkedHashMap<>(); + if (extraMetadata != null) { + for (Pair pair : extraMetadata) { + if (pair != null) { + metadata.put(pair.getKey(), pair.getValue()); + } + } + } if (addBaseExceptionMessageToLoggingDetails) { utils.addBaseExceptionMessageToExtraDetailsForLogging(ex, extraDetailsForLogging); @@ -253,7 +282,7 @@ protected ApiExceptionHandlerListenerResult handleTypeMismatchException( badPropName = ex.getPropertyName(); } String badPropValue = (ex.getValue() == null) ? null : String.valueOf(ex.getValue()); - String requiredTypeNoInfoLeak = extractRequiredTypeNoInfoLeak(ex); + String requiredTypeNoInfoLeak = extractRequiredTypeNoInfoLeak(ex.getRequiredType()); extraDetailsForLogging.add(Pair.of("bad_property_name", badPropName)); if (badPropName != null) { @@ -310,52 +339,51 @@ protected String extractPropertyName(TypeMismatchException tme) { return null; } - protected String extractRequiredTypeNoInfoLeak(TypeMismatchException tme) { - if (tme.getRequiredType() == null) { + protected String extractRequiredTypeNoInfoLeak(Class type) { + if (type == null) { return null; } - if (isRequiredTypeAssignableToOneOf(tme, Byte.class, byte.class)) { + if (isRequiredTypeAssignableToOneOf(type, Byte.class, byte.class)) { return "byte"; } - if (isRequiredTypeAssignableToOneOf(tme, Short.class, short.class)) { + if (isRequiredTypeAssignableToOneOf(type, Short.class, short.class)) { return "short"; } - if (isRequiredTypeAssignableToOneOf(tme, Integer.class, int.class)) { + if (isRequiredTypeAssignableToOneOf(type, Integer.class, int.class)) { return "int"; } - if (isRequiredTypeAssignableToOneOf(tme, Long.class, long.class)) { + if (isRequiredTypeAssignableToOneOf(type, Long.class, long.class)) { return "long"; } - if (isRequiredTypeAssignableToOneOf(tme, Float.class, float.class)) { + if (isRequiredTypeAssignableToOneOf(type, Float.class, float.class)) { return "float"; } - if (isRequiredTypeAssignableToOneOf(tme, Double.class, double.class)) { + if (isRequiredTypeAssignableToOneOf(type, Double.class, double.class)) { return "double"; } - if (isRequiredTypeAssignableToOneOf(tme, Boolean.class, boolean.class)) { + if (isRequiredTypeAssignableToOneOf(type, Boolean.class, boolean.class)) { return "boolean"; } - if (isRequiredTypeAssignableToOneOf(tme, Character.class, char.class)) { + if (isRequiredTypeAssignableToOneOf(type, Character.class, char.class)) { return "char"; } - if (isRequiredTypeAssignableToOneOf(tme, CharSequence.class)) { + if (isRequiredTypeAssignableToOneOf(type, CharSequence.class)) { return "string"; } return "[complex type]"; } - protected boolean isRequiredTypeAssignableToOneOf(TypeMismatchException tme, Class... allowedClasses) { - Class desiredClass = tme.getRequiredType(); + protected boolean isRequiredTypeAssignableToOneOf(Class desiredClass, Class... allowedClasses) { for (Class allowedClass : allowedClasses) { if (allowedClass.isAssignableFrom(desiredClass)) { return true; @@ -380,4 +408,233 @@ protected boolean isA401UnauthorizedExceptionClassname(String exClassname) { protected boolean isA503TemporaryProblemExceptionClassname(String exClassname) { return DEFAULT_TO_503_CLASSNAMES.contains(exClassname); } + + protected @NotNull ApiExceptionHandlerListenerResult handleResponseStatusException( + @NotNull ResponseStatusException ex + ) { + int statusCode = ex.getStatusCode().value(); + List> extraDetailsForLogging = new ArrayList<>(); + utils.addBaseExceptionMessageToExtraDetailsForLogging(ex, extraDetailsForLogging); + addExtraDetailsForLoggingForResponseStatusException(ex, extraDetailsForLogging); + + // TODO: Handle HandlerMethodValidationException, which was introduced in spring 6.1, + // which means we'd have to do nasty reflection to deal with it. + + // Search for a more specific way to handle this based on the cause. + Throwable exCause = ex.getCause(); + if (exCause instanceof TypeMismatchException) { + // If the cause is a TypeMismatchException and status code is acceptable, then we can have the + // handleTypeMismatchException(...) method deal with it for a more specific response. + + // For safety make sure the status code is one we expect. + TypeMismatchException tmeCause = (TypeMismatchException) ex.getCause(); + int expectedStatusCode = (tmeCause instanceof ConversionNotSupportedException) ? 500 : 400; + if (statusCode == expectedStatusCode) { + // The specific cause exception type and the status code match, + // so we can use handleTypeMismatchException(...). + return handleTypeMismatchException( + tmeCause, + extraDetailsForLogging, + false, + extractExtraMetadataForServerWebInputException(ex) + ); + } + } + else if (exCause instanceof DecodingException && statusCode == 400) { + if (Objects.equals(ex.getReason(), "No request body") || exCause.getMessage().startsWith("No request body for:")) { + return handleError(projectApiErrors.getMissingExpectedContentApiError(), extraDetailsForLogging); + } + + return handleError(projectApiErrors.getMalformedRequestApiError(), extraDetailsForLogging); + } + + // Exception cause didn't help. Try parsing the reason message. + String exReason = (ex.getReason() == null) ? "" : ex.getReason(); + String[] exReasonWords = exReason.split(" "); + + RequiredParamData missingRequiredParam = parseExReasonForMissingRequiredParam(ex, exReasonWords, exReason); + if (missingRequiredParam != null && statusCode == 400) { + return handleError( + new ApiErrorWithMetadata( + projectApiErrors.getMalformedRequestApiError(), + missingRequiredParam.getAsApiErrorMetadata() + ), + extraDetailsForLogging + ); + } + else if (exReason.startsWith("Request body is missing") && statusCode == 400) { + return handleError(projectApiErrors.getMissingExpectedContentApiError(), extraDetailsForLogging); + } + + // For any other ResponseStatusException we'll search for an appropriate ApiError by status code. + return handleError( + determineApiErrorToUseForGenericResponseStatusCode(statusCode), + extraDetailsForLogging + ); + } + + protected @Nullable List> extractExtraMetadataForServerWebInputException(Exception maybeSWIEx) { + if (!(maybeSWIEx instanceof ServerWebInputException swiEx)) { + return null; + } + + MethodParameter methodParam = swiEx.getMethodParameter(); + if (methodParam == null) { + return null; + } + + boolean isHeader = methodParam.hasParameterAnnotation(RequestHeader.class); + boolean isQueryParam = methodParam.hasParameterAnnotation(RequestParam.class); + + if (isHeader && isQueryParam) { + return Collections.singletonList(Pair.of("required_location", "header,query_param")); + } + else if (isHeader) { + return Collections.singletonList(Pair.of("required_location", "header")); + } + else if (isQueryParam) { + return Collections.singletonList(Pair.of("required_location", "query_param")); + } + + return null; + } + + protected void addExtraDetailsForLoggingForResponseStatusException( + @NotNull ResponseStatusException ex, + @NotNull List> extraDetailsForLogging + ) { + if (ex instanceof MethodNotAllowedException) { + MethodNotAllowedException detailsEx = (MethodNotAllowedException)ex; + extraDetailsForLogging.add( + Pair.of("supported_methods", concatenateCollectionToString(detailsEx.getSupportedMethods())) + ); + } + + if (ex instanceof NotAcceptableStatusException) { + NotAcceptableStatusException detailsEx = (NotAcceptableStatusException)ex; + extraDetailsForLogging.add( + Pair.of("supported_media_types", concatenateCollectionToString(detailsEx.getSupportedMediaTypes())) + ); + } + + if (ex instanceof ServerErrorException) { + ServerErrorException detailsEx = (ServerErrorException)ex; + extraDetailsForLogging.add( + Pair.of("method_parameter", String.valueOf(detailsEx.getMethodParameter())) + ); + extraDetailsForLogging.add( + Pair.of("handler_method", String.valueOf(detailsEx.getHandlerMethod())) + ); + } + + if (ex instanceof ServerWebInputException) { + ServerWebInputException detailsEx = (ServerWebInputException)ex; + extraDetailsForLogging.add( + Pair.of("method_parameter", String.valueOf(detailsEx.getMethodParameter())) + ); + } + + if (ex instanceof UnsupportedMediaTypeStatusException) { + UnsupportedMediaTypeStatusException detailsEx = (UnsupportedMediaTypeStatusException)ex; + extraDetailsForLogging.add( + Pair.of("supported_media_types", concatenateCollectionToString(detailsEx.getSupportedMediaTypes())) + ); + extraDetailsForLogging.add(Pair.of("java_body_type", String.valueOf(detailsEx.getBodyType()))); + } + } + + protected @NotNull String concatenateCollectionToString(@Nullable Collection collection) { + if (collection == null) { + return ""; + } + return collection.stream().map(Object::toString).collect(Collectors.joining(",")); + } + + protected @NotNull ApiError determineApiErrorToUseForGenericResponseStatusCode(int statusCode) { + return switch (statusCode) { + case 400 -> projectApiErrors.getGenericBadRequestApiError(); + case 401 -> projectApiErrors.getUnauthorizedApiError(); + case 403 -> projectApiErrors.getForbiddenApiError(); + case 404 -> projectApiErrors.getNotFoundApiError(); + case 405 -> projectApiErrors.getMethodNotAllowedApiError(); + case 406 -> projectApiErrors.getNoAcceptableRepresentationApiError(); + case 415 -> projectApiErrors.getUnsupportedMediaTypeApiError(); + case 429 -> projectApiErrors.getTooManyRequestsApiError(); + case 500 -> projectApiErrors.getGenericServiceError(); + case 503 -> projectApiErrors.getTemporaryServiceProblemApiError(); + default -> + // If we reach here then it wasn't a status code where we have a common ApiError in ProjectApiErrors. + // Generate a generic ApiError to cover it. + generateGenericApiErrorForResponseStatusCode(statusCode); + }; + } + + protected @NotNull ApiError generateGenericApiErrorForResponseStatusCode(int statusCode) { + // Reuse the error code for the generic bad request ApiError, unless the status code is greater than or equal + // to 500. If status code >= 500, then use the generic service error status code instead. + String errorCodeToUse = projectApiErrors.getGenericBadRequestApiError().getErrorCode(); + if (statusCode >= 500) { + errorCodeToUse = projectApiErrors.getGenericServiceError().getErrorCode(); + } + + return new ApiErrorBase( + "GENERIC_API_ERROR_FOR_RESPONSE_STATUS_CODE_" + statusCode, + errorCodeToUse, + "An error occurred that resulted in response status code " + statusCode, + statusCode + ); + } + + protected @Nullable RequiredParamData parseExReasonForMissingRequiredParam( + @NotNull ResponseStatusException ex, @NotNull String[] exReasonWords, @NotNull String exReason + ) { + // Check for an exception type where we can get the info without parsing strings. + if (ex instanceof MissingRequestValueException) { + MissingRequestValueException detailsEx = (MissingRequestValueException)ex; + return new RequiredParamData( + detailsEx.getName(), + extractRequiredTypeNoInfoLeak(detailsEx.getType()), + extractExtraMetadataForServerWebInputException(ex) + ); + } + + if (exReasonWords.length != 7) { + return null; + } + + if ("Required".equals(exReasonWords[0]) + && "parameter".equals(exReasonWords[2]) + && (exReason.endsWith("is not present.") || exReason.endsWith("is not present")) + ) { + return new RequiredParamData(exReasonWords[3].replace("'", ""), exReasonWords[1], null); + } + + return null; + } + + protected static class RequiredParamData { + public final String paramName; + public final String paramType; + public final List> extraMetadata; + + public RequiredParamData(String paramName, String paramType, List> extraMetadata) { + this.paramName = paramName; + this.paramType = paramType; + this.extraMetadata = extraMetadata; + } + + public Map getAsApiErrorMetadata() { + Map metadata = new LinkedHashMap<>(); + if (extraMetadata != null) { + for (Pair pair : extraMetadata) { + if (pair != null) { + metadata.put(pair.getKey(), pair.getValue()); + } + } + } + metadata.put("missing_param_name", paramName); + metadata.put("missing_param_type", paramType); + return metadata; + } + } } diff --git a/backstopper-spring-web/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListenerTest.java b/backstopper-spring-web/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListenerTest.java index 6d25a8a..e6d058d 100644 --- a/backstopper-spring-web/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListenerTest.java +++ b/backstopper-spring-web/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListenerTest.java @@ -1,9 +1,11 @@ package com.nike.backstopper.handler.spring.listener.impl; import com.nike.backstopper.apierror.ApiError; +import com.nike.backstopper.apierror.ApiErrorBase; import com.nike.backstopper.apierror.ApiErrorWithMetadata; import com.nike.backstopper.apierror.SortedApiErrorSet; import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; +import com.nike.backstopper.apierror.testutil.BarebonesCoreApiErrorForTesting; import com.nike.backstopper.apierror.testutil.ProjectApiErrorsForTesting; import com.nike.backstopper.exception.ApiException; import com.nike.backstopper.handler.ApiExceptionHandlerUtils; @@ -24,7 +26,12 @@ import org.springframework.beans.ConversionNotSupportedException; import org.springframework.beans.TypeMismatchException; import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.DecodingException; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConversionException; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; @@ -37,14 +44,28 @@ import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.authentication.LockedException; import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.context.request.async.AsyncRequestTimeoutException; import org.springframework.web.method.annotation.MethodArgumentConversionNotSupportedException; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.server.MethodNotAllowedException; +import org.springframework.web.server.MissingRequestValueException; +import org.springframework.web.server.NotAcceptableStatusException; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.web.server.ServerErrorException; +import org.springframework.web.server.ServerWebInputException; +import org.springframework.web.server.UnsupportedMediaTypeStatusException; import org.springframework.web.servlet.NoHandlerFoundException; +import java.beans.PropertyChangeEvent; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -53,6 +74,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import static java.util.Collections.singleton; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.doReturn; @@ -233,7 +255,7 @@ public void handleTypeMismatchException_adds_exception_message_logging_detail_de // when ApiExceptionHandlerListenerResult result = listener.handleTypeMismatchException( - new TypeMismatchException("doesNotMatter", Integer.class), extraDetailsForLogging, shouldAddExceptionMsg + new TypeMismatchException("doesNotMatter", Integer.class), extraDetailsForLogging, shouldAddExceptionMsg, null ); // then @@ -275,7 +297,7 @@ public void handleTypeMismatchException_adds_metadata_to_resulting_ApiError_as_e // when ApiExceptionHandlerListenerResult result = listener.handleTypeMismatchException( - exMock, new ArrayList<>(), true + exMock, new ArrayList<>(), true, null ); // then @@ -289,6 +311,44 @@ public void handleTypeMismatchException_adds_metadata_to_resulting_ApiError_as_e assertThat(requiredTypeMetadata).isEqualTo(expectedRequiredType); } + @Test + public void handleTypeMismatchException_adds_extra_metadata_to_resulting_ApiError_if_specified() { + // given + TypeMismatchException exMock = mock(TypeMismatchException.class); + String propName = UUID.randomUUID().toString(); + String propValue = UUID.randomUUID().toString(); + Class requiredType = Integer.class; + String expectedRequiredType = "int"; + + doReturn(propName).when(exMock).getPropertyName(); + doReturn(propValue).when(exMock).getValue(); + doReturn(requiredType).when(exMock).getRequiredType(); + + List> extraMetadata = Arrays.asList( + Pair.of("foo_extra_metadata", UUID.randomUUID().toString()), + Pair.of("bar_extra_metadata", UUID.randomUUID().toString()) + ); + + // when + ApiExceptionHandlerListenerResult result = listener.handleTypeMismatchException( + exMock, new ArrayList<>(), true, extraMetadata + ); + + // then + assertThat(result.errors).hasSize(1); + ApiError apiError = result.errors.iterator().next(); + Object propNameMetadata = apiError.getMetadata().get("bad_property_name"); + Object propValueMetadata = apiError.getMetadata().get("bad_property_value"); + Object requiredTypeMetadata = apiError.getMetadata().get("required_type"); + Object fooExtraMetadata = apiError.getMetadata().get("foo_extra_metadata"); + Object barExtraMetadata = apiError.getMetadata().get("bar_extra_metadata"); + assertThat(propNameMetadata).isEqualTo(propName); + assertThat(propValueMetadata).isEqualTo(propValue); + assertThat(requiredTypeMetadata).isEqualTo(expectedRequiredType); + assertThat(fooExtraMetadata).isEqualTo(extraMetadata.get(0).getRight()); + assertThat(barExtraMetadata).isEqualTo(extraMetadata.get(1).getRight()); + } + @DataProvider(value = { "true", "false" @@ -517,12 +577,8 @@ public static Object[][] dataProviderForExtractRequiredTypeNoInfoLeak() { @UseDataProvider("dataProviderForExtractRequiredTypeNoInfoLeak") @Test public void extractRequiredTypeNoInfoLeak_works_as_expected(Class requiredType, String expectedResult) { - // given - TypeMismatchException typeMismatchExceptionMock = mock(TypeMismatchException.class); - doReturn(requiredType).when(typeMismatchExceptionMock).getRequiredType(); - // when - String result = listener.extractRequiredTypeNoInfoLeak(typeMismatchExceptionMock); + String result = listener.extractRequiredTypeNoInfoLeak(requiredType); // then assertThat(result).isEqualTo(expectedResult); @@ -585,4 +641,690 @@ public void nullSafeStringContains_works_as_expected( // then assertThat(result).isEqualTo(expectedResult); } + + private void validateResponse( + ApiExceptionHandlerListenerResult result, + boolean expectedShouldHandle, + Collection expectedErrors, + Pair ... expectedExtraDetailsForLogging + ) { + List> loggingDetailsList = (expectedExtraDetailsForLogging == null) + ? Collections.emptyList() + : Arrays.asList(expectedExtraDetailsForLogging); + validateResponse( + result, expectedShouldHandle, expectedErrors, loggingDetailsList + ); + } + + private void validateResponse( + ApiExceptionHandlerListenerResult result, + boolean expectedShouldHandle, + Collection expectedErrors, + List> expectedExtraDetailsForLogging + ) { + if (!expectedShouldHandle) { + assertThat(result.shouldHandleResponse).isFalse(); + return; + } + + assertThat(result.errors).containsExactlyInAnyOrderElementsOf(expectedErrors); + assertThat(result.extraDetailsForLogging).containsExactlyInAnyOrderElementsOf(expectedExtraDetailsForLogging); + } + + private enum TypeMismatchExceptionScenario { + CONVERSION_NOT_SUPPORTED_500( + HttpStatus.resolve(500), + new ConversionNotSupportedException( + new PropertyChangeEvent("doesNotMatter", "somePropertyName", "oldValue", "newValue"), + Integer.class, + null + ), + testProjectApiErrors.getGenericServiceError(), + Arrays.asList( + Pair.of("bad_property_name", "somePropertyName"), + Pair.of("bad_property_value", "newValue"), + Pair.of("required_type", Integer.class.toString()) + ) + ), + GENERIC_TYPE_MISMATCH_EXCEPTION_400( + HttpStatus.resolve(400), + new TypeMismatchException( + new PropertyChangeEvent("doesNotMatter", "somePropertyName", "oldValue", "newValue"), + Integer.class + ), + new ApiErrorWithMetadata( + testProjectApiErrors.getTypeConversionApiError(), + Pair.of("bad_property_name", "somePropertyName"), + Pair.of("bad_property_value", "newValue"), + Pair.of("required_type", "int") + ), + Arrays.asList( + Pair.of("bad_property_name", "somePropertyName"), + Pair.of("bad_property_value", "newValue"), + Pair.of("required_type", Integer.class.toString()) + ) + ), + UNEXPECTED_4XX_STATUS_CODE( + HttpStatus.resolve(403), + new TypeMismatchException("doesNotMatter", Integer.class), + testProjectApiErrors.getForbiddenApiError(), + Collections.emptyList() + ), + UNEXPECTED_5XX_STATUS_CODE( + HttpStatus.resolve(503), + new TypeMismatchException("doesNotMatter", Integer.class), + testProjectApiErrors.getTemporaryServiceProblemApiError(), + Collections.emptyList() + ), + UNKNOWN_4XX_STATUS_CODE( + HttpStatus.resolve(418), + new TypeMismatchException("doesNotMatter", Integer.class), + new ApiErrorBase( + "GENERIC_API_ERROR_FOR_RESPONSE_STATUS_CODE_418", + testProjectApiErrors.getGenericBadRequestApiError().getErrorCode(), + "An error occurred that resulted in response status code 418", + 418 + ), + Collections.emptyList() + ), + UNKNOWN_5XX_STATUS_CODE( + HttpStatus.resolve(509), + new TypeMismatchException("doesNotMatter", Integer.class), + new ApiErrorBase( + "GENERIC_API_ERROR_FOR_RESPONSE_STATUS_CODE_509", + testProjectApiErrors.getGenericServiceError().getErrorCode(), + "An error occurred that resulted in response status code 509", + 509 + ), + Collections.emptyList() + ); + + public final HttpStatus status; + public final TypeMismatchException tmeCause; + public final ApiError expectedApiError; + public final List> expectedExtraDetailsForLogging; + + TypeMismatchExceptionScenario( + HttpStatus status, TypeMismatchException tmeCause, ApiError expectedApiError, + List> expectedExtraDetailsForLogging + ) { + this.status = status; + this.tmeCause = tmeCause; + this.expectedApiError = expectedApiError; + this.expectedExtraDetailsForLogging = expectedExtraDetailsForLogging; + } + } + + @DataProvider + public static List> typeMismatchExceptionScenarioDataProvider() { + return Stream.of(TypeMismatchExceptionScenario.values()) + .map(Collections::singletonList) + .collect(Collectors.toList()); + } + + @UseDataProvider("typeMismatchExceptionScenarioDataProvider") + @Test + public void shouldHandleException_handles_ResponseStatusException_with_TypeMismatchException_cause_as_expected( + TypeMismatchExceptionScenario scenario + ) { + // given + ResponseStatusException ex = new ResponseStatusException( + scenario.status, "Some ResponseStatusException reason", scenario.tmeCause + ); + List> expectedExtraDetailsForLogging = new ArrayList<>(); + ApiExceptionHandlerUtils.DEFAULT_IMPL.addBaseExceptionMessageToExtraDetailsForLogging( + ex, expectedExtraDetailsForLogging + ); + expectedExtraDetailsForLogging.addAll(scenario.expectedExtraDetailsForLogging); + + // when + ApiExceptionHandlerListenerResult result = listener.shouldHandleException(ex); + + // then + validateResponse( + result, + true, + singleton(scenario.expectedApiError), + expectedExtraDetailsForLogging + ); + } + + @DataProvider(value = { + "400 | MALFORMED_REQUEST", + "401 | UNAUTHORIZED" + }, splitBy = "\\|") + @Test + public void shouldHandleException_returns_MALFORMED_REQUEST_for_ResponseStatusException_with_DecodingException_cause_only_if_status_is_400( + int statusCode, BarebonesCoreApiErrorForTesting expectedError + ) { + // given + ResponseStatusException ex = new ResponseStatusException( + HttpStatus.resolve(statusCode), + "Some ResponseStatusException reason", + new DecodingException("Some decoding ex") + ); + List> expectedExtraDetailsForLogging = new ArrayList<>(); + ApiExceptionHandlerUtils.DEFAULT_IMPL.addBaseExceptionMessageToExtraDetailsForLogging( + ex, expectedExtraDetailsForLogging + ); + + // when + ApiExceptionHandlerListenerResult result = listener.shouldHandleException(ex); + + // then + validateResponse( + result, + true, + singleton(expectedError), + expectedExtraDetailsForLogging + ); + } + + @DataProvider(value = { + "No request body | ", + " | No request body for: blah" + }, splitBy = "\\|") + @Test + public void shouldHandleException_returns_MISSING_EXPECTED_CONTENT_for_ResponseStatusException_with_DecodingException_cause_with_magic_messages( + String exReason, String decodingExMessage + ) { + // given + ResponseStatusException ex = new ResponseStatusException( + HttpStatus.valueOf(400), + exReason, + new DecodingException(decodingExMessage) + ); + List> expectedExtraDetailsForLogging = new ArrayList<>(); + ApiExceptionHandlerUtils.DEFAULT_IMPL.addBaseExceptionMessageToExtraDetailsForLogging( + ex, expectedExtraDetailsForLogging + ); + + // when + ApiExceptionHandlerListenerResult result = listener.shouldHandleException(ex); + + // then + validateResponse( + result, + true, + singleton(BarebonesCoreApiErrorForTesting.MISSING_EXPECTED_CONTENT), + expectedExtraDetailsForLogging + ); + } + + @DataProvider(value = { + "400 | Required foo parameter 'bar' is not present | foo | bar | MALFORMED_REQUEST", + "401 | Required foo parameter 'bar' is not present | null | null | UNAUTHORIZED", + "400 | Required parameter 'bar' is not present | null | null | GENERIC_BAD_REQUEST", + "400 | Required foo parameter is not present | null | null | GENERIC_BAD_REQUEST", + "400 | Blah foo parameter 'bar' is not present | null | null | GENERIC_BAD_REQUEST", + "400 | Required foo blah 'bar' is not present | null | null | GENERIC_BAD_REQUEST", + "400 | Required foo parameter 'bar' is not blah | null | null | GENERIC_BAD_REQUEST", + "400 | Some random reason | null | null | GENERIC_BAD_REQUEST", + }, splitBy = "\\|") + @Test + public void shouldHandleException_returns_MALFORMED_REQUEST_for_ResponseStatusException_with_special_required_param_reason_string( + int statusCode, + String exReasonString, + String expectedMissingParamType, + String expectedMissingParamName, + BarebonesCoreApiErrorForTesting expectedBaseError + ) { + // given + ResponseStatusException ex = new ResponseStatusException(HttpStatus.resolve(statusCode), exReasonString); + List> expectedExtraDetailsForLogging = new ArrayList<>(); + ApiExceptionHandlerUtils.DEFAULT_IMPL.addBaseExceptionMessageToExtraDetailsForLogging( + ex, expectedExtraDetailsForLogging + ); + + ApiError expectedError = expectedBaseError; + if (expectedMissingParamName != null && expectedMissingParamType != null) { + expectedError = new ApiErrorWithMetadata( + expectedBaseError, + Pair.of("missing_param_name", expectedMissingParamName), + Pair.of("missing_param_type", expectedMissingParamType) + ); + } + + // when + ApiExceptionHandlerListenerResult result = listener.shouldHandleException(ex); + + // then + validateResponse( + result, + true, + singleton(expectedError), + expectedExtraDetailsForLogging + ); + } + + @DataProvider(value = { + "400 | Request body is missing | MISSING_EXPECTED_CONTENT", + "400 | Request body is missing blahblah | MISSING_EXPECTED_CONTENT", + "401 | Request body is missing | UNAUTHORIZED", + "400 | Request body is | GENERIC_BAD_REQUEST", + "400 | Some random reason | GENERIC_BAD_REQUEST", + }, splitBy = "\\|") + @Test + public void shouldHandleException_returns_MISSING_EXPECTED_CONTENT_for_ResponseStatusException_with_special_reason_string_beginning( + int statusCode, String exReasonString, BarebonesCoreApiErrorForTesting expectedError + ) { + // given + ResponseStatusException ex = new ResponseStatusException(HttpStatus.resolve(statusCode), exReasonString); + List> expectedExtraDetailsForLogging = new ArrayList<>(); + ApiExceptionHandlerUtils.DEFAULT_IMPL.addBaseExceptionMessageToExtraDetailsForLogging( + ex, expectedExtraDetailsForLogging + ); + + // when + ApiExceptionHandlerListenerResult result = listener.shouldHandleException(ex); + + // then + validateResponse( + result, + true, + singleton(expectedError), + expectedExtraDetailsForLogging + ); + } + + @DataProvider(value = { + "400 | GENERIC_BAD_REQUEST", + "401 | UNAUTHORIZED", + "403 | FORBIDDEN", + "404 | NOT_FOUND", + "405 | METHOD_NOT_ALLOWED", + "406 | NO_ACCEPTABLE_REPRESENTATION", + "415 | UNSUPPORTED_MEDIA_TYPE", + "429 | TOO_MANY_REQUESTS", + "500 | GENERIC_SERVICE_ERROR", + "503 | TEMPORARY_SERVICE_PROBLEM", + }, splitBy = "\\|") + @Test + public void shouldHandleException_handles_generic_ResponseStatusException_by_returning_ApiError_from_project_if_status_code_is_known( + int desiredStatusCode, BarebonesCoreApiErrorForTesting expectedError + ) { + // given + ResponseStatusException ex = new ResponseStatusException( + HttpStatus.resolve(desiredStatusCode), "Some ResponseStatusException reason" + ); + List> expectedExtraDetailsForLogging = new ArrayList<>(); + ApiExceptionHandlerUtils.DEFAULT_IMPL.addBaseExceptionMessageToExtraDetailsForLogging( + ex, expectedExtraDetailsForLogging + ); + + // when + ApiExceptionHandlerListenerResult result = listener.shouldHandleException(ex); + + // then + validateResponse( + result, + true, + singleton(expectedError), + expectedExtraDetailsForLogging + ); + } + + @DataProvider(value = { + "418", + "509" + }) + @Test + public void shouldHandleException_handles_generic_ResponseStatusException_by_returning_synthetic_ApiError_if_status_code_is_unknown( + int desiredStatusCode + ) { + // given + ResponseStatusException ex = new ResponseStatusException( + HttpStatus.resolve(desiredStatusCode), "Some ResponseStatusException reason" + ); + List> expectedExtraDetailsForLogging = new ArrayList<>(); + ApiExceptionHandlerUtils.DEFAULT_IMPL.addBaseExceptionMessageToExtraDetailsForLogging( + ex, expectedExtraDetailsForLogging + ); + + String expectedErrorCode = (desiredStatusCode >= 500) + ? testProjectApiErrors.getGenericServiceError().getErrorCode() + : testProjectApiErrors.getGenericBadRequestApiError().getErrorCode(); + + ApiError expectedError = new ApiErrorBase( + "GENERIC_API_ERROR_FOR_RESPONSE_STATUS_CODE_" + desiredStatusCode, + expectedErrorCode, + "An error occurred that resulted in response status code " + desiredStatusCode, + desiredStatusCode + ); + + // when + ApiExceptionHandlerListenerResult result = listener.shouldHandleException(ex); + + // then + validateResponse( + result, + true, + singleton(expectedError), + expectedExtraDetailsForLogging + ); + } + + @DataProvider(value = { + "true", + "false" + }) + @Test + public void shouldHandleException_handles_MethodNotAllowedException_as_expected( + boolean supportedMethodsIsEmpty + ) { + // given + String actualMethod = UUID.randomUUID().toString(); + List supportedMethods = + (supportedMethodsIsEmpty) + ? Collections.emptyList() + : Arrays.asList( + HttpMethod.GET, + HttpMethod.POST + ); + + MethodNotAllowedException ex = new MethodNotAllowedException(actualMethod, supportedMethods); + + List> expectedExtraDetailsForLogging = new ArrayList<>(); + ApiExceptionHandlerUtils.DEFAULT_IMPL.addBaseExceptionMessageToExtraDetailsForLogging( + ex, expectedExtraDetailsForLogging + ); + + // when + ApiExceptionHandlerListenerResult result = listener.shouldHandleException(ex); + + // then + // They throw the supported methods into a plain HashSet so we can't rely on the ordering. + // Verify it another way. + Optional supportedMethodsLoggingDetailsValue = result.extraDetailsForLogging + .stream() + .filter(p -> p.getKey().equals("supported_methods")) + .map(Pair::getValue) + .findAny(); + assertThat(supportedMethodsLoggingDetailsValue).isPresent(); + List actualLoggingDetailsMethods = supportedMethodsLoggingDetailsValue + .map(s -> { + if (s.equals("")) { + return Collections.emptyList(); + } + return Arrays.stream(s.split(",")).map(HttpMethod::valueOf).collect(Collectors.toList()); + }) + .orElse(Collections.emptyList()); + + assertThat(actualLoggingDetailsMethods).containsExactlyInAnyOrderElementsOf(supportedMethods); + + expectedExtraDetailsForLogging.add(Pair.of("supported_methods", supportedMethodsLoggingDetailsValue.get())); + + validateResponse( + result, + true, + singleton(testProjectApiErrors.getMethodNotAllowedApiError()), + expectedExtraDetailsForLogging + ); + } + + @DataProvider(value = { + "true", + "false" + }) + @Test + public void shouldHandleException_handles_NotAcceptableStatusException_as_expected( + boolean includesSupportedMediaTypes + ) { + // given + List supportedMediaTypes = Arrays.asList( + MediaType.APPLICATION_JSON, + MediaType.IMAGE_JPEG + ); + NotAcceptableStatusException ex = + (includesSupportedMediaTypes) + ? new NotAcceptableStatusException(supportedMediaTypes) + : new NotAcceptableStatusException("Some reason"); + + List> expectedExtraDetailsForLogging = new ArrayList<>(); + ApiExceptionHandlerUtils.DEFAULT_IMPL.addBaseExceptionMessageToExtraDetailsForLogging( + ex, expectedExtraDetailsForLogging + ); + + String expectedSupportedMediaTypesValueStr = + (includesSupportedMediaTypes) + ? supportedMediaTypes.stream().map(Object::toString).collect(Collectors.joining(",")) + : ""; + + expectedExtraDetailsForLogging.add(Pair.of("supported_media_types", expectedSupportedMediaTypesValueStr)); + + // when + ApiExceptionHandlerListenerResult result = listener.shouldHandleException(ex); + + // then + validateResponse( + result, + true, + singleton(testProjectApiErrors.getNoAcceptableRepresentationApiError()), + expectedExtraDetailsForLogging + ); + } + + @DataProvider(value = { + "true", + "false" + }) + @Test + public void shouldHandleException_handles_ServerErrorException_as_expected( + boolean nullDetails + ) throws NoSuchMethodException { + // given + MethodParameter details = new MethodParameter(String.class.getDeclaredMethod("length"), -1); + + ServerErrorException ex = + (nullDetails) + ? new ServerErrorException("Some reason", (Throwable) null) + : new ServerErrorException("Some reason", details, null); + + List> expectedExtraDetailsForLogging = new ArrayList<>(); + ApiExceptionHandlerUtils.DEFAULT_IMPL.addBaseExceptionMessageToExtraDetailsForLogging( + ex, expectedExtraDetailsForLogging + ); + + expectedExtraDetailsForLogging.add( + Pair.of("method_parameter", String.valueOf(ex.getMethodParameter())) + ); + expectedExtraDetailsForLogging.add( + Pair.of("handler_method", String.valueOf(ex.getHandlerMethod())) + ); + + // when + ApiExceptionHandlerListenerResult result = listener.shouldHandleException(ex); + + // then + validateResponse( + result, + true, + singleton(testProjectApiErrors.getGenericServiceError()), + expectedExtraDetailsForLogging + ); + } + + @DataProvider(value = { + "true", + "false" + }) + @Test + public void shouldHandleException_handles_ServerWebInputException_as_expected( + boolean nullDetails + ) throws NoSuchMethodException { + // given + MethodParameter details = new MethodParameter(String.class.getDeclaredMethod("length"), -1); + + ServerWebInputException ex = + (nullDetails) + ? new ServerWebInputException("Some reason") + : new ServerWebInputException("Some reason", details); + + List> expectedExtraDetailsForLogging = new ArrayList<>(); + ApiExceptionHandlerUtils.DEFAULT_IMPL.addBaseExceptionMessageToExtraDetailsForLogging( + ex, expectedExtraDetailsForLogging + ); + + expectedExtraDetailsForLogging.add( + Pair.of("method_parameter", String.valueOf(ex.getMethodParameter())) + ); + + // when + ApiExceptionHandlerListenerResult result = listener.shouldHandleException(ex); + + // then + validateResponse( + result, + true, + singleton(testProjectApiErrors.getGenericBadRequestApiError()), + expectedExtraDetailsForLogging + ); + } + + public void methodWithAnnotatedParams( + @RequestHeader int headerParam, + @RequestParam int queryParam, + @RequestHeader @RequestParam int bothParam, + int unknownParam + ) { + // This method is used as part of shouldHandleException_handles_MissingRequestValueException_as_expected(). + } + + @DataProvider(value = { + "header | 0", + "query_param | 1", + "header,query_param | 2", + "unknown | 3" + }, splitBy = "\\|") + @Test + public void shouldHandleException_handles_MissingRequestValueException_as_expected( + String missingValueType, int paramIndex + ) throws NoSuchMethodException { + // given + Method method = this.getClass() + .getDeclaredMethod("methodWithAnnotatedParams", int.class, int.class, int.class, int.class); + MethodParameter details = new MethodParameter( + method, + paramIndex + ); + + String missingParamName = "some-param-" + UUID.randomUUID().toString(); + MissingRequestValueException ex = new MissingRequestValueException( + missingParamName, + int.class, + "blah not used", + details + ); + + List> expectedExtraDetailsForLogging = new ArrayList<>(); + ApiExceptionHandlerUtils.DEFAULT_IMPL.addBaseExceptionMessageToExtraDetailsForLogging( + ex, expectedExtraDetailsForLogging + ); + + expectedExtraDetailsForLogging.add( + Pair.of("method_parameter", String.valueOf(ex.getMethodParameter())) + ); + + Map expectedMetadata = new LinkedHashMap<>(); + expectedMetadata.put("missing_param_name", missingParamName); + expectedMetadata.put("missing_param_type", "int"); + if (!"unknown".equals(missingValueType)) { + expectedMetadata.put("required_location", missingValueType); + } + + // when + ApiExceptionHandlerListenerResult result = listener.shouldHandleException(ex); + + // then + validateResponse( + result, + true, + singleton(new ApiErrorWithMetadata( + testProjectApiErrors.getMalformedRequestApiError(), + expectedMetadata + )), + expectedExtraDetailsForLogging + ); + } + + @DataProvider(value = { + "true", + "false" + }) + @Test + public void shouldHandleException_handles_UnsupportedMediaTypeStatusException_as_expected( + boolean includeDetails + ) { + // given + MediaType actualMediaType = MediaType.TEXT_PLAIN; + List supportedMediaTypes = Arrays.asList( + MediaType.APPLICATION_JSON, + MediaType.IMAGE_JPEG + ); + ResolvableType javaBodyType = ResolvableType.forClass(Integer.class); + UnsupportedMediaTypeStatusException ex = + (includeDetails) + ? new UnsupportedMediaTypeStatusException(actualMediaType, supportedMediaTypes, javaBodyType) + : new UnsupportedMediaTypeStatusException("Some reason"); + + List> expectedExtraDetailsForLogging = new ArrayList<>(); + ApiExceptionHandlerUtils.DEFAULT_IMPL.addBaseExceptionMessageToExtraDetailsForLogging( + ex, expectedExtraDetailsForLogging + ); + + String expectedSupportedMediaTypesValueStr = + (includeDetails) + ? supportedMediaTypes.stream().map(Object::toString).collect(Collectors.joining(",")) + : ""; + String expectedJavaBodyTypeValueStr = + (includeDetails) + ? javaBodyType.toString() + : "null"; + + expectedExtraDetailsForLogging.add(Pair.of("supported_media_types", expectedSupportedMediaTypesValueStr)); + expectedExtraDetailsForLogging.add(Pair.of("java_body_type", expectedJavaBodyTypeValueStr)); + + // when + ApiExceptionHandlerListenerResult result = listener.shouldHandleException(ex); + + // then + validateResponse( + result, + true, + singleton(testProjectApiErrors.getUnsupportedMediaTypeApiError()), + expectedExtraDetailsForLogging + ); + } + + private enum ConcatenateCollectionToStringScenario { + NULL_COLLECTION(null, ""), + EMPTY_COLLECTION(Collections.emptyList(), ""), + SINGLE_ITEM(Collections.singleton("foo"), "foo"), + MULTIPLE_ITEMS(Arrays.asList("foo", "bar"), "foo,bar"); + + public final Collection collection; + public final String expectedResult; + + ConcatenateCollectionToStringScenario(Collection collection, String expectedResult) { + this.collection = collection; + this.expectedResult = expectedResult; + } + } + + @DataProvider + public static List> concatenateCollectionToStringScenarioDataProvider() { + return Stream.of(ConcatenateCollectionToStringScenario.values()) + .map(Collections::singletonList) + .collect(Collectors.toList()); + } + + @UseDataProvider("concatenateCollectionToStringScenarioDataProvider") + @Test + public void concatenateCollectionToString_works_as_expected(ConcatenateCollectionToStringScenario scenario) { + // when + String result = listener.concatenateCollectionToString(scenario.collection); + + // then + assertThat(result).isEqualTo(scenario.expectedResult); + } } diff --git a/build.gradle b/build.gradle index ffc0945..3793863 100644 --- a/build.gradle +++ b/build.gradle @@ -88,8 +88,7 @@ ext { servletApiVersion = '6.0.0' // Compatible with Jakarta EE 10 spring6Version = '6.0.23' // Compatible with Jakarta EE 9/10 springSecurityVersion = '6.1.9' // Closest spring secrity version to our pinned spring6Version, but without going over to avoid versions being transitively bumped above what we want for testing. - springboot1Version = '1.5.2.RELEASE' - springboot2Version = '2.6.3' + springboot3Version = '3.3.3' jersey1Version = '1.19.2' jersey2Version = '2.23.2' jaxRsVersion = '2.0.1' diff --git a/settings.gradle b/settings.gradle index 1cb6618..71cea30 100644 --- a/settings.gradle +++ b/settings.gradle @@ -9,7 +9,7 @@ include "nike-internal-util", "backstopper-servlet-api", "backstopper-spring-web", "backstopper-spring-web-mvc", -// "backstopper-spring-web-flux", + "backstopper-spring-web-flux", // "backstopper-spring-boot1", // "backstopper-spring-boot2-webmvc", // // Test-only modules (not published) From c8de8cfbf3a36bc14d01592fb10fefce3e990e90 Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Wed, 11 Sep 2024 12:03:38 -0700 Subject: [PATCH 15/42] Fix logic for missing or bad type conversion on headers or query params in spring-web and spring-web-mvc --- ...bMvcFrameworkExceptionHandlerListener.java | 18 ++++- ...FrameworkExceptionHandlerListenerTest.java | 68 ++++++++++++++++++- ...mmonFrameworkExceptionHandlerListener.java | 24 +++++-- ...FrameworkExceptionHandlerListenerTest.java | 68 ++++++++++++++++++- samples/sample-spring-web-mvc/README.md | 9 ++- .../controller/SampleController.java | 8 +++ ...xpectedErrorsAreReturnedComponentTest.java | 63 +++++++++++++++-- 7 files changed, 242 insertions(+), 16 deletions(-) diff --git a/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringWebMvcFrameworkExceptionHandlerListener.java b/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringWebMvcFrameworkExceptionHandlerListener.java index 9288f9f..2e68ac0 100644 --- a/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringWebMvcFrameworkExceptionHandlerListener.java +++ b/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringWebMvcFrameworkExceptionHandlerListener.java @@ -8,9 +8,11 @@ import com.nike.internal.util.Pair; import org.jetbrains.annotations.NotNull; +import org.springframework.core.MethodParameter; import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MissingRequestHeaderException; import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.ServletRequestBindingException; import org.springframework.web.multipart.support.MissingServletRequestPartException; @@ -99,7 +101,21 @@ protected ApiExceptionHandlerListenerResult handleServletRequestBindingException errorToUse = new ApiErrorWithMetadata( errorToUse, Pair.of("missing_param_name", (Object)detailsEx.getParameterName()), - Pair.of("missing_param_type", (Object)detailsEx.getParameterType()) + Pair.of("missing_param_type", (Object)detailsEx.getParameterType()), + Pair.of("required_location", "query_param") + ); + } + else if (ex instanceof MissingRequestHeaderException mrhEx) { + MethodParameter methodParam = mrhEx.getParameter(); + String requiredTypeNoInfoLeak = extractRequiredTypeNoInfoLeak(methodParam.getParameterType()); + if (requiredTypeNoInfoLeak == null) { + requiredTypeNoInfoLeak = "unknown"; + } + errorToUse = new ApiErrorWithMetadata( + errorToUse, + Pair.of("missing_param_name", mrhEx.getHeaderName()), + Pair.of("missing_param_type", requiredTypeNoInfoLeak), + Pair.of("required_location", "header") ); } diff --git a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringWebMvcFrameworkExceptionHandlerListenerTest.java b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringWebMvcFrameworkExceptionHandlerListenerTest.java index c2967db..25e670f 100644 --- a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringWebMvcFrameworkExceptionHandlerListenerTest.java +++ b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringWebMvcFrameworkExceptionHandlerListenerTest.java @@ -15,16 +15,27 @@ import org.assertj.core.api.Assertions; import org.junit.Test; import org.junit.runner.RunWith; +import org.springframework.core.MethodParameter; import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MissingRequestHeaderException; import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.ServletRequestBindingException; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import org.springframework.web.multipart.support.MissingServletRequestPartException; +import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import java.util.UUID; +import static java.util.Collections.singleton; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -104,6 +115,16 @@ private void validateResponse( assertThat(result.errors).containsExactlyInAnyOrderElementsOf(expectedErrors); } + private void validateResponse( + ApiExceptionHandlerListenerResult result, + boolean expectedShouldHandle, + Collection expectedErrors, + List> expectedExtraDetailsForLogging + ) { + validateResponse(result, expectedShouldHandle, expectedErrors); + assertThat(result.extraDetailsForLogging).containsExactlyInAnyOrderElementsOf(expectedExtraDetailsForLogging); + } + @Test public void shouldHandleException_returns_ignoreResponse_if_passed_exception_it_does_not_want_to_handle() { // when @@ -148,7 +169,8 @@ public void shouldHandleException_returns_MALFORMED_REQUEST_for_ServletRequestBi expectedResult = new ApiErrorWithMetadata( expectedResult, Pair.of("missing_param_name", missingParamName), - Pair.of("missing_param_type", missingParamType) + Pair.of("missing_param_type", missingParamType), + Pair.of("required_location", "query_param") ); } @@ -207,4 +229,48 @@ public void shouldHandleException_returns_UNSUPPORTED_MEDIA_TYPE_for_HttpMediaTy // then validateResponse(result, true, singletonList(testProjectApiErrors.getUnsupportedMediaTypeApiError())); } + + public void methodWithAnnotatedParams( + @RequestHeader int headerParam, + @RequestParam int queryParam, + @RequestHeader @RequestParam int bothParam, + int unknownParam + ) { + // This method is used as part of shouldHandleException_handles_MissingRequestHeaderException_as_expected(). + } + + @Test + public void shouldHandleException_handles_MissingRequestHeaderException_as_expected() throws NoSuchMethodException { + // given + Method method = this.getClass() + .getDeclaredMethod("methodWithAnnotatedParams", int.class, int.class, int.class, int.class); + MethodParameter headerParamDetails = new MethodParameter(method, 0); + + String missingHeaderName = "some-header-" + UUID.randomUUID(); + MissingRequestHeaderException ex = new MissingRequestHeaderException(missingHeaderName, headerParamDetails); + + List> expectedExtraDetailsForLogging = new ArrayList<>(); + ApiExceptionHandlerUtils.DEFAULT_IMPL.addBaseExceptionMessageToExtraDetailsForLogging( + ex, expectedExtraDetailsForLogging + ); + + Map expectedMetadata = new LinkedHashMap<>(); + expectedMetadata.put("missing_param_name", missingHeaderName); + expectedMetadata.put("missing_param_type", "int"); + expectedMetadata.put("required_location", "header"); + + // when + ApiExceptionHandlerListenerResult result = listener.shouldHandleException(ex); + + // then + validateResponse( + result, + true, + singleton(new ApiErrorWithMetadata( + testProjectApiErrors.getMalformedRequestApiError(), + expectedMetadata + )), + expectedExtraDetailsForLogging + ); + } } \ No newline at end of file diff --git a/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListener.java b/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListener.java index 9797a3c..b12a068 100644 --- a/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListener.java +++ b/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListener.java @@ -316,6 +316,14 @@ protected ApiExceptionHandlerListenerResult handleTypeMismatchException( // We can add even more context log details if it's a MethodArgumentTypeMismatchException. if (ex instanceof MethodArgumentTypeMismatchException) { MethodArgumentTypeMismatchException matmEx = (MethodArgumentTypeMismatchException)ex; + List> hOrQMetadata = extractExtraMetadataForHeaderOrQueryParamException(matmEx); + if (hOrQMetadata != null) { + for (Pair pair : hOrQMetadata) { + if (pair != null) { + metadata.put(pair.getKey(), pair.getValue()); + } + } + } extraDetailsForLogging.add(Pair.of("method_arg_name", matmEx.getName())); extraDetailsForLogging.add(Pair.of("method_arg_target_param", matmEx.getParameter().toString())); } @@ -436,7 +444,7 @@ protected boolean isA503TemporaryProblemExceptionClassname(String exClassname) { tmeCause, extraDetailsForLogging, false, - extractExtraMetadataForServerWebInputException(ex) + extractExtraMetadataForHeaderOrQueryParamException(ex) ); } } @@ -473,12 +481,16 @@ else if (exReason.startsWith("Request body is missing") && statusCode == 400) { ); } - protected @Nullable List> extractExtraMetadataForServerWebInputException(Exception maybeSWIEx) { - if (!(maybeSWIEx instanceof ServerWebInputException swiEx)) { - return null; + protected @Nullable List> extractExtraMetadataForHeaderOrQueryParamException( + Exception maybeMethodParamEx + ) { + MethodParameter methodParam = null; + if (maybeMethodParamEx instanceof ServerWebInputException swiEx) { + methodParam = swiEx.getMethodParameter(); + } else if (maybeMethodParamEx instanceof MethodArgumentTypeMismatchException matmEx) { + methodParam = matmEx.getParameter(); } - MethodParameter methodParam = swiEx.getMethodParameter(); if (methodParam == null) { return null; } @@ -594,7 +606,7 @@ protected void addExtraDetailsForLoggingForResponseStatusException( return new RequiredParamData( detailsEx.getName(), extractRequiredTypeNoInfoLeak(detailsEx.getType()), - extractExtraMetadataForServerWebInputException(ex) + extractExtraMetadataForHeaderOrQueryParamException(ex) ); } diff --git a/backstopper-spring-web/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListenerTest.java b/backstopper-spring-web/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListenerTest.java index e6d058d..f75d72f 100644 --- a/backstopper-spring-web/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListenerTest.java +++ b/backstopper-spring-web/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListenerTest.java @@ -1187,7 +1187,7 @@ public void methodWithAnnotatedParams( @RequestHeader @RequestParam int bothParam, int unknownParam ) { - // This method is used as part of shouldHandleException_handles_MissingRequestValueException_as_expected(). + // This method is used as part of some other tests, for generating the necessary MethodParameter objects. } @DataProvider(value = { @@ -1202,7 +1202,7 @@ public void shouldHandleException_handles_MissingRequestValueException_as_expect ) throws NoSuchMethodException { // given Method method = this.getClass() - .getDeclaredMethod("methodWithAnnotatedParams", int.class, int.class, int.class, int.class); + .getDeclaredMethod("methodWithAnnotatedParams", int.class, int.class, int.class, int.class); MethodParameter details = new MethodParameter( method, paramIndex @@ -1247,6 +1247,70 @@ public void shouldHandleException_handles_MissingRequestValueException_as_expect ); } + @DataProvider(value = { + "header | 0", + "query_param | 1", + "header,query_param | 2", + "unknown | 3" + }, splitBy = "\\|") + @Test + public void shouldHandleException_handles_MethodArgumentTypeMismatchException_as_expected_for_annotated_params( + String badValueSource, int paramIndex + ) throws NoSuchMethodException { + // given + Method method = this.getClass() + .getDeclaredMethod("methodWithAnnotatedParams", int.class, int.class, int.class, int.class); + MethodParameter details = new MethodParameter( + method, + paramIndex + ); + + String badParamName = "some-param-" + UUID.randomUUID(); + String badParamValue = "some-bad-value-" + UUID.randomUUID(); + MethodArgumentTypeMismatchException ex = new MethodArgumentTypeMismatchException( + badParamValue, + int.class, + badParamName, + details, + new RuntimeException("some cause") + ); + + List> expectedExtraDetailsForLogging = new ArrayList<>(); + ApiExceptionHandlerUtils.DEFAULT_IMPL.addBaseExceptionMessageToExtraDetailsForLogging( + ex, expectedExtraDetailsForLogging + ); + + expectedExtraDetailsForLogging.addAll(Arrays.asList( + Pair.of("bad_property_name", badParamName), + Pair.of("bad_property_value", badParamValue), + Pair.of("required_type", "int"), + Pair.of("method_arg_name", badParamName), + Pair.of("method_arg_target_param", String.valueOf(ex.getParameter())) + )); + + Map expectedMetadata = new LinkedHashMap<>(); + expectedMetadata.put("bad_property_name", badParamName); + expectedMetadata.put("bad_property_value", badParamValue); + expectedMetadata.put("required_type", "int"); + if (!"unknown".equals(badValueSource)) { + expectedMetadata.put("required_location", badValueSource); + } + + // when + ApiExceptionHandlerListenerResult result = listener.shouldHandleException(ex); + + // then + validateResponse( + result, + true, + singleton(new ApiErrorWithMetadata( + testProjectApiErrors.getTypeConversionApiError(), + expectedMetadata + )), + expectedExtraDetailsForLogging + ); + } + @DataProvider(value = { "true", "false" diff --git a/samples/sample-spring-web-mvc/README.md b/samples/sample-spring-web-mvc/README.md index 91e5785..0855577 100644 --- a/samples/sample-spring-web-mvc/README.md +++ b/samples/sample-spring-web-mvc/README.md @@ -1,6 +1,10 @@ # Backstopper Sample Application - spring-web-mvc -Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 7 and greater. +Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 17 and greater. + +(NOTE: The [Backstopper 1.x branch](https://github.com/Nike-Inc/backstopper/tree/v1.x) contains a version of +Backstopper for Java 7+, and for the `javax` ecosystem. The current Backstopper supports Java 17+ and the `jakarta` +ecosystem. The Backstopper 1.x releases also contain support for Spring 4 and 5, and Springboot 1 and 2.) This submodule contains a sample application based on Spring Web MVC that fully integrates Backstopper. @@ -51,6 +55,9 @@ logs for each error represented in a returned error contract (there can be more * `GET /sample/withRequiredQueryParam?requiredQueryParamValue=not-an-int` - Triggers an error in the Spring Web MVC framework when it cannot coerce the query param value to the required type (an integer), which results in a Backstopper `"Type conversion error"`. +* `GET /sample/withRequiredHeader` with a `requiredHeaderValue: not-an-int` header - Similar to the query param + example, this triggers an error in the Spring Boot framework when it cannot coerce the header value to the required + type (an integer), which results in a Backstopper `"Type conversion error"`. * `GET /does-not-exist` - Triggers a framework 404 which Backstopper handles. * `DELETE /sample` - Triggers a framework 405 which Backstopper handles. * `GET /sample` with `Accept: application/octet-stream` header - Triggers a framework 406 which Backstopper handles. diff --git a/samples/sample-spring-web-mvc/src/main/java/com/nike/backstopper/springsample/controller/SampleController.java b/samples/sample-spring-web-mvc/src/main/java/com/nike/backstopper/springsample/controller/SampleController.java index e5907f7..ee988cd 100644 --- a/samples/sample-spring-web-mvc/src/main/java/com/nike/backstopper/springsample/controller/SampleController.java +++ b/samples/sample-spring-web-mvc/src/main/java/com/nike/backstopper/springsample/controller/SampleController.java @@ -12,6 +12,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; @@ -40,6 +41,7 @@ public class SampleController { public static final String SAMPLE_PATH = "/sample"; public static final String CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH = "/coreErrorWrapper"; public static final String WITH_REQUIRED_QUERY_PARAM_SUBPATH = "/withRequiredQueryParam"; + public static final String WITH_REQUIRED_HEADER_SUBPATH = "/withRequiredHeader"; public static final String TRIGGER_UNHANDLED_ERROR_SUBPATH = "/triggerUnhandledError"; public static int nextRangeInt(int lowerBound, int upperBound) { @@ -105,6 +107,12 @@ public String withRequiredQueryParam(@RequestParam(name = "requiredQueryParamVal return "You passed in " + someRequiredQueryParam + " for the required query param value"; } + @GetMapping(path = WITH_REQUIRED_HEADER_SUBPATH, produces = "text/plain") + @ResponseBody + public String withRequiredHeader(@RequestHeader(name = "requiredHeaderValue") int someRequiredHeader) { + return "You passed in " + someRequiredHeader + " for the required header value"; + } + @GetMapping(path = TRIGGER_UNHANDLED_ERROR_SUBPATH) public void triggerUnhandledError() { throw new RuntimeException("This should be handled by SpringUnhandledExceptionHandler."); diff --git a/samples/sample-spring-web-mvc/src/test/java/com/nike/backstopper/springsample/componenttest/VerifyExpectedErrorsAreReturnedComponentTest.java b/samples/sample-spring-web-mvc/src/test/java/com/nike/backstopper/springsample/componenttest/VerifyExpectedErrorsAreReturnedComponentTest.java index ff6d11b..59d46e2 100644 --- a/samples/sample-spring-web-mvc/src/test/java/com/nike/backstopper/springsample/componenttest/VerifyExpectedErrorsAreReturnedComponentTest.java +++ b/samples/sample-spring-web-mvc/src/test/java/com/nike/backstopper/springsample/componenttest/VerifyExpectedErrorsAreReturnedComponentTest.java @@ -36,6 +36,7 @@ import static com.nike.backstopper.springsample.controller.SampleController.CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH; import static com.nike.backstopper.springsample.controller.SampleController.SAMPLE_PATH; import static com.nike.backstopper.springsample.controller.SampleController.TRIGGER_UNHANDLED_ERROR_SUBPATH; +import static com.nike.backstopper.springsample.controller.SampleController.WITH_REQUIRED_HEADER_SUBPATH; import static com.nike.backstopper.springsample.controller.SampleController.WITH_REQUIRED_QUERY_PARAM_SUBPATH; import static com.nike.backstopper.springsample.controller.SampleController.nextRandomColor; import static com.nike.backstopper.springsample.controller.SampleController.nextRangeInt; @@ -378,7 +379,7 @@ public void verify_sample_post_fails_with_UNSUPPORTED_MEDIA_TYPE_if_passed_inval } @Test - public void verify_MALFORMED_REQUEST_is_thrown_when_required_data_is_missing() { + public void verify_MALFORMED_REQUEST_is_thrown_when_required_query_param_is_missing() { ExtractableResponse response = given() .baseUri("http://localhost") @@ -395,14 +396,15 @@ public void verify_MALFORMED_REQUEST_is_thrown_when_required_data_is_missing() { response, new ApiErrorWithMetadata( SampleCoreApiError.MALFORMED_REQUEST, + Pair.of("missing_param_name", "requiredQueryParamValue"), Pair.of("missing_param_type", "int"), - Pair.of("missing_param_name", "requiredQueryParamValue") + Pair.of("required_location", "query_param") ) ); } @Test - public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert_type() { + public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert_type_for_query_param() { ExtractableResponse response = given() .baseUri("http://localhost") @@ -418,8 +420,59 @@ public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert verifyErrorReceived(response, new ApiErrorWithMetadata( SampleCoreApiError.TYPE_CONVERSION_ERROR, - MapBuilder.builder("bad_property_name", (Object)"requiredQueryParamValue") - .put("bad_property_value", "not-an-integer") + MapBuilder.builder("bad_property_name", (Object) "requiredQueryParamValue") + .put("bad_property_value","not-an-integer") + .put("required_location","query_param") + .put("required_type", "int") + .build() + )); + } + + @Test + public void verify_MALFORMED_REQUEST_is_thrown_when_required_header_is_missing() { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(SERVER_PORT) + .basePath(SAMPLE_PATH + WITH_REQUIRED_HEADER_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived( + response, + new ApiErrorWithMetadata( + SampleCoreApiError.MALFORMED_REQUEST, + Pair.of("missing_param_name", "requiredHeaderValue"), + Pair.of("missing_param_type", "int"), + Pair.of("required_location", "header") + ) + ); + } + + @Test + public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert_type_for_header() { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(SERVER_PORT) + .basePath(SAMPLE_PATH + WITH_REQUIRED_HEADER_SUBPATH) + .header("requiredHeaderValue", "not-an-integer") + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, new ApiErrorWithMetadata( + SampleCoreApiError.TYPE_CONVERSION_ERROR, + MapBuilder.builder("bad_property_name", (Object) "requiredHeaderValue") + .put("bad_property_value","not-an-integer") + .put("required_location","header") .put("required_type", "int") .build() )); From 05f0ea56eeea97566395a88e9ab54f347e65cd73 Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Wed, 11 Sep 2024 12:06:58 -0700 Subject: [PATCH 16/42] Convert springboot 2 webflux sample app for springboot 3 --- .../README.md | 13 ++- .../build.gradle | 26 ++--- .../buildSample.sh | 0 .../runSample.sh | 0 .../springboot3webfluxsample}/Main.java | 8 +- ...SampleSpringboot3WebFluxSpringConfig.java} | 16 +-- .../controller/SampleController.java | 21 ++-- .../error/SampleProjectApiError.java | 4 +- .../error/SampleProjectApiErrorsImpl.java | 4 +- .../model/RgbColor.java | 2 +- .../model/SampleModel.java | 10 +- .../src/main/resources/application.properties | 0 .../src/main/resources/logback.xml | 0 ...xpectedErrorsAreReturnedComponentTest.java | 98 ++++++++++++++----- .../error/SampleProjectApiErrorsImplTest.java | 2 +- .../ApplicationJsr303AnnotationTroller.java | 0 .../VerifyJsr303ContractTest.java | 2 +- ...rtsToClassTypeAnnotationsAreValidTest.java | 0 settings.gradle | 6 +- 19 files changed, 133 insertions(+), 79 deletions(-) rename samples/{sample-spring-boot2-webflux => sample-spring-boot3-webflux}/README.md (88%) rename samples/{sample-spring-boot2-webflux => sample-spring-boot3-webflux}/build.gradle (50%) rename samples/{sample-spring-boot2-webflux => sample-spring-boot3-webflux}/buildSample.sh (100%) rename samples/{sample-spring-boot2-webflux => sample-spring-boot3-webflux}/runSample.sh (100%) rename samples/{sample-spring-boot2-webflux/src/main/java/com/nike/backstopper/springboot2webfluxsample => sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample}/Main.java (58%) rename samples/{sample-spring-boot2-webflux/src/main/java/com/nike/backstopper/springboot2webfluxsample/config/SampleSpringboot2WebFluxSpringConfig.java => sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample/config/SampleSpringboot3WebFluxSpringConfig.java} (93%) rename samples/{sample-spring-boot2-webflux/src/main/java/com/nike/backstopper/springboot2webfluxsample => sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample}/controller/SampleController.java (90%) rename samples/{sample-spring-boot2-webflux/src/main/java/com/nike/backstopper/springboot2webfluxsample => sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample}/error/SampleProjectApiError.java (97%) rename samples/{sample-spring-boot2-webflux/src/main/java/com/nike/backstopper/springboot2webfluxsample => sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample}/error/SampleProjectApiErrorsImpl.java (94%) rename samples/{sample-spring-boot2-webflux/src/main/java/com/nike/backstopper/springboot2webfluxsample => sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample}/model/RgbColor.java (94%) rename samples/{sample-spring-boot2-webflux/src/main/java/com/nike/backstopper/springboot2webfluxsample => sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample}/model/SampleModel.java (87%) rename samples/{sample-spring-boot2-webflux => sample-spring-boot3-webflux}/src/main/resources/application.properties (100%) rename samples/{sample-spring-boot2-webflux => sample-spring-boot3-webflux}/src/main/resources/logback.xml (100%) rename samples/{sample-spring-boot2-webflux => sample-spring-boot3-webflux}/src/test/java/com/nike/backstopper/springbootsample/componenttest/VerifyExpectedErrorsAreReturnedComponentTest.java (86%) rename samples/{sample-spring-boot2-webflux => sample-spring-boot3-webflux}/src/test/java/com/nike/backstopper/springbootsample/error/SampleProjectApiErrorsImplTest.java (91%) rename samples/{sample-spring-boot2-webflux => sample-spring-boot3-webflux}/src/test/java/jsr303convention/ApplicationJsr303AnnotationTroller.java (100%) rename samples/{sample-spring-boot2-webflux => sample-spring-boot3-webflux}/src/test/java/jsr303convention/VerifyJsr303ContractTest.java (96%) rename samples/{sample-spring-boot2-webflux => sample-spring-boot3-webflux}/src/test/java/jsr303convention/VerifyStringConvertsToClassTypeAnnotationsAreValidTest.java (100%) diff --git a/samples/sample-spring-boot2-webflux/README.md b/samples/sample-spring-boot3-webflux/README.md similarity index 88% rename from samples/sample-spring-boot2-webflux/README.md rename to samples/sample-spring-boot3-webflux/README.md index aee4d6c..9a33b31 100644 --- a/samples/sample-spring-boot2-webflux/README.md +++ b/samples/sample-spring-boot3-webflux/README.md @@ -1,8 +1,12 @@ -# Backstopper Sample Application - spring-boot2-webflux +# Backstopper Sample Application - spring-boot3-webflux -Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 7 and greater. +Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 17 and greater. -This submodule contains a sample application based on Spring Boot 2 + WebFlux (Netty) that fully integrates Backstopper. +(NOTE: The [Backstopper 1.x branch](https://github.com/Nike-Inc/backstopper/tree/v1.x) contains a version of +Backstopper for Java 7+, and for the `javax` ecosystem. The current Backstopper supports Java 17+ and the `jakarta` +ecosystem. The Backstopper 1.x releases also contain support for Spring 4 and 5, and Springboot 1 and 2.) + +This submodule contains a sample application based on Spring Boot 3 + WebFlux (Netty) that fully integrates Backstopper. * Build the sample by running the `./buildSample.sh` script. * Launch the sample by running the `./runSample.sh` script. It will bind to port 8080 by default. @@ -51,6 +55,9 @@ Backstopper and converted to a generic service exception. * `GET /sample/withRequiredQueryParam?requiredQueryParamValue=not-an-int` - Triggers an error in the Spring Boot framework when it cannot coerce the query param value to the required type (an integer), which results in a Backstopper `"Type conversion error"`. +* `GET /sample/withRequiredHeader` with a `requiredHeaderValue: not-an-int` header - Similar to the query param + example, this triggers an error in the Spring Boot framework when it cannot coerce the header value to the required + type (an integer), which results in a Backstopper `"Type conversion error"`. * `GET /does-not-exist` - Triggers a framework 404 which Backstopper handles. * `DELETE /sample` - Triggers a framework 405 which Backstopper handles. * `GET /sample` with `Accept: application/octet-stream` header - Triggers a framework 406 which Backstopper handles. diff --git a/samples/sample-spring-boot2-webflux/build.gradle b/samples/sample-spring-boot3-webflux/build.gradle similarity index 50% rename from samples/sample-spring-boot2-webflux/build.gradle rename to samples/sample-spring-boot3-webflux/build.gradle index 35965f9..4e72b8f 100644 --- a/samples/sample-spring-boot2-webflux/build.gradle +++ b/samples/sample-spring-boot3-webflux/build.gradle @@ -5,13 +5,10 @@ buildscript { mavenCentral() } dependencies { - classpath("org.springframework.boot:spring-boot-gradle-plugin:${springboot2Version}") + classpath("org.springframework.boot:spring-boot-gradle-plugin:${springboot3Version}") } } -sourceCompatibility = JavaVersion.VERSION_1_8 -targetCompatibility = JavaVersion.VERSION_1_8 - apply plugin: 'org.springframework.boot' apply plugin: "io.spring.dependency-management" @@ -23,12 +20,11 @@ dependencies { implementation( project(":backstopper-spring-web-flux"), project(":backstopper-custom-validators"), - "ch.qos.logback:logback-classic:$logbackVersion", - "org.springframework.boot:spring-boot-dependencies:$springboot2Version", + "ch.qos.logback:logback-classic", + "org.springframework.boot:spring-boot-dependencies:$springboot3Version", "org.springframework.boot:spring-boot-starter-webflux", - "org.hibernate:hibernate-validator:$hibernateValidatorVersionForNewerSpring", - "javax.el:javax.el-api:$elApiVersion", // The el-api and el-impl are needed for the JSR 303 validation - "org.glassfish:javax.el:$elImplVersion", + "org.hibernate.validator:hibernate-validator", + "org.glassfish.expressly:expressly:$glassfishExpresslyVersion", ) compileOnly( "org.jetbrains:annotations:$jetbrainsAnnotationsVersion" @@ -39,20 +35,14 @@ dependencies { "org.junit.jupiter:junit-jupiter-engine:$junit5Version", "org.junit.jupiter:junit-jupiter-params:$junit5Version", "org.mockito:mockito-core:$mockitoVersion", - "ch.qos.logback:logback-classic:$logbackVersion", + "ch.qos.logback:logback-classic", "org.assertj:assertj-core:$assertJVersion", - "io.rest-assured:rest-assured:$restAssuredVersion", - // Thanks Springboot BOM! :/ - // https://stackoverflow.com/questions/44993615/java-lang-noclassdeffounderror-io-restassured-mapper-factory-gsonobjectmapperfa - "io.rest-assured:json-path:$restAssuredVersion", - "io.rest-assured:xml-path:$restAssuredVersion", - // The jaxb-api is needed for building on the java 11 JDK as these classes were moved out of the Java SE libs. - "javax.xml.bind:jaxb-api:$jaxbApiVersion", + "io.rest-assured:rest-assured", ) } apply plugin: "application" -mainClassName = "com.nike.backstopper.springboot2webmvcsample.Main" +mainClassName = "com.nike.backstopper.springboot3webfluxsample.Main" run { systemProperties = System.getProperties() diff --git a/samples/sample-spring-boot2-webflux/buildSample.sh b/samples/sample-spring-boot3-webflux/buildSample.sh similarity index 100% rename from samples/sample-spring-boot2-webflux/buildSample.sh rename to samples/sample-spring-boot3-webflux/buildSample.sh diff --git a/samples/sample-spring-boot2-webflux/runSample.sh b/samples/sample-spring-boot3-webflux/runSample.sh similarity index 100% rename from samples/sample-spring-boot2-webflux/runSample.sh rename to samples/sample-spring-boot3-webflux/runSample.sh diff --git a/samples/sample-spring-boot2-webflux/src/main/java/com/nike/backstopper/springboot2webfluxsample/Main.java b/samples/sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample/Main.java similarity index 58% rename from samples/sample-spring-boot2-webflux/src/main/java/com/nike/backstopper/springboot2webfluxsample/Main.java rename to samples/sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample/Main.java index 742ca6c..4b5ba51 100644 --- a/samples/sample-spring-boot2-webflux/src/main/java/com/nike/backstopper/springboot2webfluxsample/Main.java +++ b/samples/sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample/Main.java @@ -1,18 +1,18 @@ -package com.nike.backstopper.springboot2webfluxsample; +package com.nike.backstopper.springboot3webfluxsample; -import com.nike.backstopper.springboot2webfluxsample.config.SampleSpringboot2WebFluxSpringConfig; +import com.nike.backstopper.springboot3webfluxsample.config.SampleSpringboot3WebFluxSpringConfig; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Import; /** - * Starts up the Backstopper Spring Boot 2 WebFlux Sample server (on port 8080 by default). + * Starts up the Backstopper Spring Boot 3 WebFlux Sample server (on port 8080 by default). * * @author Nic Munroe */ @SpringBootApplication -@Import(SampleSpringboot2WebFluxSpringConfig.class) +@Import(SampleSpringboot3WebFluxSpringConfig.class) public class Main { public static void main(String[] args) { SpringApplication.run(Main.class, args); diff --git a/samples/sample-spring-boot2-webflux/src/main/java/com/nike/backstopper/springboot2webfluxsample/config/SampleSpringboot2WebFluxSpringConfig.java b/samples/sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample/config/SampleSpringboot3WebFluxSpringConfig.java similarity index 93% rename from samples/sample-spring-boot2-webflux/src/main/java/com/nike/backstopper/springboot2webfluxsample/config/SampleSpringboot2WebFluxSpringConfig.java rename to samples/sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample/config/SampleSpringboot3WebFluxSpringConfig.java index ef90140..c192f59 100644 --- a/samples/sample-spring-boot2-webflux/src/main/java/com/nike/backstopper/springboot2webfluxsample/config/SampleSpringboot2WebFluxSpringConfig.java +++ b/samples/sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample/config/SampleSpringboot3WebFluxSpringConfig.java @@ -1,11 +1,11 @@ -package com.nike.backstopper.springboot2webfluxsample.config; +package com.nike.backstopper.springboot3webfluxsample.config; import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; import com.nike.backstopper.exception.ApiException; import com.nike.backstopper.handler.spring.webflux.config.BackstopperSpringWebFluxConfig; -import com.nike.backstopper.springboot2webfluxsample.controller.SampleController; -import com.nike.backstopper.springboot2webfluxsample.error.SampleProjectApiError; -import com.nike.backstopper.springboot2webfluxsample.error.SampleProjectApiErrorsImpl; +import com.nike.backstopper.springboot3webfluxsample.controller.SampleController; +import com.nike.backstopper.springboot3webfluxsample.error.SampleProjectApiError; +import com.nike.backstopper.springboot3webfluxsample.error.SampleProjectApiErrorsImpl; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -23,12 +23,12 @@ import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; -import javax.validation.Validation; -import javax.validation.Validator; +import jakarta.validation.Validation; +import jakarta.validation.Validator; import reactor.core.publisher.Mono; -import static com.nike.backstopper.springboot2webfluxsample.controller.SampleController.SAMPLE_FROM_ROUTER_FUNCTION_PATH; +import static com.nike.backstopper.springboot3webfluxsample.controller.SampleController.SAMPLE_FROM_ROUTER_FUNCTION_PATH; import static org.springframework.web.reactive.function.server.RequestPredicates.GET; /** @@ -47,7 +47,7 @@ @Import(BackstopperSpringWebFluxConfig.class) // Instead of @Import(BackstopperSpringWebFluxConfig.class), you could component scan the com.nike.backstopper // package like this if you prefer component scanning: @ComponentScan(basePackages = "com.nike.backstopper") -public class SampleSpringboot2WebFluxSpringConfig { +public class SampleSpringboot3WebFluxSpringConfig { /** * @return The {@link ProjectApiErrors} to use for this sample app. diff --git a/samples/sample-spring-boot2-webflux/src/main/java/com/nike/backstopper/springboot2webfluxsample/controller/SampleController.java b/samples/sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample/controller/SampleController.java similarity index 90% rename from samples/sample-spring-boot2-webflux/src/main/java/com/nike/backstopper/springboot2webfluxsample/controller/SampleController.java rename to samples/sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample/controller/SampleController.java index b75101e..3facb09 100644 --- a/samples/sample-spring-boot2-webflux/src/main/java/com/nike/backstopper/springboot2webfluxsample/controller/SampleController.java +++ b/samples/sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample/controller/SampleController.java @@ -1,10 +1,10 @@ -package com.nike.backstopper.springboot2webfluxsample.controller; +package com.nike.backstopper.springboot3webfluxsample.controller; import com.nike.backstopper.exception.ApiException; import com.nike.backstopper.service.ClientDataValidationService; -import com.nike.backstopper.springboot2webfluxsample.error.SampleProjectApiError; -import com.nike.backstopper.springboot2webfluxsample.model.RgbColor; -import com.nike.backstopper.springboot2webfluxsample.model.SampleModel; +import com.nike.backstopper.springboot3webfluxsample.error.SampleProjectApiError; +import com.nike.backstopper.springboot3webfluxsample.model.RgbColor; +import com.nike.backstopper.springboot3webfluxsample.model.SampleModel; import com.nike.internal.util.Pair; import org.springframework.http.HttpStatus; @@ -12,6 +12,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; @@ -22,12 +23,11 @@ import java.util.Arrays; import java.util.UUID; -import javax.validation.Valid; - +import jakarta.validation.Valid; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import static com.nike.backstopper.springboot2webfluxsample.controller.SampleController.SAMPLE_PATH; +import static com.nike.backstopper.springboot3webfluxsample.controller.SampleController.SAMPLE_PATH; import static java.util.Collections.singletonList; /** @@ -45,6 +45,7 @@ public class SampleController { public static final String SAMPLE_PATH = "/sample"; public static final String CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH = "/coreErrorWrapper"; public static final String WITH_REQUIRED_QUERY_PARAM_SUBPATH = "/withRequiredQueryParam"; + public static final String WITH_REQUIRED_HEADER_SUBPATH = "/withRequiredHeader"; public static final String TRIGGER_UNHANDLED_ERROR_SUBPATH = "/triggerUnhandledError"; public static final String SAMPLE_FROM_ROUTER_FUNCTION_PATH = "/sample/fromRouterFunction"; public static final String SAMPLE_FLUX_SUBPATH = "/flux"; @@ -116,6 +117,12 @@ public Mono withRequiredQueryParam(@RequestParam(name = "requiredQueryPa return Mono.just("You passed in " + someRequiredQueryParam + " for the required query param value"); } + @GetMapping(path = WITH_REQUIRED_HEADER_SUBPATH, produces = "text/plain") + @ResponseBody + public Mono withRequiredHeader(@RequestHeader(name = "requiredHeaderValue") int someRequiredHeader) { + return Mono.just("You passed in " + someRequiredHeader + " for the required header value"); + } + @GetMapping(path = TRIGGER_UNHANDLED_ERROR_SUBPATH) public void triggerUnhandledError() { throw new RuntimeException("This should be handled by SpringUnhandledExceptionHandler."); diff --git a/samples/sample-spring-boot2-webflux/src/main/java/com/nike/backstopper/springboot2webfluxsample/error/SampleProjectApiError.java b/samples/sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample/error/SampleProjectApiError.java similarity index 97% rename from samples/sample-spring-boot2-webflux/src/main/java/com/nike/backstopper/springboot2webfluxsample/error/SampleProjectApiError.java rename to samples/sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample/error/SampleProjectApiError.java index 0ece8d0..196b88b 100644 --- a/samples/sample-spring-boot2-webflux/src/main/java/com/nike/backstopper/springboot2webfluxsample/error/SampleProjectApiError.java +++ b/samples/sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample/error/SampleProjectApiError.java @@ -1,10 +1,10 @@ -package com.nike.backstopper.springboot2webfluxsample.error; +package com.nike.backstopper.springboot3webfluxsample.error; import com.nike.backstopper.apierror.ApiError; import com.nike.backstopper.apierror.ApiErrorBase; import com.nike.backstopper.apierror.ApiErrorWithMetadata; import com.nike.backstopper.apierror.sample.SampleCoreApiError; -import com.nike.backstopper.springboot2webfluxsample.model.RgbColor; +import com.nike.backstopper.springboot3webfluxsample.model.RgbColor; import com.nike.internal.util.MapBuilder; import org.springframework.http.HttpStatus; diff --git a/samples/sample-spring-boot2-webflux/src/main/java/com/nike/backstopper/springboot2webfluxsample/error/SampleProjectApiErrorsImpl.java b/samples/sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample/error/SampleProjectApiErrorsImpl.java similarity index 94% rename from samples/sample-spring-boot2-webflux/src/main/java/com/nike/backstopper/springboot2webfluxsample/error/SampleProjectApiErrorsImpl.java rename to samples/sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample/error/SampleProjectApiErrorsImpl.java index 2565fda..71556c9 100644 --- a/samples/sample-spring-boot2-webflux/src/main/java/com/nike/backstopper/springboot2webfluxsample/error/SampleProjectApiErrorsImpl.java +++ b/samples/sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample/error/SampleProjectApiErrorsImpl.java @@ -1,4 +1,4 @@ -package com.nike.backstopper.springboot2webfluxsample.error; +package com.nike.backstopper.springboot3webfluxsample.error; import com.nike.backstopper.apierror.ApiError; import com.nike.backstopper.apierror.projectspecificinfo.ProjectSpecificErrorCodeRange; @@ -9,7 +9,7 @@ import java.util.Arrays; import java.util.List; -import javax.inject.Singleton; +import jakarta.inject.Singleton; /** * Returns the project specific errors for this sample application. {@link #getProjectApiErrors()} will return a diff --git a/samples/sample-spring-boot2-webflux/src/main/java/com/nike/backstopper/springboot2webfluxsample/model/RgbColor.java b/samples/sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample/model/RgbColor.java similarity index 94% rename from samples/sample-spring-boot2-webflux/src/main/java/com/nike/backstopper/springboot2webfluxsample/model/RgbColor.java rename to samples/sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample/model/RgbColor.java index 7a2a525..8011b61 100644 --- a/samples/sample-spring-boot2-webflux/src/main/java/com/nike/backstopper/springboot2webfluxsample/model/RgbColor.java +++ b/samples/sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample/model/RgbColor.java @@ -1,4 +1,4 @@ -package com.nike.backstopper.springboot2webfluxsample.model; +package com.nike.backstopper.springboot3webfluxsample.model; import com.fasterxml.jackson.annotation.JsonCreator; diff --git a/samples/sample-spring-boot2-webflux/src/main/java/com/nike/backstopper/springboot2webfluxsample/model/SampleModel.java b/samples/sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample/model/SampleModel.java similarity index 87% rename from samples/sample-spring-boot2-webflux/src/main/java/com/nike/backstopper/springboot2webfluxsample/model/SampleModel.java rename to samples/sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample/model/SampleModel.java index e0bf1aa..6ef47a2 100644 --- a/samples/sample-spring-boot2-webflux/src/main/java/com/nike/backstopper/springboot2webfluxsample/model/SampleModel.java +++ b/samples/sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample/model/SampleModel.java @@ -1,15 +1,15 @@ -package com.nike.backstopper.springboot2webfluxsample.model; +package com.nike.backstopper.springboot3webfluxsample.model; import com.nike.backstopper.apierror.ApiError; import com.nike.backstopper.apierror.sample.SampleCoreApiError; -import com.nike.backstopper.springboot2webfluxsample.error.SampleProjectApiError; -import com.nike.backstopper.springboot2webfluxsample.error.SampleProjectApiErrorsImpl; +import com.nike.backstopper.springboot3webfluxsample.error.SampleProjectApiError; +import com.nike.backstopper.springboot3webfluxsample.error.SampleProjectApiErrorsImpl; import com.nike.backstopper.validation.constraints.StringConvertsToClassType; import org.hibernate.validator.constraints.Range; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; /** * Simple model class showing the JSR 303 Bean Validation integration in Backstopper. Each message for a JSR 303 diff --git a/samples/sample-spring-boot2-webflux/src/main/resources/application.properties b/samples/sample-spring-boot3-webflux/src/main/resources/application.properties similarity index 100% rename from samples/sample-spring-boot2-webflux/src/main/resources/application.properties rename to samples/sample-spring-boot3-webflux/src/main/resources/application.properties diff --git a/samples/sample-spring-boot2-webflux/src/main/resources/logback.xml b/samples/sample-spring-boot3-webflux/src/main/resources/logback.xml similarity index 100% rename from samples/sample-spring-boot2-webflux/src/main/resources/logback.xml rename to samples/sample-spring-boot3-webflux/src/main/resources/logback.xml diff --git a/samples/sample-spring-boot2-webflux/src/test/java/com/nike/backstopper/springbootsample/componenttest/VerifyExpectedErrorsAreReturnedComponentTest.java b/samples/sample-spring-boot3-webflux/src/test/java/com/nike/backstopper/springbootsample/componenttest/VerifyExpectedErrorsAreReturnedComponentTest.java similarity index 86% rename from samples/sample-spring-boot2-webflux/src/test/java/com/nike/backstopper/springbootsample/componenttest/VerifyExpectedErrorsAreReturnedComponentTest.java rename to samples/sample-spring-boot3-webflux/src/test/java/com/nike/backstopper/springbootsample/componenttest/VerifyExpectedErrorsAreReturnedComponentTest.java index 0c49856..3a63e8f 100644 --- a/samples/sample-spring-boot2-webflux/src/test/java/com/nike/backstopper/springbootsample/componenttest/VerifyExpectedErrorsAreReturnedComponentTest.java +++ b/samples/sample-spring-boot3-webflux/src/test/java/com/nike/backstopper/springbootsample/componenttest/VerifyExpectedErrorsAreReturnedComponentTest.java @@ -5,10 +5,10 @@ import com.nike.backstopper.apierror.sample.SampleCoreApiError; import com.nike.backstopper.model.DefaultErrorContractDTO; import com.nike.backstopper.model.DefaultErrorDTO; -import com.nike.backstopper.springboot2webfluxsample.Main; -import com.nike.backstopper.springboot2webfluxsample.error.SampleProjectApiError; -import com.nike.backstopper.springboot2webfluxsample.model.RgbColor; -import com.nike.backstopper.springboot2webfluxsample.model.SampleModel; +import com.nike.backstopper.springboot3webfluxsample.Main; +import com.nike.backstopper.springboot3webfluxsample.error.SampleProjectApiError; +import com.nike.backstopper.springboot3webfluxsample.model.RgbColor; +import com.nike.backstopper.springboot3webfluxsample.model.SampleModel; import com.nike.internal.util.MapBuilder; import com.nike.internal.util.Pair; @@ -37,19 +37,20 @@ import io.restassured.http.ContentType; import io.restassured.response.ExtractableResponse; -import static com.nike.backstopper.springboot2webfluxsample.controller.SampleController.CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH; -import static com.nike.backstopper.springboot2webfluxsample.controller.SampleController.FLUX_ERROR_SUBPATH; -import static com.nike.backstopper.springboot2webfluxsample.controller.SampleController.MONO_ERROR_SUBPATH; -import static com.nike.backstopper.springboot2webfluxsample.controller.SampleController.SAMPLE_FLUX_SUBPATH; -import static com.nike.backstopper.springboot2webfluxsample.controller.SampleController.SAMPLE_FROM_ROUTER_FUNCTION_PATH; -import static com.nike.backstopper.springboot2webfluxsample.controller.SampleController.SAMPLE_PATH; -import static com.nike.backstopper.springboot2webfluxsample.controller.SampleController.TRIGGER_UNHANDLED_ERROR_SUBPATH; -import static com.nike.backstopper.springboot2webfluxsample.controller.SampleController.WITH_REQUIRED_QUERY_PARAM_SUBPATH; -import static com.nike.backstopper.springboot2webfluxsample.controller.SampleController.nextRandomColor; -import static com.nike.backstopper.springboot2webfluxsample.controller.SampleController.nextRangeInt; -import static com.nike.backstopper.springboot2webfluxsample.error.SampleProjectApiError.INVALID_RANGE_VALUE; -import static com.nike.backstopper.springboot2webfluxsample.error.SampleProjectApiError.NOT_RGB_COLOR_ENUM; -import static com.nike.backstopper.springboot2webfluxsample.error.SampleProjectApiError.RGB_COLOR_CANNOT_BE_NULL; +import static com.nike.backstopper.springboot3webfluxsample.controller.SampleController.CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH; +import static com.nike.backstopper.springboot3webfluxsample.controller.SampleController.FLUX_ERROR_SUBPATH; +import static com.nike.backstopper.springboot3webfluxsample.controller.SampleController.MONO_ERROR_SUBPATH; +import static com.nike.backstopper.springboot3webfluxsample.controller.SampleController.SAMPLE_FLUX_SUBPATH; +import static com.nike.backstopper.springboot3webfluxsample.controller.SampleController.SAMPLE_FROM_ROUTER_FUNCTION_PATH; +import static com.nike.backstopper.springboot3webfluxsample.controller.SampleController.SAMPLE_PATH; +import static com.nike.backstopper.springboot3webfluxsample.controller.SampleController.TRIGGER_UNHANDLED_ERROR_SUBPATH; +import static com.nike.backstopper.springboot3webfluxsample.controller.SampleController.WITH_REQUIRED_HEADER_SUBPATH; +import static com.nike.backstopper.springboot3webfluxsample.controller.SampleController.WITH_REQUIRED_QUERY_PARAM_SUBPATH; +import static com.nike.backstopper.springboot3webfluxsample.controller.SampleController.nextRandomColor; +import static com.nike.backstopper.springboot3webfluxsample.controller.SampleController.nextRangeInt; +import static com.nike.backstopper.springboot3webfluxsample.error.SampleProjectApiError.INVALID_RANGE_VALUE; +import static com.nike.backstopper.springboot3webfluxsample.error.SampleProjectApiError.NOT_RGB_COLOR_ENUM; +import static com.nike.backstopper.springboot3webfluxsample.error.SampleProjectApiError.RGB_COLOR_CANNOT_BE_NULL; import static com.nike.internal.util.testing.TestUtils.findFreePort; import static io.restassured.RestAssured.given; import static java.util.Collections.singleton; @@ -502,7 +503,7 @@ public void verify_sample_post_fails_with_UNSUPPORTED_MEDIA_TYPE_if_passed_inval } @Test - public void verify_MALFORMED_REQUEST_is_thrown_when_required_data_is_missing() { + public void verify_MALFORMED_REQUEST_is_thrown_when_required_query_param_is_missing() { ExtractableResponse response = given() .baseUri("http://localhost") @@ -519,14 +520,15 @@ public void verify_MALFORMED_REQUEST_is_thrown_when_required_data_is_missing() { response, new ApiErrorWithMetadata( SampleCoreApiError.MALFORMED_REQUEST, + Pair.of("missing_param_name", "requiredQueryParamValue"), Pair.of("missing_param_type", "int"), - Pair.of("missing_param_name", "requiredQueryParamValue") + Pair.of("required_location", "query_param") ) ); } @Test - public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert_type() { + public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert_type_for_query_param() { ExtractableResponse response = given() .baseUri("http://localhost") @@ -542,9 +544,59 @@ public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert verifyErrorReceived(response, new ApiErrorWithMetadata( SampleCoreApiError.TYPE_CONVERSION_ERROR, - // We can't expect the bad_property_name=requiredQueryParamValue metadata like we do in Spring Web MVC, - // because Spring WebFlux doesn't add it to the TypeMismatchException cause. - MapBuilder.builder("bad_property_value", (Object) "not-an-integer") + MapBuilder.builder("bad_property_name", (Object) "requiredQueryParamValue") + .put("bad_property_value","not-an-integer") + .put("required_location","query_param") + .put("required_type", "int") + .build() + )); + } + + @Test + public void verify_MALFORMED_REQUEST_is_thrown_when_required_header_is_missing() { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(SERVER_PORT) + .basePath(SAMPLE_PATH + WITH_REQUIRED_HEADER_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived( + response, + new ApiErrorWithMetadata( + SampleCoreApiError.MALFORMED_REQUEST, + Pair.of("missing_param_name", "requiredHeaderValue"), + Pair.of("missing_param_type", "int"), + Pair.of("required_location", "header") + ) + ); + } + + @Test + public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert_type_for_header() { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(SERVER_PORT) + .basePath(SAMPLE_PATH + WITH_REQUIRED_HEADER_SUBPATH) + .header("requiredHeaderValue", "not-an-integer") + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, new ApiErrorWithMetadata( + SampleCoreApiError.TYPE_CONVERSION_ERROR, + MapBuilder.builder("bad_property_name", (Object) "requiredHeaderValue") + .put("bad_property_value","not-an-integer") + .put("required_location","header") .put("required_type", "int") .build() )); diff --git a/samples/sample-spring-boot2-webflux/src/test/java/com/nike/backstopper/springbootsample/error/SampleProjectApiErrorsImplTest.java b/samples/sample-spring-boot3-webflux/src/test/java/com/nike/backstopper/springbootsample/error/SampleProjectApiErrorsImplTest.java similarity index 91% rename from samples/sample-spring-boot2-webflux/src/test/java/com/nike/backstopper/springbootsample/error/SampleProjectApiErrorsImplTest.java rename to samples/sample-spring-boot3-webflux/src/test/java/com/nike/backstopper/springbootsample/error/SampleProjectApiErrorsImplTest.java index 2a1c3ef..f4373c8 100644 --- a/samples/sample-spring-boot2-webflux/src/test/java/com/nike/backstopper/springbootsample/error/SampleProjectApiErrorsImplTest.java +++ b/samples/sample-spring-boot3-webflux/src/test/java/com/nike/backstopper/springbootsample/error/SampleProjectApiErrorsImplTest.java @@ -2,7 +2,7 @@ import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrorsTestBase; -import com.nike.backstopper.springboot2webfluxsample.error.SampleProjectApiErrorsImpl; +import com.nike.backstopper.springboot3webfluxsample.error.SampleProjectApiErrorsImpl; /** * Extends {@link ProjectApiErrorsTestBase} in order to inherit tests that will verify the correctness of this diff --git a/samples/sample-spring-boot2-webflux/src/test/java/jsr303convention/ApplicationJsr303AnnotationTroller.java b/samples/sample-spring-boot3-webflux/src/test/java/jsr303convention/ApplicationJsr303AnnotationTroller.java similarity index 100% rename from samples/sample-spring-boot2-webflux/src/test/java/jsr303convention/ApplicationJsr303AnnotationTroller.java rename to samples/sample-spring-boot3-webflux/src/test/java/jsr303convention/ApplicationJsr303AnnotationTroller.java diff --git a/samples/sample-spring-boot2-webflux/src/test/java/jsr303convention/VerifyJsr303ContractTest.java b/samples/sample-spring-boot3-webflux/src/test/java/jsr303convention/VerifyJsr303ContractTest.java similarity index 96% rename from samples/sample-spring-boot2-webflux/src/test/java/jsr303convention/VerifyJsr303ContractTest.java rename to samples/sample-spring-boot3-webflux/src/test/java/jsr303convention/VerifyJsr303ContractTest.java index 571d903..a3e065a 100644 --- a/samples/sample-spring-boot2-webflux/src/test/java/jsr303convention/VerifyJsr303ContractTest.java +++ b/samples/sample-spring-boot3-webflux/src/test/java/jsr303convention/VerifyJsr303ContractTest.java @@ -3,7 +3,7 @@ import com.nike.backstopper.apierror.contract.jsr303convention.ReflectionBasedJsr303AnnotationTrollerBase; import com.nike.backstopper.apierror.contract.jsr303convention.VerifyJsr303ValidationMessagesPointToApiErrorsTest; import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.springboot2webfluxsample.error.SampleProjectApiErrorsImpl; +import com.nike.backstopper.springboot3webfluxsample.error.SampleProjectApiErrorsImpl; /** * Verifies that *ALL* non-excluded JSR 303 validation annotations in this project have a message defined that maps to a diff --git a/samples/sample-spring-boot2-webflux/src/test/java/jsr303convention/VerifyStringConvertsToClassTypeAnnotationsAreValidTest.java b/samples/sample-spring-boot3-webflux/src/test/java/jsr303convention/VerifyStringConvertsToClassTypeAnnotationsAreValidTest.java similarity index 100% rename from samples/sample-spring-boot2-webflux/src/test/java/jsr303convention/VerifyStringConvertsToClassTypeAnnotationsAreValidTest.java rename to samples/sample-spring-boot3-webflux/src/test/java/jsr303convention/VerifyStringConvertsToClassTypeAnnotationsAreValidTest.java diff --git a/settings.gradle b/settings.gradle index 71cea30..3a9356c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -10,7 +10,6 @@ include "nike-internal-util", "backstopper-spring-web", "backstopper-spring-web-mvc", "backstopper-spring-web-flux", -// "backstopper-spring-boot1", // "backstopper-spring-boot2-webmvc", // // Test-only modules (not published) // "testonly:testonly-spring-reusable-test-support", @@ -20,7 +19,6 @@ include "nike-internal-util", // "testonly:testonly-springboot2-webmvc", // "testonly:testonly-springboot2-webflux", // // Sample modules (not published) - "samples:sample-spring-web-mvc" -// "samples:sample-spring-boot1", + "samples:sample-spring-web-mvc", // "samples:sample-spring-boot2-webmvc", -// "samples:sample-spring-boot2-webflux", \ No newline at end of file + "samples:sample-spring-boot3-webflux" \ No newline at end of file From 3ab437210c0c342378fccf41be2e8aec8704f4d5 Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Wed, 11 Sep 2024 12:37:40 -0700 Subject: [PATCH 17/42] Add new classname to spring-web for spring's NoResourceFoundException which changed packages --- .../OneOffSpringCommonFrameworkExceptionHandlerListener.java | 1 + 1 file changed, 1 insertion(+) diff --git a/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListener.java b/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListener.java index b12a068..90f3ab3 100644 --- a/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListener.java +++ b/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListener.java @@ -72,6 +72,7 @@ public abstract class OneOffSpringCommonFrameworkExceptionHandlerListener implem // Support all the various 404 cases from competing dependencies using classname matching. protected final Set DEFAULT_TO_404_CLASSNAMES = new LinkedHashSet<>(Arrays.asList( // NoHandlerFoundException is found in the spring-webmvc dependency, not spring-web. + "org.springframework.web.servlet.resource.NoResourceFoundException", "org.springframework.web.servlet.NoHandlerFoundException" )); From 544a6ef08db9cef3e7e5a80b64295b3f5f59c401 Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Wed, 11 Sep 2024 12:39:27 -0700 Subject: [PATCH 18/42] Migrate backstopper-spring-boot3-webmvc from springboot2 and from javax to jakarta --- backstopper-spring-boot2-webmvc/build.gradle | 37 --------------- .../README.md | 45 ++++++++++--------- backstopper-spring-boot3-webmvc/build.gradle | 26 +++++++++++ .../BackstopperSpringboot3WebMvcConfig.java | 12 ++--- ...erSpringboot3ContainerErrorController.java | 8 ++-- .../SanityCheckComponentTest.java | 18 ++++---- ...ackstopperSpringboot3WebMvcConfigTest.java | 6 +-- ...ringboot3ContainerErrorControllerTest.java | 16 +++---- .../src/test/resources/logback.xml | 0 settings.gradle | 2 +- 10 files changed, 81 insertions(+), 89 deletions(-) delete mode 100644 backstopper-spring-boot2-webmvc/build.gradle rename {backstopper-spring-boot2-webmvc => backstopper-spring-boot3-webmvc}/README.md (72%) create mode 100644 backstopper-spring-boot3-webmvc/build.gradle rename backstopper-spring-boot2-webmvc/src/main/java/com/nike/backstopper/handler/springboot/config/BackstopperSpringboot2WebMvcConfig.java => backstopper-spring-boot3-webmvc/src/main/java/com/nike/backstopper/handler/springboot/config/BackstopperSpringboot3WebMvcConfig.java (93%) rename backstopper-spring-boot2-webmvc/src/main/java/com/nike/backstopper/handler/springboot/controller/BackstopperSpringboot2ContainerErrorController.java => backstopper-spring-boot3-webmvc/src/main/java/com/nike/backstopper/handler/springboot/controller/BackstopperSpringboot3ContainerErrorController.java (88%) rename {backstopper-spring-boot2-webmvc => backstopper-spring-boot3-webmvc}/src/test/java/com/nike/backstopper/handler/springboot/componenttest/SanityCheckComponentTest.java (96%) rename backstopper-spring-boot2-webmvc/src/test/java/com/nike/backstopper/handler/springboot/config/BackstopperSpringboot2WebMvcConfigTest.java => backstopper-spring-boot3-webmvc/src/test/java/com/nike/backstopper/handler/springboot/config/BackstopperSpringboot3WebMvcConfigTest.java (54%) rename backstopper-spring-boot2-webmvc/src/test/java/com/nike/backstopper/handler/springboot/controller/BackstopperSpringboot2ContainerErrorControllerTest.java => backstopper-spring-boot3-webmvc/src/test/java/com/nike/backstopper/handler/springboot/controller/BackstopperSpringboot3ContainerErrorControllerTest.java (87%) rename {backstopper-spring-boot2-webmvc => backstopper-spring-boot3-webmvc}/src/test/resources/logback.xml (100%) diff --git a/backstopper-spring-boot2-webmvc/build.gradle b/backstopper-spring-boot2-webmvc/build.gradle deleted file mode 100644 index af30389..0000000 --- a/backstopper-spring-boot2-webmvc/build.gradle +++ /dev/null @@ -1,37 +0,0 @@ -evaluationDependsOn(':') - -sourceCompatibility = JavaVersion.VERSION_1_8 -targetCompatibility = JavaVersion.VERSION_1_8 - -ext { - // Springboot 2 requires Servlet API to be at least version 4.0.0 - servletApiForSpringboot2Version = '4.0.0' -} - -dependencies { - api( - project(":backstopper-spring-web-mvc"), - ) - compileOnly( - "org.jetbrains:annotations:$jetbrainsAnnotationsVersion", - "org.springframework.boot:spring-boot-autoconfigure:$springboot2Version", - "org.springframework:spring-webmvc:$spring5Version", - "javax.servlet:javax.servlet-api:$servletApiForSpringboot2Version", - ) - testImplementation( - "junit:junit:$junitVersion", - "org.mockito:mockito-core:$mockitoVersion", - "ch.qos.logback:logback-classic:$logbackVersion", - "org.assertj:assertj-core:$assertJVersion", - "com.tngtech.java:junit-dataprovider:$junitDataproviderVersion", - "javax.servlet:javax.servlet-api:$servletApiVersion", - "io.rest-assured:rest-assured:$restAssuredVersion", - "javax.servlet:javax.servlet-api:$servletApiForSpringboot2Version", - "org.springframework.boot:spring-boot-starter-web:$springboot2Version", - "org.hibernate:hibernate-validator:$hibernateValidatorVersionForNewerSpring", - "javax.el:javax.el-api:$elApiVersion", // The el-api and el-impl are needed for the JSR 303 validation - "org.glassfish:javax.el:$elImplVersion", - // The jaxb-api is needed for building on the java 11 JDK as these classes were moved out of the Java SE libs. - "javax.xml.bind:jaxb-api:$jaxbApiVersion", - ) -} diff --git a/backstopper-spring-boot2-webmvc/README.md b/backstopper-spring-boot3-webmvc/README.md similarity index 72% rename from backstopper-spring-boot2-webmvc/README.md rename to backstopper-spring-boot3-webmvc/README.md index 265383e..d4276e3 100644 --- a/backstopper-spring-boot2-webmvc/README.md +++ b/backstopper-spring-boot3-webmvc/README.md @@ -1,39 +1,41 @@ -# Backstopper - spring-boot2 +# Backstopper - spring-boot3 -Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 7 and greater. +Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 17 and greater. -**NOTE: While the core of Backstopper only requires Java 7, you will need Java 8 for this Spring Boot 2 library.** +(NOTE: The [Backstopper 1.x branch](https://github.com/Nike-Inc/backstopper/tree/v1.x) contains a version of +Backstopper for Java 7+, and for the `javax` ecosystem. The current Backstopper supports Java 17+ and the `jakarta` +ecosystem. The Backstopper 1.x releases also contain support for Spring 4 and 5, and Springboot 1 and 2.) -This readme focuses specifically on the Backstopper Spring Boot 2 integration utilizing the Spring Web MVC framework. +This readme focuses specifically on the Backstopper Spring Boot 3 integration utilizing the Spring Web MVC framework. If you are looking for a different framework integration check out the [relevant section](../README.md#framework_modules) of the base readme to see if one already exists. The [base project README.md](../README.md) and [User Guide](../USER_GUIDE.md) contain the main bulk of information regarding Backstopper. -**NOTE: There is a [Spring Boot 2 Web MVC sample application](../samples/sample-spring-boot2-webmvc/) that provides a +**NOTE: There is a [Spring Boot 3 Web MVC sample application](../samples/sample-spring-boot3-webmvc/) that provides a simple concrete example of the information covered in this readme.** _ALSO NOTE: **This library does not cover Spring WebFlux (Netty) applications**. If you're looking for a -Spring Boot 2 app using the Spring WebFlux framework, then you should use the +Spring Boot 3 app using the Spring WebFlux framework, then you should use the [backstopper-spring-web-flux](../backstopper-spring-web-flux) integration instead._ This library is just for -Spring Boot 2 apps using Spring Web MVC (Servlet). +Spring Boot 3 apps using Spring Web MVC (Servlet). -## Backstopper Spring Boot 2 Web MVC Setup, Configuration, and Usage +## Backstopper Spring Boot 3 Web MVC Setup, Configuration, and Usage ### Setup -* Pull in the `com.nike.backstopper:backstopper-spring-boot2-webmvc` dependency into your project. -* Register Backstopper components with Spring Boot, either via `@Import({BackstopperSpringboot2WebMvcConfig.class})`, or -`@ComponentScan(basePackages = "com.nike.backstopper")`. See the javadocs on `BackstopperSpringboot2WebMvcConfig` for -some related details. +* Pull in the `com.nike.backstopper:backstopper-spring-boot3-webmvc` dependency into your project. +* Register Backstopper components with Spring Boot, either via `@Import({BackstopperSpringboot3WebMvcConfig.class})`, + or `@ComponentScan(basePackages = "com.nike.backstopper")`. See the javadocs on + `BackstopperSpringboot3WebMvcConfig` for some related details. * This causes `SpringApiExceptionHandler` and `SpringUnhandledExceptionHandler` to be registered with the Spring Boot `HandlerExceptionResolver` error handling chain in a way that overrides the default error handlers so that the Backstopper handlers will take care of *all* errors. It sets up `SpringApiExceptionHandler` with a default list of `ApiExceptionHandlerListener` listeners that should be sufficient for most projects. You can override that list of listeners (and/or many other Backstopper components) if needed in your project's Spring config. - * It also registers `BackstopperSpringboot2ContainerErrorController` to handle errors that happen outside Spring + * It also registers `BackstopperSpringboot3ContainerErrorController` to handle errors that happen outside Spring Boot (i.e. in the Servlet container), and make sure they're routed through Backstopper as well. -* Expose your project's `ProjectApiErrors` and a JSR 303 `javax.validation.Validator` implementation in your Spring +* Expose your project's `ProjectApiErrors` and a JSR 303 `jakarta.validation.Validator` implementation in your Spring dependency injection config. * `ProjectApiErrors` creation is discussed in the base Backstopper readme [here](../README.md#quickstart_usage_project_api_errors). @@ -71,7 +73,7 @@ public SomeOutputObject postSomeInput( This method signature with the two `@Valid` annotations would cause both the `@ModelAttribute` `headersAndQueryParams` and `@RequestBody` `inputObject` arguments to be run through JSR 303 validation. Any constraint violations caught at this time will cause a Spring-specific exception to be thrown with the constraint violation details buried inside. -This `backstopper-spring-boot2-webmvc` plugin library's error handler listeners know how to convert this to the +This `backstopper-spring-boot3-webmvc` plugin library's error handler listeners know how to convert this to the appropriate set of `ApiError` cases (from your `ProjectApiErrors`) automatically using the [Backstopper JSR 303 naming convention](../USER_GUIDE.md#jsr303_conventions), which are then returned to the client using the standard error contract. @@ -82,8 +84,8 @@ call a `ClientDataValidationService`. ## NOTE - Spring Boot Autoconfigure, Spring WebMVC, and Servlet API dependencies required at runtime -This `backstopper-spring-boot2-webmvc` module does not export any transitive Spring Boot, Spring, or Servlet API dependencies -to prevent runtime version conflicts with whatever Spring Boot and Servlet environment you deploy to. +This `backstopper-spring-boot3-webmvc` module does not export any transitive Spring Boot, Spring, or Servlet API +dependencies to prevent runtime version conflicts with whatever Spring Boot and Servlet environment you deploy to. This should not affect most users since this library is likely to be used in a Spring Boot/Servlet environment where the required dependencies are already on the classpath at runtime, however if you receive class-not-found errors related to @@ -91,9 +93,12 @@ Spring Boot, Spring, or Servlet API classes then you'll need to pull the necessa The dependencies you may need to pull in: -* Spring Boot Autoconfigure: [org.springframework.boot:spring-boot-autoconfigure:\[spring-boot2-version\]](https://search.maven.org/search?q=g:org.springframework.boot%20AND%20a:spring-boot-autoconfigure) -* Spring Web MVC: [org.springframework:spring-webmvc:\[spring-version\]](https://search.maven.org/search?q=g:org.springframework%20AND%20a:spring-webmvc) -* Servlet 4.0.0+ API: [javax.servlet:javax.servlet-api:\[servlet-api-version\]](https://search.maven.org/search?q=g:javax.servlet%20AND%20a:javax.servlet-api) +* Spring Boot Autoconfigure: + [org.springframework.boot:spring-boot-autoconfigure:\[spring-boot3-version\]](https://search.maven.org/search?q=g:org.springframework.boot%20AND%20a:spring-boot-autoconfigure) +* Spring Web MVC: + [org.springframework:spring-webmvc:\[spring-version\]](https://search.maven.org/search?q=g:org.springframework%20AND%20a:spring-webmvc) +* Jakarta Servlet API: + [jakarta.servlet:jakarta.servlet-api:\[servlet-api-version\]](https://search.maven.org/search?q=g:jakarta.servlet%20AND%20a:jakarta.servlet-api) ## More Info diff --git a/backstopper-spring-boot3-webmvc/build.gradle b/backstopper-spring-boot3-webmvc/build.gradle new file mode 100644 index 0000000..80cb41e --- /dev/null +++ b/backstopper-spring-boot3-webmvc/build.gradle @@ -0,0 +1,26 @@ +evaluationDependsOn(':') + +dependencies { + api( + project(":backstopper-spring-web-mvc"), + ) + compileOnly( + "org.jetbrains:annotations:$jetbrainsAnnotationsVersion", + "org.springframework.boot:spring-boot-autoconfigure:$springboot3Version", + "org.springframework:spring-webmvc:$spring6Version", + "jakarta.servlet:jakarta.servlet-api:$servletApiVersion", + ) + testImplementation( + "junit:junit:$junitVersion", + "org.mockito:mockito-core:$mockitoVersion", + "ch.qos.logback:logback-classic:$logbackVersion", + "org.assertj:assertj-core:$assertJVersion", + "com.tngtech.java:junit-dataprovider:$junitDataproviderVersion", + "jakarta.servlet:jakarta.servlet-api:$servletApiVersion", + "io.rest-assured:rest-assured:$restAssuredVersion", + "jakarta.servlet:jakarta.servlet-api:$servletApiVersion", + "org.springframework.boot:spring-boot-starter-web:$springboot3Version", + "org.hibernate.validator:hibernate-validator:$hibernateValidatorVersion", + "org.glassfish.expressly:expressly:$glassfishExpresslyVersion", + ) +} diff --git a/backstopper-spring-boot2-webmvc/src/main/java/com/nike/backstopper/handler/springboot/config/BackstopperSpringboot2WebMvcConfig.java b/backstopper-spring-boot3-webmvc/src/main/java/com/nike/backstopper/handler/springboot/config/BackstopperSpringboot3WebMvcConfig.java similarity index 93% rename from backstopper-spring-boot2-webmvc/src/main/java/com/nike/backstopper/handler/springboot/config/BackstopperSpringboot2WebMvcConfig.java rename to backstopper-spring-boot3-webmvc/src/main/java/com/nike/backstopper/handler/springboot/config/BackstopperSpringboot3WebMvcConfig.java index 06557a4..760aac2 100644 --- a/backstopper-spring-boot2-webmvc/src/main/java/com/nike/backstopper/handler/springboot/config/BackstopperSpringboot2WebMvcConfig.java +++ b/backstopper-spring-boot3-webmvc/src/main/java/com/nike/backstopper/handler/springboot/config/BackstopperSpringboot3WebMvcConfig.java @@ -7,7 +7,7 @@ import com.nike.backstopper.handler.spring.SpringUnhandledExceptionHandler; import com.nike.backstopper.handler.spring.config.BackstopperSpringWebMvcConfig; import com.nike.backstopper.handler.spring.listener.ApiExceptionHandlerListenerList; -import com.nike.backstopper.handler.springboot.controller.BackstopperSpringboot2ContainerErrorController; +import com.nike.backstopper.handler.springboot.controller.BackstopperSpringboot3ContainerErrorController; import com.nike.backstopper.service.ClientDataValidationService; import com.nike.backstopper.service.FailFastServersideValidationService; import com.nike.backstopper.service.NoOpJsr303Validator; @@ -16,7 +16,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -import javax.validation.Validator; +import jakarta.validation.Validator; /** * This Spring Boot configuration is an alternative to simply scanning all of {@code com.nike.backstopper}. You can @@ -25,10 +25,10 @@ * handlers will supersede the built-in spring exception handler chain and will translate ALL errors heading to * the caller so that they conform to the API error contract. * - * This also pulls in {@link BackstopperSpringboot2ContainerErrorController} to handle exceptions that originate in the + * This also pulls in {@link BackstopperSpringboot3ContainerErrorController} to handle exceptions that originate in the * Servlet container outside Spring proper so they can also be handled by Backstopper. See the * {@link SpringApiExceptionHandler}, {@link SpringUnhandledExceptionHandler}, and - * {@link BackstopperSpringboot2ContainerErrorController} classes themselves for more info. + * {@link BackstopperSpringboot3ContainerErrorController} classes themselves for more info. * *

Most of the necessary dependencies are setup for autowiring so this configuration class should be sufficient * to enable Backstopper error handling in your Spring Boot application, except for two things: @@ -63,8 +63,8 @@ @Configuration @Import({ BackstopperSpringWebMvcConfig.class, - BackstopperSpringboot2ContainerErrorController.class + BackstopperSpringboot3ContainerErrorController.class }) -public class BackstopperSpringboot2WebMvcConfig { +public class BackstopperSpringboot3WebMvcConfig { } diff --git a/backstopper-spring-boot2-webmvc/src/main/java/com/nike/backstopper/handler/springboot/controller/BackstopperSpringboot2ContainerErrorController.java b/backstopper-spring-boot3-webmvc/src/main/java/com/nike/backstopper/handler/springboot/controller/BackstopperSpringboot3ContainerErrorController.java similarity index 88% rename from backstopper-spring-boot2-webmvc/src/main/java/com/nike/backstopper/handler/springboot/controller/BackstopperSpringboot2ContainerErrorController.java rename to backstopper-spring-boot3-webmvc/src/main/java/com/nike/backstopper/handler/springboot/controller/BackstopperSpringboot3ContainerErrorController.java index 1e60b52..b24b08a 100644 --- a/backstopper-spring-boot2-webmvc/src/main/java/com/nike/backstopper/handler/springboot/controller/BackstopperSpringboot2ContainerErrorController.java +++ b/backstopper-spring-boot3-webmvc/src/main/java/com/nike/backstopper/handler/springboot/controller/BackstopperSpringboot3ContainerErrorController.java @@ -9,7 +9,7 @@ import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; -import javax.servlet.ServletRequest; +import jakarta.servlet.ServletRequest; /** * The purpose of this controller is to give a place for the Servlet container to route errors to that would otherwise @@ -25,14 +25,14 @@ @Controller @RequestMapping("${server.error.path:${error.path:/error}}") @SuppressWarnings("WeakerAccess") -public class BackstopperSpringboot2ContainerErrorController implements ErrorController { +public class BackstopperSpringboot3ContainerErrorController implements ErrorController { protected final @NotNull ProjectApiErrors projectApiErrors; protected final @NotNull UnhandledServletContainerErrorHelper unhandledServletContainerErrorHelper; protected final String errorPath; @SuppressWarnings("ConstantConditions") - public BackstopperSpringboot2ContainerErrorController( + public BackstopperSpringboot3ContainerErrorController( @NotNull ProjectApiErrors projectApiErrors, @NotNull UnhandledServletContainerErrorHelper unhandledServletContainerErrorHelper, @NotNull ServerProperties serverProperties @@ -59,8 +59,6 @@ public void error(ServletRequest request) throws Throwable { throw unhandledServletContainerErrorHelper.extractOrGenerateErrorForRequest(request, projectApiErrors); } - // This used to be part of the ErrorController interface in earlier versions of Springboot 2, but now it's not - // in more recent versions. So we can't use the @Override annotation. public String getErrorPath() { return errorPath; } diff --git a/backstopper-spring-boot2-webmvc/src/test/java/com/nike/backstopper/handler/springboot/componenttest/SanityCheckComponentTest.java b/backstopper-spring-boot3-webmvc/src/test/java/com/nike/backstopper/handler/springboot/componenttest/SanityCheckComponentTest.java similarity index 96% rename from backstopper-spring-boot2-webmvc/src/test/java/com/nike/backstopper/handler/springboot/componenttest/SanityCheckComponentTest.java rename to backstopper-spring-boot3-webmvc/src/test/java/com/nike/backstopper/handler/springboot/componenttest/SanityCheckComponentTest.java index 707b65c..fdb34ca 100644 --- a/backstopper-spring-boot2-webmvc/src/test/java/com/nike/backstopper/handler/springboot/componenttest/SanityCheckComponentTest.java +++ b/backstopper-spring-boot3-webmvc/src/test/java/com/nike/backstopper/handler/springboot/componenttest/SanityCheckComponentTest.java @@ -7,7 +7,7 @@ import com.nike.backstopper.apierror.sample.SampleCoreApiError; import com.nike.backstopper.apierror.sample.SampleProjectApiErrorsBase; import com.nike.backstopper.exception.ApiException; -import com.nike.backstopper.handler.springboot.config.BackstopperSpringboot2WebMvcConfig; +import com.nike.backstopper.handler.springboot.config.BackstopperSpringboot3WebMvcConfig; import com.nike.backstopper.model.DefaultErrorContractDTO; import com.nike.backstopper.model.DefaultErrorDTO; @@ -41,13 +41,13 @@ import java.util.Map; import java.util.UUID; -import javax.inject.Singleton; -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.validation.Validation; -import javax.validation.Validator; +import jakarta.inject.Singleton; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Validation; +import jakarta.validation.Validator; import io.restassured.response.ExtractableResponse; @@ -195,7 +195,7 @@ public void verify_ERROR_THROWN_IN_SERVLET_FILTER_OUTSIDE_SPRING_returned_if_ser @SpringBootApplication @Configuration - @Import({BackstopperSpringboot2WebMvcConfig.class, SanityCheckController.class }) + @Import({BackstopperSpringboot3WebMvcConfig.class, SanityCheckController.class }) static class SanitcyCheckComponentTestApp { @Bean public ProjectApiErrors getProjectApiErrors() { diff --git a/backstopper-spring-boot2-webmvc/src/test/java/com/nike/backstopper/handler/springboot/config/BackstopperSpringboot2WebMvcConfigTest.java b/backstopper-spring-boot3-webmvc/src/test/java/com/nike/backstopper/handler/springboot/config/BackstopperSpringboot3WebMvcConfigTest.java similarity index 54% rename from backstopper-spring-boot2-webmvc/src/test/java/com/nike/backstopper/handler/springboot/config/BackstopperSpringboot2WebMvcConfigTest.java rename to backstopper-spring-boot3-webmvc/src/test/java/com/nike/backstopper/handler/springboot/config/BackstopperSpringboot3WebMvcConfigTest.java index d5d65e4..d451d46 100644 --- a/backstopper-spring-boot2-webmvc/src/test/java/com/nike/backstopper/handler/springboot/config/BackstopperSpringboot2WebMvcConfigTest.java +++ b/backstopper-spring-boot3-webmvc/src/test/java/com/nike/backstopper/handler/springboot/config/BackstopperSpringboot3WebMvcConfigTest.java @@ -3,16 +3,16 @@ import org.junit.Test; /** - * Tests the functionality of {@link BackstopperSpringboot2WebMvcConfig}. + * Tests the functionality of {@link BackstopperSpringboot3WebMvcConfig}. * * @author Nic Munroe */ -public class BackstopperSpringboot2WebMvcConfigTest { +public class BackstopperSpringboot3WebMvcConfigTest { @Test public void code_coverage_hoops() { // jump! - new BackstopperSpringboot2WebMvcConfig(); + new BackstopperSpringboot3WebMvcConfig(); } } \ No newline at end of file diff --git a/backstopper-spring-boot2-webmvc/src/test/java/com/nike/backstopper/handler/springboot/controller/BackstopperSpringboot2ContainerErrorControllerTest.java b/backstopper-spring-boot3-webmvc/src/test/java/com/nike/backstopper/handler/springboot/controller/BackstopperSpringboot3ContainerErrorControllerTest.java similarity index 87% rename from backstopper-spring-boot2-webmvc/src/test/java/com/nike/backstopper/handler/springboot/controller/BackstopperSpringboot2ContainerErrorControllerTest.java rename to backstopper-spring-boot3-webmvc/src/test/java/com/nike/backstopper/handler/springboot/controller/BackstopperSpringboot3ContainerErrorControllerTest.java index 9a0bff8..16deff4 100644 --- a/backstopper-spring-boot2-webmvc/src/test/java/com/nike/backstopper/handler/springboot/controller/BackstopperSpringboot2ContainerErrorControllerTest.java +++ b/backstopper-spring-boot3-webmvc/src/test/java/com/nike/backstopper/handler/springboot/controller/BackstopperSpringboot3ContainerErrorControllerTest.java @@ -10,7 +10,7 @@ import java.util.UUID; -import javax.servlet.ServletRequest; +import jakarta.servlet.ServletRequest; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; @@ -19,11 +19,11 @@ import static org.mockito.Mockito.verify; /** - * Tests the functionality of {@link BackstopperSpringboot2ContainerErrorController}. + * Tests the functionality of {@link BackstopperSpringboot3ContainerErrorController}. * * @author Nic Munroe */ -public class BackstopperSpringboot2ContainerErrorControllerTest { +public class BackstopperSpringboot3ContainerErrorControllerTest { private ProjectApiErrors projectApiErrorsMock; private UnhandledServletContainerErrorHelper unhandledContainerErrorHelperMock; @@ -48,7 +48,7 @@ public void beforeMethod() { @Test public void constructor_sets_fields_as_expected() { // when - BackstopperSpringboot2ContainerErrorController impl = new BackstopperSpringboot2ContainerErrorController( + BackstopperSpringboot3ContainerErrorController impl = new BackstopperSpringboot3ContainerErrorController( projectApiErrorsMock, unhandledContainerErrorHelperMock, serverPropertiesMock ); @@ -63,7 +63,7 @@ public void constructor_sets_fields_as_expected() { public void constructor_throws_NPE_if_passed_null_ProjectApiErrors() { // when Throwable ex = catchThrowable( - () -> new BackstopperSpringboot2ContainerErrorController( + () -> new BackstopperSpringboot3ContainerErrorController( null, unhandledContainerErrorHelperMock, serverPropertiesMock ) ); @@ -78,7 +78,7 @@ public void constructor_throws_NPE_if_passed_null_ProjectApiErrors() { public void constructor_throws_NPE_if_passed_null_UnhandledServletContainerErrorHelper() { // when Throwable ex = catchThrowable( - () -> new BackstopperSpringboot2ContainerErrorController( + () -> new BackstopperSpringboot3ContainerErrorController( projectApiErrorsMock, null, serverPropertiesMock ) ); @@ -93,7 +93,7 @@ public void constructor_throws_NPE_if_passed_null_UnhandledServletContainerError public void constructor_throws_NPE_if_passed_null_ServerProperties() { // when Throwable ex = catchThrowable( - () -> new BackstopperSpringboot2ContainerErrorController( + () -> new BackstopperSpringboot3ContainerErrorController( projectApiErrorsMock, unhandledContainerErrorHelperMock, null ) ); @@ -107,7 +107,7 @@ public void constructor_throws_NPE_if_passed_null_ServerProperties() { @Test public void error_method_throws_result_of_calling_UnhandledServletContainerErrorHelper() { // given - BackstopperSpringboot2ContainerErrorController impl = new BackstopperSpringboot2ContainerErrorController( + BackstopperSpringboot3ContainerErrorController impl = new BackstopperSpringboot3ContainerErrorController( projectApiErrorsMock, unhandledContainerErrorHelperMock, serverPropertiesMock ); Throwable expectedEx = new RuntimeException("intentional test exception"); diff --git a/backstopper-spring-boot2-webmvc/src/test/resources/logback.xml b/backstopper-spring-boot3-webmvc/src/test/resources/logback.xml similarity index 100% rename from backstopper-spring-boot2-webmvc/src/test/resources/logback.xml rename to backstopper-spring-boot3-webmvc/src/test/resources/logback.xml diff --git a/settings.gradle b/settings.gradle index 3a9356c..40247b2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -10,7 +10,7 @@ include "nike-internal-util", "backstopper-spring-web", "backstopper-spring-web-mvc", "backstopper-spring-web-flux", -// "backstopper-spring-boot2-webmvc", + "backstopper-spring-boot3-webmvc", // // Test-only modules (not published) // "testonly:testonly-spring-reusable-test-support", // "testonly:testonly-spring4-webmvc", From 3547e4e15237912507423ceb06eb094e829394c5 Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Wed, 11 Sep 2024 15:22:51 -0700 Subject: [PATCH 19/42] Adjust backstopper-spring-web-mvc SpringContainerErrorController to drop support for springboot 1 --- .../SpringContainerErrorController.java | 29 ++++++------------- ...otErrorControllerIsNotOnClasspathTest.java | 12 ++------ 2 files changed, 12 insertions(+), 29 deletions(-) diff --git a/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/SpringContainerErrorController.java b/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/SpringContainerErrorController.java index 8b5df19..e668758 100644 --- a/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/SpringContainerErrorController.java +++ b/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/SpringContainerErrorController.java @@ -32,19 +32,18 @@ * this controller listens on the same path as Springboot's default {@code BasicErrorController} and we'd get a * conflict otherwise. * - *

If you're in a Springboot environment, you should pull in the {@code backstopper-spring-boot1} or - * {@code backstopper-spring-boot2-webmvc} library (whichever is appropriate for your app) and register - * {@code BackstopperSpringboot[1/2]ContainerErrorController} to take the place of this class (and override the default - * {@code BasicErrorController}). + *

If you're in a Springboot environment, you should pull in the {@code backstopper-spring-boot3-webmvc} library + * and register {@code BackstopperSpringboot3ContainerErrorController} to take the place of this class (and override + * the default {@code BasicErrorController}). */ @Controller @RequestMapping("${server.error.path:${error.path:/error}}") -// Use a @Conditional to prevent this class from being registered if we're running in a Springboot 1 or Springboot 2 +// Use a @Conditional to prevent this class from being registered if we're running in a Springboot 3 // application. This is necessary because this class would conflict with the auto-registered BasicErrorController // since they both listen to the same path. // As mentioned in the class javadocs, if you're in a Springboot environment then you should pull in the -// backstopper-spring-boot1 or backstopper-spring-boot2-webmvc library and -// register BackstopperSpringboot[1/2]ContainerErrorController to take the place of this class. +// backstopper-spring-boot3-webmvc library and register BackstopperSpringboot3ContainerErrorController to take the +// place of this class. @Conditional(SpringbootErrorControllerIsNotOnClasspath.class) public class SpringContainerErrorController { @@ -76,7 +75,7 @@ public void error(ServletRequest request) throws Throwable { /** * A {@link ConfigurationCondition} for use with the {@link Conditional} annotation that can be used to prevent * the inclusion of a bean during classpath scanning / importing. This particular class will prevent bean registration - * if Springboot 1 or Springboot 2's {@code ErrorController} is on the classpath. + * if Springboot's {@code ErrorController} is on the classpath. * *

This is used to prevent {@link SpringContainerErrorController} from being registered if you're running in * a Springboot environment, because that controller would conflict with the auto-registered @@ -95,18 +94,8 @@ public ConfigurationPhase getConfigurationPhase() { public boolean matches( ConditionContext context, AnnotatedTypeMetadata metadata ) { - if ( - // Springboot 1 - isClassAvailableOnClasspath("org.springframework.boot.autoconfigure.web.ErrorController") - // Springboot 2 - || isClassAvailableOnClasspath("org.springframework.boot.web.servlet.error.ErrorController") - ) { - // We're in a Springboot 1 or Springboot 2 application. Return false to prevent registration. - return false; - } - - // Didn't detect ErrorController on the classpath, so return true. - return true; + // If we're in a Springboot application we want to return false to prevent registration. + return !isClassAvailableOnClasspath("org.springframework.boot.web.servlet.error.ErrorController"); } protected boolean isClassAvailableOnClasspath(String classname) { diff --git a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringbootErrorControllerIsNotOnClasspathTest.java b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringbootErrorControllerIsNotOnClasspathTest.java index aaa6d49..5d4c4d6 100644 --- a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringbootErrorControllerIsNotOnClasspathTest.java +++ b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringbootErrorControllerIsNotOnClasspathTest.java @@ -51,24 +51,18 @@ public void isClassAvailableOnClasspath_returns_false_for_class_not_on_classpath } @DataProvider(value = { - "true | true | false", - "false | true | false", - "true | false | false", - "false | false | true", + "true | false", + "false | true", }, splitBy = "\\|") @Test public void matches_method_works_as_expected( - boolean sb1IsOnClasspath, boolean sb2IsOnClasspath, boolean expectedResult + boolean sb2IsOnClasspath, boolean expectedResult ) { // given SpringbootErrorControllerIsNotOnClasspath implSpy = spy(impl); ConditionContext contextMock = mock(ConditionContext.class); AnnotatedTypeMetadata metadataMock = mock(AnnotatedTypeMetadata.class); - doReturn(sb1IsOnClasspath) - .when(implSpy) - .isClassAvailableOnClasspath("org.springframework.boot.autoconfigure.web.ErrorController"); - doReturn(sb2IsOnClasspath) .when(implSpy) .isClassAvailableOnClasspath("org.springframework.boot.web.servlet.error.ErrorController"); From 7d3f73615905461c1f21a5d8f25bb6a159d3c033 Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Wed, 11 Sep 2024 15:25:25 -0700 Subject: [PATCH 20/42] Convert springboot 2 webmvc sample app for springboot 3 --- .../sample-spring-boot2-webmvc/build.gradle | 59 ------------ .../README.md | 15 ++-- .../sample-spring-boot3-webmvc/build.gradle | 49 ++++++++++ .../buildSample.sh | 0 .../runSample.sh | 0 .../springboot3webmvcsample}/Main.java | 8 +- .../SampleSpringboot3WebMvcSpringConfig.java} | 32 +++---- .../controller/SampleController.java | 20 +++-- .../error/SampleProjectApiError.java | 4 +- .../error/SampleProjectApiErrorsImpl.java | 4 +- .../model/RgbColor.java | 2 +- .../model/SampleModel.java | 10 +-- .../src/main/resources/application.properties | 0 .../src/main/resources/logback.xml | 0 ...xpectedErrorsAreReturnedComponentTest.java | 89 +++++++++++++++---- .../error/SampleProjectApiErrorsImplTest.java | 2 +- .../ApplicationJsr303AnnotationTroller.java | 0 .../VerifyJsr303ContractTest.java | 2 +- ...rtsToClassTypeAnnotationsAreValidTest.java | 0 samples/sample-spring-web-mvc/README.md | 2 +- settings.gradle | 2 +- 21 files changed, 177 insertions(+), 123 deletions(-) delete mode 100644 samples/sample-spring-boot2-webmvc/build.gradle rename samples/{sample-spring-boot2-webmvc => sample-spring-boot3-webmvc}/README.md (86%) create mode 100644 samples/sample-spring-boot3-webmvc/build.gradle rename samples/{sample-spring-boot2-webmvc => sample-spring-boot3-webmvc}/buildSample.sh (100%) rename samples/{sample-spring-boot2-webmvc => sample-spring-boot3-webmvc}/runSample.sh (100%) rename samples/{sample-spring-boot2-webmvc/src/main/java/com/nike/backstopper/springboot2webmvcsample => sample-spring-boot3-webmvc/src/main/java/com/nike/backstopper/springboot3webmvcsample}/Main.java (59%) rename samples/{sample-spring-boot2-webmvc/src/main/java/com/nike/backstopper/springboot2webmvcsample/config/SampleSpringboot2WebMvcSpringConfig.java => sample-spring-boot3-webmvc/src/main/java/com/nike/backstopper/springboot3webmvcsample/config/SampleSpringboot3WebMvcSpringConfig.java} (83%) rename samples/{sample-spring-boot2-webmvc/src/main/java/com/nike/backstopper/springboot2webmvcsample => sample-spring-boot3-webmvc/src/main/java/com/nike/backstopper/springboot3webmvcsample}/controller/SampleController.java (86%) rename samples/{sample-spring-boot2-webmvc/src/main/java/com/nike/backstopper/springboot2webmvcsample => sample-spring-boot3-webmvc/src/main/java/com/nike/backstopper/springboot3webmvcsample}/error/SampleProjectApiError.java (96%) rename samples/{sample-spring-boot2-webmvc/src/main/java/com/nike/backstopper/springboot2webmvcsample => sample-spring-boot3-webmvc/src/main/java/com/nike/backstopper/springboot3webmvcsample}/error/SampleProjectApiErrorsImpl.java (94%) rename samples/{sample-spring-boot2-webmvc/src/main/java/com/nike/backstopper/springboot2webmvcsample => sample-spring-boot3-webmvc/src/main/java/com/nike/backstopper/springboot3webmvcsample}/model/RgbColor.java (94%) rename samples/{sample-spring-boot2-webmvc/src/main/java/com/nike/backstopper/springboot2webmvcsample => sample-spring-boot3-webmvc/src/main/java/com/nike/backstopper/springboot3webmvcsample}/model/SampleModel.java (87%) rename samples/{sample-spring-boot2-webmvc => sample-spring-boot3-webmvc}/src/main/resources/application.properties (100%) rename samples/{sample-spring-boot2-webmvc => sample-spring-boot3-webmvc}/src/main/resources/logback.xml (100%) rename samples/{sample-spring-boot2-webmvc => sample-spring-boot3-webmvc}/src/test/java/com/nike/backstopper/springbootsample/componenttest/VerifyExpectedErrorsAreReturnedComponentTest.java (85%) rename samples/{sample-spring-boot2-webmvc => sample-spring-boot3-webmvc}/src/test/java/com/nike/backstopper/springbootsample/error/SampleProjectApiErrorsImplTest.java (91%) rename samples/{sample-spring-boot2-webmvc => sample-spring-boot3-webmvc}/src/test/java/jsr303convention/ApplicationJsr303AnnotationTroller.java (100%) rename samples/{sample-spring-boot2-webmvc => sample-spring-boot3-webmvc}/src/test/java/jsr303convention/VerifyJsr303ContractTest.java (96%) rename samples/{sample-spring-boot2-webmvc => sample-spring-boot3-webmvc}/src/test/java/jsr303convention/VerifyStringConvertsToClassTypeAnnotationsAreValidTest.java (100%) diff --git a/samples/sample-spring-boot2-webmvc/build.gradle b/samples/sample-spring-boot2-webmvc/build.gradle deleted file mode 100644 index b367b5e..0000000 --- a/samples/sample-spring-boot2-webmvc/build.gradle +++ /dev/null @@ -1,59 +0,0 @@ -evaluationDependsOn(':') - -buildscript { - repositories { - mavenCentral() - } - dependencies { - classpath("org.springframework.boot:spring-boot-gradle-plugin:${springboot2Version}") - } -} - -sourceCompatibility = JavaVersion.VERSION_1_8 -targetCompatibility = JavaVersion.VERSION_1_8 - -apply plugin: 'org.springframework.boot' -apply plugin: "io.spring.dependency-management" - -test { - useJUnitPlatform() -} - -dependencies { - implementation( - project(":backstopper-spring-boot2-webmvc"), - project(":backstopper-custom-validators"), - "ch.qos.logback:logback-classic:$logbackVersion", - "org.springframework.boot:spring-boot-dependencies:$springboot2Version", - "org.springframework.boot:spring-boot-starter-web", - "org.hibernate:hibernate-validator:$hibernateValidatorVersionForNewerSpring", - "javax.el:javax.el-api:$elApiVersion", // The el-api and el-impl are needed for the JSR 303 validation - "org.glassfish:javax.el:$elImplVersion", - ) - compileOnly( - "org.jetbrains:annotations:$jetbrainsAnnotationsVersion" - ) - testImplementation( - project(":backstopper-reusable-tests-junit5"), - "org.junit.jupiter:junit-jupiter-api:$junit5Version", - "org.junit.jupiter:junit-jupiter-engine:$junit5Version", - "org.junit.jupiter:junit-jupiter-params:$junit5Version", - "org.mockito:mockito-core:$mockitoVersion", - "ch.qos.logback:logback-classic:$logbackVersion", - "org.assertj:assertj-core:$assertJVersion", - "io.rest-assured:rest-assured:$restAssuredVersion", - // Thanks Springboot BOM! :/ - // https://stackoverflow.com/questions/44993615/java-lang-noclassdeffounderror-io-restassured-mapper-factory-gsonobjectmapperfa - "io.rest-assured:json-path:$restAssuredVersion", - "io.rest-assured:xml-path:$restAssuredVersion", - // The jaxb-api is needed for building on the java 11 JDK as these classes were moved out of the Java SE libs. - "javax.xml.bind:jaxb-api:$jaxbApiVersion", - ) -} - -apply plugin: "application" -mainClassName = "com.nike.backstopper.springboot2webmvcsample.Main" - -run { - systemProperties = System.getProperties() -} diff --git a/samples/sample-spring-boot2-webmvc/README.md b/samples/sample-spring-boot3-webmvc/README.md similarity index 86% rename from samples/sample-spring-boot2-webmvc/README.md rename to samples/sample-spring-boot3-webmvc/README.md index b447160..d1ff074 100644 --- a/samples/sample-spring-boot2-webmvc/README.md +++ b/samples/sample-spring-boot3-webmvc/README.md @@ -1,8 +1,8 @@ -# Backstopper Sample Application - spring-boot2-webmvc +# Backstopper Sample Application - spring-boot3-webmvc Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 7 and greater. -This submodule contains a sample application based on Spring Boot 2 + Web MVC (Servlet) that fully integrates +This submodule contains a sample application based on Spring Boot 3 + Web MVC (Servlet) that fully integrates Backstopper. * Build the sample by running the `./buildSample.sh` script. @@ -28,7 +28,7 @@ up in the logs for each error represented in a returned error contract (there ca * `GET /sample` - Returns the JSON serialization for the `SampleModel` model object. You can copy this into a `POST` call to experiment with triggering errors. -* `POST /sample` with `ContentType: application/json` header - Using the JSON model retrieved by the `GET` call, you +* `POST /sample` with `Content-Type: application/json` header - Using the JSON model retrieved by the `GET` call, you can trigger numerous different types of errors, all of which get caught by the Backstopper system and converted into the appropriate error contract. * Omit the `foo` field. @@ -52,13 +52,16 @@ Backstopper and converted to a generic service exception. * `GET /sample/withRequiredQueryParam?requiredQueryParamValue=not-an-int` - Triggers an error in the Spring Boot framework when it cannot coerce the query param value to the required type (an integer), which results in a Backstopper `"Type conversion error"`. +* `GET /sample/withRequiredHeader` with a `requiredHeaderValue: not-an-int` header - Similar to the query param + example, this triggers an error in the Spring Boot framework when it cannot coerce the header value to the required + type (an integer), which results in a Backstopper `"Type conversion error"`. * `GET /does-not-exist` - Triggers a framework 404 which Backstopper handles. * `DELETE /sample` - Triggers a framework 405 which Backstopper handles. * `GET /sample` with `Accept: application/octet-stream` header - Triggers a framework 406 which Backstopper handles. -* `POST /sample` with `ContentType: text/plain` - Triggers a framework 415 which Backstopper handles. +* `POST /sample` with `Content-Type: text/plain` - Triggers a framework 415 which Backstopper handles. * Any request with a `throw-servlet-filter-exception` header set to `true`. This will trigger an exception in a -Servlet filter before the request ever hits Spring. The `BackstopperSpringboot2ContainerErrorController` -from the `backstopper-spring-boot2-webmvc` dependency registers itself to handle these non-Spring container errors, +Servlet filter before the request ever hits Spring. The `BackstopperSpringboot3ContainerErrorController` +from the `backstopper-spring-boot3-webmvc` dependency registers itself to handle these non-Spring container errors, where it is passed off to Backstopper. ## More Info diff --git a/samples/sample-spring-boot3-webmvc/build.gradle b/samples/sample-spring-boot3-webmvc/build.gradle new file mode 100644 index 0000000..dbeb23d --- /dev/null +++ b/samples/sample-spring-boot3-webmvc/build.gradle @@ -0,0 +1,49 @@ +evaluationDependsOn(':') + +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:${springboot3Version}") + } +} + +apply plugin: 'org.springframework.boot' +apply plugin: "io.spring.dependency-management" + +test { + useJUnitPlatform() +} + +dependencies { + implementation( + project(":backstopper-spring-boot3-webmvc"), + project(":backstopper-custom-validators"), + "ch.qos.logback:logback-classic", + "org.springframework.boot:spring-boot-dependencies:$springboot3Version", + "org.springframework.boot:spring-boot-starter-web", + "org.hibernate.validator:hibernate-validator", + "org.glassfish.expressly:expressly:$glassfishExpresslyVersion", + ) + compileOnly( + "org.jetbrains:annotations:$jetbrainsAnnotationsVersion" + ) + testImplementation( + project(":backstopper-reusable-tests-junit5"), + "org.junit.jupiter:junit-jupiter-api:$junit5Version", + "org.junit.jupiter:junit-jupiter-engine:$junit5Version", + "org.junit.jupiter:junit-jupiter-params:$junit5Version", + "org.mockito:mockito-core:$mockitoVersion", + "ch.qos.logback:logback-classic", + "org.assertj:assertj-core:$assertJVersion", + "io.rest-assured:rest-assured", + ) +} + +apply plugin: "application" +mainClassName = "com.nike.backstopper.springboot3webmvcsample.Main" + +run { + systemProperties = System.getProperties() +} diff --git a/samples/sample-spring-boot2-webmvc/buildSample.sh b/samples/sample-spring-boot3-webmvc/buildSample.sh similarity index 100% rename from samples/sample-spring-boot2-webmvc/buildSample.sh rename to samples/sample-spring-boot3-webmvc/buildSample.sh diff --git a/samples/sample-spring-boot2-webmvc/runSample.sh b/samples/sample-spring-boot3-webmvc/runSample.sh similarity index 100% rename from samples/sample-spring-boot2-webmvc/runSample.sh rename to samples/sample-spring-boot3-webmvc/runSample.sh diff --git a/samples/sample-spring-boot2-webmvc/src/main/java/com/nike/backstopper/springboot2webmvcsample/Main.java b/samples/sample-spring-boot3-webmvc/src/main/java/com/nike/backstopper/springboot3webmvcsample/Main.java similarity index 59% rename from samples/sample-spring-boot2-webmvc/src/main/java/com/nike/backstopper/springboot2webmvcsample/Main.java rename to samples/sample-spring-boot3-webmvc/src/main/java/com/nike/backstopper/springboot3webmvcsample/Main.java index 44f7b4f..8c0f24b 100644 --- a/samples/sample-spring-boot2-webmvc/src/main/java/com/nike/backstopper/springboot2webmvcsample/Main.java +++ b/samples/sample-spring-boot3-webmvc/src/main/java/com/nike/backstopper/springboot3webmvcsample/Main.java @@ -1,18 +1,18 @@ -package com.nike.backstopper.springboot2webmvcsample; +package com.nike.backstopper.springboot3webmvcsample; -import com.nike.backstopper.springboot2webmvcsample.config.SampleSpringboot2WebMvcSpringConfig; +import com.nike.backstopper.springboot3webmvcsample.config.SampleSpringboot3WebMvcSpringConfig; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Import; /** - * Starts up the Backstopper Spring Boot 2 Web MVC Sample server (on port 8080 by default). + * Starts up the Backstopper Spring Boot 3 Web MVC Sample server (on port 8080 by default). * * @author Nic Munroe */ @SpringBootApplication -@Import(SampleSpringboot2WebMvcSpringConfig.class) +@Import(SampleSpringboot3WebMvcSpringConfig.class) public class Main { public static void main(String[] args) { SpringApplication.run(Main.class, args); diff --git a/samples/sample-spring-boot2-webmvc/src/main/java/com/nike/backstopper/springboot2webmvcsample/config/SampleSpringboot2WebMvcSpringConfig.java b/samples/sample-spring-boot3-webmvc/src/main/java/com/nike/backstopper/springboot3webmvcsample/config/SampleSpringboot3WebMvcSpringConfig.java similarity index 83% rename from samples/sample-spring-boot2-webmvc/src/main/java/com/nike/backstopper/springboot2webmvcsample/config/SampleSpringboot2WebMvcSpringConfig.java rename to samples/sample-spring-boot3-webmvc/src/main/java/com/nike/backstopper/springboot3webmvcsample/config/SampleSpringboot3WebMvcSpringConfig.java index 8dc9ca7..a462163 100644 --- a/samples/sample-spring-boot2-webmvc/src/main/java/com/nike/backstopper/springboot2webmvcsample/config/SampleSpringboot2WebMvcSpringConfig.java +++ b/samples/sample-spring-boot3-webmvc/src/main/java/com/nike/backstopper/springboot3webmvcsample/config/SampleSpringboot3WebMvcSpringConfig.java @@ -1,11 +1,11 @@ -package com.nike.backstopper.springboot2webmvcsample.config; +package com.nike.backstopper.springboot3webmvcsample.config; import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; import com.nike.backstopper.exception.ApiException; -import com.nike.backstopper.handler.springboot.config.BackstopperSpringboot2WebMvcConfig; -import com.nike.backstopper.handler.springboot.controller.BackstopperSpringboot2ContainerErrorController; -import com.nike.backstopper.springboot2webmvcsample.error.SampleProjectApiError; -import com.nike.backstopper.springboot2webmvcsample.error.SampleProjectApiErrorsImpl; +import com.nike.backstopper.handler.springboot.config.BackstopperSpringboot3WebMvcConfig; +import com.nike.backstopper.handler.springboot.controller.BackstopperSpringboot3ContainerErrorController; +import com.nike.backstopper.springboot3webmvcsample.error.SampleProjectApiError; +import com.nike.backstopper.springboot3webmvcsample.error.SampleProjectApiErrorsImpl; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; @@ -16,19 +16,19 @@ import java.io.IOException; -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.validation.Validation; -import javax.validation.Validator; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.validation.Validation; +import jakarta.validation.Validator; /** * Simple Spring Boot config for the sample app. The {@link ProjectApiErrors} and {@link Validator} beans defined * in this class are needed for autowiring Backstopper and the {@link ProjectApiErrors} in particular allows you * to specify project-specific errors and behaviors. * - *

NOTE: This integrates Backstopper by {@link Import}ing {@link BackstopperSpringboot2WebMvcConfig}. Alternatively, + *

NOTE: This integrates Backstopper by {@link Import}ing {@link BackstopperSpringboot3WebMvcConfig}. Alternatively, * you could integrate Backstopper by component scanning all of the {@code com.nike.backstopper} package and its * subpackages, e.g. by annotating with * {@link org.springframework.context.annotation.ComponentScan @ComponentScan(basePackages = "com.nike.backstopper")}. @@ -36,10 +36,10 @@ * @author Nic Munroe */ @Configuration -@Import(BackstopperSpringboot2WebMvcConfig.class) -// Instead of @Import(BackstopperSpringboot2WebMvcConfig.class), you could component scan the com.nike.backstopper +@Import(BackstopperSpringboot3WebMvcConfig.class) +// Instead of @Import(BackstopperSpringboot3WebMvcConfig.class), you could component scan the com.nike.backstopper // package like this if you prefer component scanning: @ComponentScan(basePackages = "com.nike.backstopper") -public class SampleSpringboot2WebMvcSpringConfig { +public class SampleSpringboot3WebMvcSpringConfig { /** * @return The {@link ProjectApiErrors} to use for this sample app. @@ -66,7 +66,7 @@ public Validator getJsr303Validator() { /** * Registers a custom {@link ExplodingFilter} Servlet filter at the highest precedence that will throw an exception * when the request contains a special header. This exception will be thrown outside of Springboot, and can - * be used to exercise the {@link BackstopperSpringboot2ContainerErrorController}. You wouldn't want this in + * be used to exercise the {@link BackstopperSpringboot3ContainerErrorController}. You wouldn't want this in * a real app. */ @Bean diff --git a/samples/sample-spring-boot2-webmvc/src/main/java/com/nike/backstopper/springboot2webmvcsample/controller/SampleController.java b/samples/sample-spring-boot3-webmvc/src/main/java/com/nike/backstopper/springboot3webmvcsample/controller/SampleController.java similarity index 86% rename from samples/sample-spring-boot2-webmvc/src/main/java/com/nike/backstopper/springboot2webmvcsample/controller/SampleController.java rename to samples/sample-spring-boot3-webmvc/src/main/java/com/nike/backstopper/springboot3webmvcsample/controller/SampleController.java index c8263c8..0564fbe 100644 --- a/samples/sample-spring-boot2-webmvc/src/main/java/com/nike/backstopper/springboot2webmvcsample/controller/SampleController.java +++ b/samples/sample-spring-boot3-webmvc/src/main/java/com/nike/backstopper/springboot3webmvcsample/controller/SampleController.java @@ -1,10 +1,10 @@ -package com.nike.backstopper.springboot2webmvcsample.controller; +package com.nike.backstopper.springboot3webmvcsample.controller; import com.nike.backstopper.exception.ApiException; import com.nike.backstopper.service.ClientDataValidationService; -import com.nike.backstopper.springboot2webmvcsample.error.SampleProjectApiError; -import com.nike.backstopper.springboot2webmvcsample.model.RgbColor; -import com.nike.backstopper.springboot2webmvcsample.model.SampleModel; +import com.nike.backstopper.springboot3webmvcsample.error.SampleProjectApiError; +import com.nike.backstopper.springboot3webmvcsample.model.RgbColor; +import com.nike.backstopper.springboot3webmvcsample.model.SampleModel; import com.nike.internal.util.Pair; import org.springframework.http.HttpStatus; @@ -12,6 +12,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; @@ -20,9 +21,9 @@ import java.util.Arrays; import java.util.UUID; -import javax.validation.Valid; +import jakarta.validation.Valid; -import static com.nike.backstopper.springboot2webmvcsample.controller.SampleController.SAMPLE_PATH; +import static com.nike.backstopper.springboot3webmvcsample.controller.SampleController.SAMPLE_PATH; import static java.util.Collections.singletonList; /** @@ -40,6 +41,7 @@ public class SampleController { public static final String SAMPLE_PATH = "/sample"; public static final String CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH = "/coreErrorWrapper"; public static final String WITH_REQUIRED_QUERY_PARAM_SUBPATH = "/withRequiredQueryParam"; + public static final String WITH_REQUIRED_HEADER_SUBPATH = "/withRequiredHeader"; public static final String TRIGGER_UNHANDLED_ERROR_SUBPATH = "/triggerUnhandledError"; public static int nextRangeInt(int lowerBound, int upperBound) { @@ -105,6 +107,12 @@ public String withRequiredQueryParam(@RequestParam(name = "requiredQueryParamVal return "You passed in " + someRequiredQueryParam + " for the required query param value"; } + @GetMapping(path = WITH_REQUIRED_HEADER_SUBPATH, produces = "text/plain") + @ResponseBody + public String withRequiredHeader(@RequestHeader(name = "requiredHeaderValue") int someRequiredHeader) { + return "You passed in " + someRequiredHeader + " for the required header value"; + } + @GetMapping(path = TRIGGER_UNHANDLED_ERROR_SUBPATH) public void triggerUnhandledError() { throw new RuntimeException("This should be handled by SpringUnhandledExceptionHandler."); diff --git a/samples/sample-spring-boot2-webmvc/src/main/java/com/nike/backstopper/springboot2webmvcsample/error/SampleProjectApiError.java b/samples/sample-spring-boot3-webmvc/src/main/java/com/nike/backstopper/springboot3webmvcsample/error/SampleProjectApiError.java similarity index 96% rename from samples/sample-spring-boot2-webmvc/src/main/java/com/nike/backstopper/springboot2webmvcsample/error/SampleProjectApiError.java rename to samples/sample-spring-boot3-webmvc/src/main/java/com/nike/backstopper/springboot3webmvcsample/error/SampleProjectApiError.java index 15b3a10..45ffe0e 100644 --- a/samples/sample-spring-boot2-webmvc/src/main/java/com/nike/backstopper/springboot2webmvcsample/error/SampleProjectApiError.java +++ b/samples/sample-spring-boot3-webmvc/src/main/java/com/nike/backstopper/springboot3webmvcsample/error/SampleProjectApiError.java @@ -1,10 +1,10 @@ -package com.nike.backstopper.springboot2webmvcsample.error; +package com.nike.backstopper.springboot3webmvcsample.error; import com.nike.backstopper.apierror.ApiError; import com.nike.backstopper.apierror.ApiErrorBase; import com.nike.backstopper.apierror.ApiErrorWithMetadata; import com.nike.backstopper.apierror.sample.SampleCoreApiError; -import com.nike.backstopper.springboot2webmvcsample.model.RgbColor; +import com.nike.backstopper.springboot3webmvcsample.model.RgbColor; import com.nike.internal.util.MapBuilder; import org.springframework.http.HttpStatus; diff --git a/samples/sample-spring-boot2-webmvc/src/main/java/com/nike/backstopper/springboot2webmvcsample/error/SampleProjectApiErrorsImpl.java b/samples/sample-spring-boot3-webmvc/src/main/java/com/nike/backstopper/springboot3webmvcsample/error/SampleProjectApiErrorsImpl.java similarity index 94% rename from samples/sample-spring-boot2-webmvc/src/main/java/com/nike/backstopper/springboot2webmvcsample/error/SampleProjectApiErrorsImpl.java rename to samples/sample-spring-boot3-webmvc/src/main/java/com/nike/backstopper/springboot3webmvcsample/error/SampleProjectApiErrorsImpl.java index 637b2d4..bdfcf71 100644 --- a/samples/sample-spring-boot2-webmvc/src/main/java/com/nike/backstopper/springboot2webmvcsample/error/SampleProjectApiErrorsImpl.java +++ b/samples/sample-spring-boot3-webmvc/src/main/java/com/nike/backstopper/springboot3webmvcsample/error/SampleProjectApiErrorsImpl.java @@ -1,4 +1,4 @@ -package com.nike.backstopper.springboot2webmvcsample.error; +package com.nike.backstopper.springboot3webmvcsample.error; import com.nike.backstopper.apierror.ApiError; import com.nike.backstopper.apierror.projectspecificinfo.ProjectSpecificErrorCodeRange; @@ -9,7 +9,7 @@ import java.util.Arrays; import java.util.List; -import javax.inject.Singleton; +import jakarta.inject.Singleton; /** * Returns the project specific errors for this sample application. {@link #getProjectApiErrors()} will return a diff --git a/samples/sample-spring-boot2-webmvc/src/main/java/com/nike/backstopper/springboot2webmvcsample/model/RgbColor.java b/samples/sample-spring-boot3-webmvc/src/main/java/com/nike/backstopper/springboot3webmvcsample/model/RgbColor.java similarity index 94% rename from samples/sample-spring-boot2-webmvc/src/main/java/com/nike/backstopper/springboot2webmvcsample/model/RgbColor.java rename to samples/sample-spring-boot3-webmvc/src/main/java/com/nike/backstopper/springboot3webmvcsample/model/RgbColor.java index 8f68438..993074c 100644 --- a/samples/sample-spring-boot2-webmvc/src/main/java/com/nike/backstopper/springboot2webmvcsample/model/RgbColor.java +++ b/samples/sample-spring-boot3-webmvc/src/main/java/com/nike/backstopper/springboot3webmvcsample/model/RgbColor.java @@ -1,4 +1,4 @@ -package com.nike.backstopper.springboot2webmvcsample.model; +package com.nike.backstopper.springboot3webmvcsample.model; import com.fasterxml.jackson.annotation.JsonCreator; diff --git a/samples/sample-spring-boot2-webmvc/src/main/java/com/nike/backstopper/springboot2webmvcsample/model/SampleModel.java b/samples/sample-spring-boot3-webmvc/src/main/java/com/nike/backstopper/springboot3webmvcsample/model/SampleModel.java similarity index 87% rename from samples/sample-spring-boot2-webmvc/src/main/java/com/nike/backstopper/springboot2webmvcsample/model/SampleModel.java rename to samples/sample-spring-boot3-webmvc/src/main/java/com/nike/backstopper/springboot3webmvcsample/model/SampleModel.java index 3ce689a..ca76fc8 100644 --- a/samples/sample-spring-boot2-webmvc/src/main/java/com/nike/backstopper/springboot2webmvcsample/model/SampleModel.java +++ b/samples/sample-spring-boot3-webmvc/src/main/java/com/nike/backstopper/springboot3webmvcsample/model/SampleModel.java @@ -1,15 +1,15 @@ -package com.nike.backstopper.springboot2webmvcsample.model; +package com.nike.backstopper.springboot3webmvcsample.model; import com.nike.backstopper.apierror.ApiError; import com.nike.backstopper.apierror.sample.SampleCoreApiError; -import com.nike.backstopper.springboot2webmvcsample.error.SampleProjectApiError; -import com.nike.backstopper.springboot2webmvcsample.error.SampleProjectApiErrorsImpl; +import com.nike.backstopper.springboot3webmvcsample.error.SampleProjectApiError; +import com.nike.backstopper.springboot3webmvcsample.error.SampleProjectApiErrorsImpl; import com.nike.backstopper.validation.constraints.StringConvertsToClassType; import org.hibernate.validator.constraints.Range; -import javax.validation.constraints.NotBlank; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; /** * Simple model class showing the JSR 303 Bean Validation integration in Backstopper. Each message for a JSR 303 diff --git a/samples/sample-spring-boot2-webmvc/src/main/resources/application.properties b/samples/sample-spring-boot3-webmvc/src/main/resources/application.properties similarity index 100% rename from samples/sample-spring-boot2-webmvc/src/main/resources/application.properties rename to samples/sample-spring-boot3-webmvc/src/main/resources/application.properties diff --git a/samples/sample-spring-boot2-webmvc/src/main/resources/logback.xml b/samples/sample-spring-boot3-webmvc/src/main/resources/logback.xml similarity index 100% rename from samples/sample-spring-boot2-webmvc/src/main/resources/logback.xml rename to samples/sample-spring-boot3-webmvc/src/main/resources/logback.xml diff --git a/samples/sample-spring-boot2-webmvc/src/test/java/com/nike/backstopper/springbootsample/componenttest/VerifyExpectedErrorsAreReturnedComponentTest.java b/samples/sample-spring-boot3-webmvc/src/test/java/com/nike/backstopper/springbootsample/componenttest/VerifyExpectedErrorsAreReturnedComponentTest.java similarity index 85% rename from samples/sample-spring-boot2-webmvc/src/test/java/com/nike/backstopper/springbootsample/componenttest/VerifyExpectedErrorsAreReturnedComponentTest.java rename to samples/sample-spring-boot3-webmvc/src/test/java/com/nike/backstopper/springbootsample/componenttest/VerifyExpectedErrorsAreReturnedComponentTest.java index a80d68d..7c94e3c 100644 --- a/samples/sample-spring-boot2-webmvc/src/test/java/com/nike/backstopper/springbootsample/componenttest/VerifyExpectedErrorsAreReturnedComponentTest.java +++ b/samples/sample-spring-boot3-webmvc/src/test/java/com/nike/backstopper/springbootsample/componenttest/VerifyExpectedErrorsAreReturnedComponentTest.java @@ -5,10 +5,10 @@ import com.nike.backstopper.apierror.sample.SampleCoreApiError; import com.nike.backstopper.model.DefaultErrorContractDTO; import com.nike.backstopper.model.DefaultErrorDTO; -import com.nike.backstopper.springboot2webmvcsample.Main; -import com.nike.backstopper.springboot2webmvcsample.error.SampleProjectApiError; -import com.nike.backstopper.springboot2webmvcsample.model.RgbColor; -import com.nike.backstopper.springboot2webmvcsample.model.SampleModel; +import com.nike.backstopper.springboot3webmvcsample.Main; +import com.nike.backstopper.springboot3webmvcsample.error.SampleProjectApiError; +import com.nike.backstopper.springboot3webmvcsample.model.RgbColor; +import com.nike.backstopper.springboot3webmvcsample.model.SampleModel; import com.nike.internal.util.MapBuilder; import com.nike.internal.util.Pair; @@ -36,15 +36,16 @@ import io.restassured.http.ContentType; import io.restassured.response.ExtractableResponse; -import static com.nike.backstopper.springboot2webmvcsample.controller.SampleController.CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH; -import static com.nike.backstopper.springboot2webmvcsample.controller.SampleController.SAMPLE_PATH; -import static com.nike.backstopper.springboot2webmvcsample.controller.SampleController.TRIGGER_UNHANDLED_ERROR_SUBPATH; -import static com.nike.backstopper.springboot2webmvcsample.controller.SampleController.WITH_REQUIRED_QUERY_PARAM_SUBPATH; -import static com.nike.backstopper.springboot2webmvcsample.controller.SampleController.nextRandomColor; -import static com.nike.backstopper.springboot2webmvcsample.controller.SampleController.nextRangeInt; -import static com.nike.backstopper.springboot2webmvcsample.error.SampleProjectApiError.INVALID_RANGE_VALUE; -import static com.nike.backstopper.springboot2webmvcsample.error.SampleProjectApiError.NOT_RGB_COLOR_ENUM; -import static com.nike.backstopper.springboot2webmvcsample.error.SampleProjectApiError.RGB_COLOR_CANNOT_BE_NULL; +import static com.nike.backstopper.springboot3webmvcsample.controller.SampleController.CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH; +import static com.nike.backstopper.springboot3webmvcsample.controller.SampleController.SAMPLE_PATH; +import static com.nike.backstopper.springboot3webmvcsample.controller.SampleController.TRIGGER_UNHANDLED_ERROR_SUBPATH; +import static com.nike.backstopper.springboot3webmvcsample.controller.SampleController.WITH_REQUIRED_HEADER_SUBPATH; +import static com.nike.backstopper.springboot3webmvcsample.controller.SampleController.WITH_REQUIRED_QUERY_PARAM_SUBPATH; +import static com.nike.backstopper.springboot3webmvcsample.controller.SampleController.nextRandomColor; +import static com.nike.backstopper.springboot3webmvcsample.controller.SampleController.nextRangeInt; +import static com.nike.backstopper.springboot3webmvcsample.error.SampleProjectApiError.INVALID_RANGE_VALUE; +import static com.nike.backstopper.springboot3webmvcsample.error.SampleProjectApiError.NOT_RGB_COLOR_ENUM; +import static com.nike.backstopper.springboot3webmvcsample.error.SampleProjectApiError.RGB_COLOR_CANNOT_BE_NULL; import static com.nike.internal.util.testing.TestUtils.findFreePort; import static io.restassured.RestAssured.given; import static java.util.Collections.singleton; @@ -379,7 +380,7 @@ public void verify_sample_post_fails_with_UNSUPPORTED_MEDIA_TYPE_if_passed_inval } @Test - public void verify_MALFORMED_REQUEST_is_thrown_when_required_data_is_missing() { + public void verify_MALFORMED_REQUEST_is_thrown_when_required_query_param_is_missing() { ExtractableResponse response = given() .baseUri("http://localhost") @@ -396,14 +397,15 @@ public void verify_MALFORMED_REQUEST_is_thrown_when_required_data_is_missing() { response, new ApiErrorWithMetadata( SampleCoreApiError.MALFORMED_REQUEST, + Pair.of("missing_param_name", "requiredQueryParamValue"), Pair.of("missing_param_type", "int"), - Pair.of("missing_param_name", "requiredQueryParamValue") + Pair.of("required_location", "query_param") ) ); } @Test - public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert_type() { + public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert_type_for_query_param() { ExtractableResponse response = given() .baseUri("http://localhost") @@ -419,8 +421,59 @@ public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert verifyErrorReceived(response, new ApiErrorWithMetadata( SampleCoreApiError.TYPE_CONVERSION_ERROR, - MapBuilder.builder("bad_property_name", (Object)"requiredQueryParamValue") - .put("bad_property_value", "not-an-integer") + MapBuilder.builder("bad_property_name", (Object) "requiredQueryParamValue") + .put("bad_property_value","not-an-integer") + .put("required_location","query_param") + .put("required_type", "int") + .build() + )); + } + + @Test + public void verify_MALFORMED_REQUEST_is_thrown_when_required_header_is_missing() { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(SERVER_PORT) + .basePath(SAMPLE_PATH + WITH_REQUIRED_HEADER_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived( + response, + new ApiErrorWithMetadata( + SampleCoreApiError.MALFORMED_REQUEST, + Pair.of("missing_param_name", "requiredHeaderValue"), + Pair.of("missing_param_type", "int"), + Pair.of("required_location", "header") + ) + ); + } + + @Test + public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert_type_for_header() { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(SERVER_PORT) + .basePath(SAMPLE_PATH + WITH_REQUIRED_HEADER_SUBPATH) + .header("requiredHeaderValue", "not-an-integer") + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, new ApiErrorWithMetadata( + SampleCoreApiError.TYPE_CONVERSION_ERROR, + MapBuilder.builder("bad_property_name", (Object) "requiredHeaderValue") + .put("bad_property_value","not-an-integer") + .put("required_location","header") .put("required_type", "int") .build() )); diff --git a/samples/sample-spring-boot2-webmvc/src/test/java/com/nike/backstopper/springbootsample/error/SampleProjectApiErrorsImplTest.java b/samples/sample-spring-boot3-webmvc/src/test/java/com/nike/backstopper/springbootsample/error/SampleProjectApiErrorsImplTest.java similarity index 91% rename from samples/sample-spring-boot2-webmvc/src/test/java/com/nike/backstopper/springbootsample/error/SampleProjectApiErrorsImplTest.java rename to samples/sample-spring-boot3-webmvc/src/test/java/com/nike/backstopper/springbootsample/error/SampleProjectApiErrorsImplTest.java index e774267..5c3a40d 100644 --- a/samples/sample-spring-boot2-webmvc/src/test/java/com/nike/backstopper/springbootsample/error/SampleProjectApiErrorsImplTest.java +++ b/samples/sample-spring-boot3-webmvc/src/test/java/com/nike/backstopper/springbootsample/error/SampleProjectApiErrorsImplTest.java @@ -2,7 +2,7 @@ import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrorsTestBase; -import com.nike.backstopper.springboot2webmvcsample.error.SampleProjectApiErrorsImpl; +import com.nike.backstopper.springboot3webmvcsample.error.SampleProjectApiErrorsImpl; /** * Extends {@link ProjectApiErrorsTestBase} in order to inherit tests that will verify the correctness of this diff --git a/samples/sample-spring-boot2-webmvc/src/test/java/jsr303convention/ApplicationJsr303AnnotationTroller.java b/samples/sample-spring-boot3-webmvc/src/test/java/jsr303convention/ApplicationJsr303AnnotationTroller.java similarity index 100% rename from samples/sample-spring-boot2-webmvc/src/test/java/jsr303convention/ApplicationJsr303AnnotationTroller.java rename to samples/sample-spring-boot3-webmvc/src/test/java/jsr303convention/ApplicationJsr303AnnotationTroller.java diff --git a/samples/sample-spring-boot2-webmvc/src/test/java/jsr303convention/VerifyJsr303ContractTest.java b/samples/sample-spring-boot3-webmvc/src/test/java/jsr303convention/VerifyJsr303ContractTest.java similarity index 96% rename from samples/sample-spring-boot2-webmvc/src/test/java/jsr303convention/VerifyJsr303ContractTest.java rename to samples/sample-spring-boot3-webmvc/src/test/java/jsr303convention/VerifyJsr303ContractTest.java index 8979cf1..abed086 100644 --- a/samples/sample-spring-boot2-webmvc/src/test/java/jsr303convention/VerifyJsr303ContractTest.java +++ b/samples/sample-spring-boot3-webmvc/src/test/java/jsr303convention/VerifyJsr303ContractTest.java @@ -3,7 +3,7 @@ import com.nike.backstopper.apierror.contract.jsr303convention.ReflectionBasedJsr303AnnotationTrollerBase; import com.nike.backstopper.apierror.contract.jsr303convention.VerifyJsr303ValidationMessagesPointToApiErrorsTest; import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.springboot2webmvcsample.error.SampleProjectApiErrorsImpl; +import com.nike.backstopper.springboot3webmvcsample.error.SampleProjectApiErrorsImpl; /** * Verifies that *ALL* non-excluded JSR 303 validation annotations in this project have a message defined that maps to a diff --git a/samples/sample-spring-boot2-webmvc/src/test/java/jsr303convention/VerifyStringConvertsToClassTypeAnnotationsAreValidTest.java b/samples/sample-spring-boot3-webmvc/src/test/java/jsr303convention/VerifyStringConvertsToClassTypeAnnotationsAreValidTest.java similarity index 100% rename from samples/sample-spring-boot2-webmvc/src/test/java/jsr303convention/VerifyStringConvertsToClassTypeAnnotationsAreValidTest.java rename to samples/sample-spring-boot3-webmvc/src/test/java/jsr303convention/VerifyStringConvertsToClassTypeAnnotationsAreValidTest.java diff --git a/samples/sample-spring-web-mvc/README.md b/samples/sample-spring-web-mvc/README.md index 0855577..bf4f5a5 100644 --- a/samples/sample-spring-web-mvc/README.md +++ b/samples/sample-spring-web-mvc/README.md @@ -56,7 +56,7 @@ logs for each error represented in a returned error contract (there can be more framework when it cannot coerce the query param value to the required type (an integer), which results in a Backstopper `"Type conversion error"`. * `GET /sample/withRequiredHeader` with a `requiredHeaderValue: not-an-int` header - Similar to the query param - example, this triggers an error in the Spring Boot framework when it cannot coerce the header value to the required + example, this triggers an error in the Spring Web MVC framework when it cannot coerce the header value to the required type (an integer), which results in a Backstopper `"Type conversion error"`. * `GET /does-not-exist` - Triggers a framework 404 which Backstopper handles. * `DELETE /sample` - Triggers a framework 405 which Backstopper handles. diff --git a/settings.gradle b/settings.gradle index 40247b2..f58293c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -20,5 +20,5 @@ include "nike-internal-util", // "testonly:testonly-springboot2-webflux", // // Sample modules (not published) "samples:sample-spring-web-mvc", -// "samples:sample-spring-boot2-webmvc", + "samples:sample-spring-boot3-webmvc", "samples:sample-spring-boot3-webflux" \ No newline at end of file From 104c931afdccc823083cd183c2da2e4a366ecbb4 Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Wed, 11 Sep 2024 15:28:03 -0700 Subject: [PATCH 21/42] Delete springboot 1 stuff --- backstopper-spring-boot1/README.md | 97 ---- backstopper-spring-boot1/build.gradle | 35 -- .../config/BackstopperSpringboot1Config.java | 70 --- ...erSpringboot1ContainerErrorController.java | 67 --- .../SanityCheckComponentTest.java | 322 ------------ .../BackstopperSpringboot1ConfigTest.java | 18 - ...ringboot1ContainerErrorControllerTest.java | 126 ----- .../src/test/resources/logback.xml | 11 - samples/sample-spring-boot1/README.md | 70 --- samples/sample-spring-boot1/build.gradle | 46 -- samples/sample-spring-boot1/buildSample.sh | 2 - samples/sample-spring-boot1/runSample.sh | 3 - .../backstopper/springbootsample/Main.java | 20 - .../config/SampleSpringboot1SpringConfig.java | 96 ---- .../controller/SampleController.java | 112 ----- .../error/SampleProjectApiError.java | 94 ---- .../error/SampleProjectApiErrorsImpl.java | 41 -- .../springbootsample/model/RgbColor.java | 27 - .../springbootsample/model/SampleModel.java | 51 -- .../src/main/resources/application.properties | 1 - .../src/main/resources/logback.xml | 11 - ...xpectedErrorsAreReturnedComponentTest.java | 474 ------------------ .../error/SampleProjectApiErrorsImplTest.java | 20 - .../ApplicationJsr303AnnotationTroller.java | 47 -- .../VerifyJsr303ContractTest.java | 34 -- ...rtsToClassTypeAnnotationsAreValidTest.java | 25 - 26 files changed, 1920 deletions(-) delete mode 100644 backstopper-spring-boot1/README.md delete mode 100644 backstopper-spring-boot1/build.gradle delete mode 100644 backstopper-spring-boot1/src/main/java/com/nike/backstopper/handler/springboot/config/BackstopperSpringboot1Config.java delete mode 100644 backstopper-spring-boot1/src/main/java/com/nike/backstopper/handler/springboot/controller/BackstopperSpringboot1ContainerErrorController.java delete mode 100644 backstopper-spring-boot1/src/test/java/com/nike/backstopper/handler/springboot/componenttest/SanityCheckComponentTest.java delete mode 100644 backstopper-spring-boot1/src/test/java/com/nike/backstopper/handler/springboot/config/BackstopperSpringboot1ConfigTest.java delete mode 100644 backstopper-spring-boot1/src/test/java/com/nike/backstopper/handler/springboot/controller/BackstopperSpringboot1ContainerErrorControllerTest.java delete mode 100644 backstopper-spring-boot1/src/test/resources/logback.xml delete mode 100644 samples/sample-spring-boot1/README.md delete mode 100644 samples/sample-spring-boot1/build.gradle delete mode 100755 samples/sample-spring-boot1/buildSample.sh delete mode 100755 samples/sample-spring-boot1/runSample.sh delete mode 100644 samples/sample-spring-boot1/src/main/java/com/nike/backstopper/springbootsample/Main.java delete mode 100644 samples/sample-spring-boot1/src/main/java/com/nike/backstopper/springbootsample/config/SampleSpringboot1SpringConfig.java delete mode 100644 samples/sample-spring-boot1/src/main/java/com/nike/backstopper/springbootsample/controller/SampleController.java delete mode 100644 samples/sample-spring-boot1/src/main/java/com/nike/backstopper/springbootsample/error/SampleProjectApiError.java delete mode 100644 samples/sample-spring-boot1/src/main/java/com/nike/backstopper/springbootsample/error/SampleProjectApiErrorsImpl.java delete mode 100644 samples/sample-spring-boot1/src/main/java/com/nike/backstopper/springbootsample/model/RgbColor.java delete mode 100644 samples/sample-spring-boot1/src/main/java/com/nike/backstopper/springbootsample/model/SampleModel.java delete mode 100644 samples/sample-spring-boot1/src/main/resources/application.properties delete mode 100644 samples/sample-spring-boot1/src/main/resources/logback.xml delete mode 100644 samples/sample-spring-boot1/src/test/java/com/nike/backstopper/springbootsample/componenttest/VerifyExpectedErrorsAreReturnedComponentTest.java delete mode 100644 samples/sample-spring-boot1/src/test/java/com/nike/backstopper/springbootsample/error/SampleProjectApiErrorsImplTest.java delete mode 100644 samples/sample-spring-boot1/src/test/java/jsr303convention/ApplicationJsr303AnnotationTroller.java delete mode 100644 samples/sample-spring-boot1/src/test/java/jsr303convention/VerifyJsr303ContractTest.java delete mode 100644 samples/sample-spring-boot1/src/test/java/jsr303convention/VerifyStringConvertsToClassTypeAnnotationsAreValidTest.java diff --git a/backstopper-spring-boot1/README.md b/backstopper-spring-boot1/README.md deleted file mode 100644 index 6f15099..0000000 --- a/backstopper-spring-boot1/README.md +++ /dev/null @@ -1,97 +0,0 @@ -# Backstopper - spring-boot1 - -Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 7 and greater. - -This readme focuses specifically on the Backstopper Spring Boot 1 integration. If you are looking for a different -framework integration check out the [relevant section](../README.md#framework_modules) of the base readme to see if one -already exists. The [base project README.md](../README.md) and [User Guide](../USER_GUIDE.md) contain the main bulk of -information regarding Backstopper. - -**NOTE: There is a [Spring Boot 1 sample application](../samples/sample-spring-boot1/) that provides a simple concrete -example of the information covered in this readme.** - -## Backstopper Spring Boot 1 Setup, Configuration, and Usage - -### Setup - -* Pull in the `com.nike.backstopper:backstopper-spring-boot1` dependency into your project. -* Register Backstopper components with Spring Boot, either via `@Import({BackstopperSpringboot1Config.class})`, or -`@ComponentScan(basePackages = "com.nike.backstopper")`. See the javadocs on `BackstopperSpringboot1Config` for some -related details. - * This causes `SpringApiExceptionHandler` and `SpringUnhandledExceptionHandler` to be registered with the - Spring Boot `HandlerExceptionResolver` error handling chain in a way that overrides the default error handlers so - that the Backstopper handlers will take care of *all* errors. It sets up `SpringApiExceptionHandler` with a default - list of `ApiExceptionHandlerListener` listeners that should be sufficient for most projects. You can override that - list of listeners (and/or many other Backstopper components) if needed in your project's Spring config. - * It also registers `BackstopperSpringboot1ContainerErrorController` to handle errors that happen outside Spring - Boot (i.e. in the Servlet container), and make sure they're routed through Backstopper as well. -* Expose your project's `ProjectApiErrors` and a JSR 303 `javax.validation.Validator` implementation in your Spring -dependency injection config. - * `ProjectApiErrors` creation is discussed in the base Backstopper readme - [here](../README.md#quickstart_usage_project_api_errors). - * JSR 303 setup and generation of a `Validator` is discussed in the Backstopper User Guide - [here](../USER_GUIDE.md#jsr_303_basic_setup). If you're not going to be doing any JSR 303 validation outside what - is built-in supported by Spring Web MVC, *and* you don't want to bother jumping through the hoops to get a handle - on Spring's JSR 303 validator impl provided by `WebMvcConfigurer.getValidator()`, *and* you don't want to bother - creating a real `Validator` yourself then you can simply register `NoOpJsr303Validator.SINGLETON_IMPL` as the - `Validator` that gets exposed by your Spring config. `ClientDataValidationService` and - `FailFastServersideValidationService` would fail to do anything, but if you don't use those then it wouldn't matter. -* Setup the reusable unit tests for your project as described in the Backstopper User Guide -[here](../USER_GUIDE.md#reusable_tests) and shown in the sample application. - -### Usage - -The base Backstopper readme covers the [usage basics](../README.md#quickstart_usage). There should be no difference -when running in a Spring Boot environment, but since Spring Boot integrates a JSR 303 validation system into its core -functionality we can get one extra nice tidbit: to have Spring Boot run validation on objects deserialized from -incoming user data you can simply add `@Valid` annotations on the objects you're deserializing for your controller -endpoints (`@RequestBody` object, `@ModelAttribute` objects, etc). For example: - -``` java -@RequestMapping(method=RequestMethod.POST) -@ResponseBody -@ResponseStatus(HttpStatus.CREATED) -public SomeOutputObject postSomeInput( - @ModelAttribute @Valid HeadersAndQueryParams headersAndQueryParams, - @RequestBody @Valid SomeInputObject inputObject) { - - // ... Normal controller processing - -} -``` - -This method signature with the two `@Valid` annotations would cause both the `@ModelAttribute` `headersAndQueryParams` -and `@RequestBody` `inputObject` arguments to be run through JSR 303 validation. Any constraint violations caught at -this time will cause a Spring-specific exception to be thrown with the constraint violation details buried inside. -This `backstopper-spring-boot1` plugin library's error handler listeners know how to convert this to the appropriate -set of `ApiError` cases (from your `ProjectApiErrors`) automatically using the -[Backstopper JSR 303 naming convention](../USER_GUIDE.md#jsr303_conventions), which are then returned to the client -using the standard error contract. - -This feature allows you to enjoy the Backstopper JSR 303 validation integration support automatically at the point -where caller-provided data is deserialized and passed to your controller endpoint without having to inject and manually -call a `ClientDataValidationService`. - -## NOTE - Spring Boot Autoconfigure, Spring WebMVC, and Servlet API dependencies required at runtime - -This `backstopper-spring-boot1` module does not export any transitive Spring Boot, Spring, or Servlet API dependencies -to prevent runtime version conflicts with whatever Spring Boot and Servlet environment you deploy to. - -This should not affect most users since this library is likely to be used in a Spring Boot/Servlet environment where the -required dependencies are already on the classpath at runtime, however if you receive class-not-found errors related to -Spring Boot, Spring, or Servlet API classes then you'll need to pull the necessary dependency into your project. - -The dependencies you may need to pull in: - -* Spring Boot Autoconfigure: [org.springframework.boot:spring-boot-autoconfigure:\[spring-boot1-version\]](https://search.maven.org/search?q=g:org.springframework.boot%20AND%20a:spring-boot-autoconfigure) -* Spring Web MVC: [org.springframework:spring-webmvc:\[spring-version\]](https://search.maven.org/search?q=g:org.springframework%20AND%20a:spring-webmvc) -* Servlet 3.1.0+ API: [javax.servlet:javax.servlet-api:\[servlet-api-version\]](https://search.maven.org/search?q=g:javax.servlet%20AND%20a:javax.servlet-api) - -## More Info - -See the [base project README.md](../README.md), [User Guide](../USER_GUIDE.md), and Backstopper repository source code -and javadocs for all further information. - -## License - -Backstopper is released under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/backstopper-spring-boot1/build.gradle b/backstopper-spring-boot1/build.gradle deleted file mode 100644 index ef4921d..0000000 --- a/backstopper-spring-boot1/build.gradle +++ /dev/null @@ -1,35 +0,0 @@ -evaluationDependsOn(':') - -compileTestJava { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 -} - -ext { - // Springboot 1 requires Servlet API to be at least version 3.1 - servletApiForSpringboot1Version = '3.1.0' -} - -dependencies { - api( - project(":backstopper-spring-web-mvc"), - ) - compileOnly( - "org.jetbrains:annotations:$jetbrainsAnnotationsVersion", - "org.springframework.boot:spring-boot-autoconfigure:$springboot1Version", - "org.springframework:spring-webmvc:$spring4Version", - "javax.servlet:javax.servlet-api:$servletApiForSpringboot1Version", - ) - testImplementation( - "junit:junit:$junitVersion", - "org.mockito:mockito-core:$mockitoVersion", - "ch.qos.logback:logback-classic:$logbackVersion", - "org.assertj:assertj-core:$assertJVersion", - "com.tngtech.java:junit-dataprovider:$junitDataproviderVersion", - "io.rest-assured:rest-assured:$restAssuredVersion", - "javax.servlet:javax.servlet-api:$servletApiForSpringboot1Version", - "org.springframework.boot:spring-boot-starter-web:$springboot1Version", - // The jaxb-api is needed for building on the java 11 JDK as these classes were moved out of the Java SE libs. - "javax.xml.bind:jaxb-api:$jaxbApiVersion", - ) -} diff --git a/backstopper-spring-boot1/src/main/java/com/nike/backstopper/handler/springboot/config/BackstopperSpringboot1Config.java b/backstopper-spring-boot1/src/main/java/com/nike/backstopper/handler/springboot/config/BackstopperSpringboot1Config.java deleted file mode 100644 index 44eba29..0000000 --- a/backstopper-spring-boot1/src/main/java/com/nike/backstopper/handler/springboot/config/BackstopperSpringboot1Config.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.nike.backstopper.handler.springboot.config; - -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.apierror.sample.SampleProjectApiErrorsBase; -import com.nike.backstopper.handler.spring.SpringApiExceptionHandler; -import com.nike.backstopper.handler.spring.SpringApiExceptionHandlerUtils; -import com.nike.backstopper.handler.spring.SpringUnhandledExceptionHandler; -import com.nike.backstopper.handler.spring.config.BackstopperSpringWebMvcConfig; -import com.nike.backstopper.handler.spring.listener.ApiExceptionHandlerListenerList; -import com.nike.backstopper.handler.springboot.controller.BackstopperSpringboot1ContainerErrorController; -import com.nike.backstopper.service.ClientDataValidationService; -import com.nike.backstopper.service.FailFastServersideValidationService; -import com.nike.backstopper.service.NoOpJsr303Validator; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; - -import javax.validation.Validator; - -/** - * This Spring Boot configuration is an alternative to simply scanning all of {@code com.nike.backstopper}. You can - * import this spring config into your main springboot config with the {@link Import} annotation to enable {@link - * SpringApiExceptionHandler} and {@link SpringUnhandledExceptionHandler} in your application. These two exception - * handlers will supersede the built-in spring exception handler chain and will translate ALL errors heading to - * the caller so that they conform to the API error contract. - * - * This also pulls in {@link BackstopperSpringboot1ContainerErrorController} to handle exceptions that originate in the - * Servlet container outside Spring proper so they can also be handled by Backstopper. See the - * {@link SpringApiExceptionHandler}, {@link SpringUnhandledExceptionHandler}, and - * {@link BackstopperSpringboot1ContainerErrorController} classes themselves for more info. - * - *

Most of the necessary dependencies are setup for autowiring so this configuration class should be sufficient - * to enable Backstopper error handling in your Spring Boot application, except for two things: - *

    - *
  1. - * Backstopper needs to know what your {@link ProjectApiErrors} is. You must expose an instance of that class - * as a dependency-injectable bean (e.g. using {@link Bean} in your Spring Boot config). See the javadocs - * for {@link ProjectApiErrors} for more information, and {@link SampleProjectApiErrorsBase} for an example base - * class that sets up all the core errors. Feel free to extend {@link SampleProjectApiErrorsBase} and use it - * directly if the error codes and messages of the core errors it provides are ok for your application). - *
  2. - *
  3. - * The {@link ClientDataValidationService} and {@link FailFastServersideValidationService} JSR 303 utility - * services need an injected reference to a {@link Validator}. If you have a JSR 303 Bean Validation - * implementation on your classpath you can just expose that (e.g. via {@link Bean}), otherwise if you - * don't need or want the functionality those services provide you can simply expose - * {@link NoOpJsr303Validator#SINGLETON_IMPL} as your {@link Validator}. Those services would then be - * useless, however if you're not going to use them anyway this would allow you to satisfy the dependency - * injection requirements without pulling in extra jars into your application just to get a {@link Validator} - * impl. - *
  4. - *
- * - *

There are a few critical extension points in Backstopper that you might want to know about for fine tuning what - * errors Backstopper knows how to handle and how your error contract looks. In particular if you want a different set - * of handler listeners for {@link SpringApiExceptionHandler} you should specify a custom {@link - * ApiExceptionHandlerListenerList} bean to override the default. And if you want to change how the final error contract - * is serialized (and/or what's inside it) for the caller you can specify a custom {@link - * SpringApiExceptionHandlerUtils}. There are other extension points for other behavior as well - Backstopper is - * designed to be customizable. - */ -@Configuration -@Import({ - BackstopperSpringWebMvcConfig.class, - BackstopperSpringboot1ContainerErrorController.class -}) -public class BackstopperSpringboot1Config { - -} diff --git a/backstopper-spring-boot1/src/main/java/com/nike/backstopper/handler/springboot/controller/BackstopperSpringboot1ContainerErrorController.java b/backstopper-spring-boot1/src/main/java/com/nike/backstopper/handler/springboot/controller/BackstopperSpringboot1ContainerErrorController.java deleted file mode 100644 index d7d06c7..0000000 --- a/backstopper-spring-boot1/src/main/java/com/nike/backstopper/handler/springboot/controller/BackstopperSpringboot1ContainerErrorController.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.nike.backstopper.handler.springboot.controller; - -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.servletapi.UnhandledServletContainerErrorHelper; - -import org.jetbrains.annotations.NotNull; -import org.springframework.boot.autoconfigure.web.ErrorController; -import org.springframework.boot.autoconfigure.web.ServerProperties; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestMapping; - -import javax.servlet.ServletRequest; - -/** - * The purpose of this controller is to give a place for the Servlet container to route errors to that would otherwise - * be served by Spring Boot's default {@code BasicErrorController}. Since this controller is handled by Spring, once - * the container forwards the request here, we can simply extract the original exception from the request attributes - * and throw it to let Backstopper handle it. We use {@link - * UnhandledServletContainerErrorHelper#extractOrGenerateErrorForRequest(ServletRequest, ProjectApiErrors)} for - * this purpose. - * - *

If this controller is registered with Spring, then {@code BasicErrorController} will not be registered, and this - * will be used for container error handling instead. - */ -@Controller -@RequestMapping("${server.error.path:${error.path:/error}}") -@SuppressWarnings("WeakerAccess") -public class BackstopperSpringboot1ContainerErrorController implements ErrorController { - - protected final @NotNull ProjectApiErrors projectApiErrors; - protected final @NotNull UnhandledServletContainerErrorHelper unhandledServletContainerErrorHelper; - protected final String errorPath; - - @SuppressWarnings("ConstantConditions") - public BackstopperSpringboot1ContainerErrorController( - @NotNull ProjectApiErrors projectApiErrors, - @NotNull UnhandledServletContainerErrorHelper unhandledServletContainerErrorHelper, - @NotNull ServerProperties serverProperties - ) { - if (projectApiErrors == null) { - throw new NullPointerException("ProjectApiErrors cannot be null."); - } - - if (unhandledServletContainerErrorHelper == null) { - throw new NullPointerException("UnhandledServletContainerErrorHelper cannot be null."); - } - - if (serverProperties == null) { - throw new NullPointerException("ServerProperties cannot be null."); - } - - this.projectApiErrors = projectApiErrors; - this.unhandledServletContainerErrorHelper = unhandledServletContainerErrorHelper; - this.errorPath = serverProperties.getError().getPath(); - } - - @RequestMapping - public void error(ServletRequest request) throws Throwable { - throw unhandledServletContainerErrorHelper.extractOrGenerateErrorForRequest(request, projectApiErrors); - } - - @Override - public String getErrorPath() { - return errorPath; - } - -} diff --git a/backstopper-spring-boot1/src/test/java/com/nike/backstopper/handler/springboot/componenttest/SanityCheckComponentTest.java b/backstopper-spring-boot1/src/test/java/com/nike/backstopper/handler/springboot/componenttest/SanityCheckComponentTest.java deleted file mode 100644 index a45d6a3..0000000 --- a/backstopper-spring-boot1/src/test/java/com/nike/backstopper/handler/springboot/componenttest/SanityCheckComponentTest.java +++ /dev/null @@ -1,322 +0,0 @@ -package com.nike.backstopper.handler.springboot.componenttest; - -import com.nike.backstopper.apierror.ApiError; -import com.nike.backstopper.apierror.ApiErrorBase; -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.apierror.projectspecificinfo.ProjectSpecificErrorCodeRange; -import com.nike.backstopper.apierror.sample.SampleCoreApiError; -import com.nike.backstopper.apierror.sample.SampleProjectApiErrorsBase; -import com.nike.backstopper.exception.ApiException; -import com.nike.backstopper.handler.springboot.config.BackstopperSpringboot1Config; -import com.nike.backstopper.model.DefaultErrorContractDTO; -import com.nike.backstopper.model.DefaultErrorDTO; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.tngtech.java.junit.dataprovider.DataProviderRunner; - -import org.junit.After; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.web.servlet.FilterRegistrationBean; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.core.Ordered; -import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.ResponseBody; -import org.springframework.web.filter.OncePerRequestFilter; - -import java.io.IOException; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -import javax.inject.Singleton; -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.validation.Validation; -import javax.validation.Validator; - -import io.restassured.response.ExtractableResponse; - -import static com.nike.backstopper.handler.springboot.componenttest.SanityCheckComponentTest.SanityCheckController.ERROR_THROWING_ENDPOINT_PATH; -import static com.nike.backstopper.handler.springboot.componenttest.SanityCheckComponentTest.SanityCheckController.NON_ERROR_ENDPOINT_PATH; -import static com.nike.backstopper.handler.springboot.componenttest.SanityCheckComponentTest.SanityCheckController.NON_ERROR_RESPONSE_PAYLOAD; -import static com.nike.internal.util.testing.TestUtils.findFreePort; -import static io.restassured.RestAssured.given; -import static java.util.Collections.singleton; -import static org.assertj.core.api.Assertions.assertThat; - -/** - * This component test is just a sanity check on the few features this module provides above and beyond the regular - * backstopper+spring stuff. The samples and testonly modules have more comprehensive component tests. - * - * @author Nic Munroe - */ -@RunWith(DataProviderRunner.class) -public class SanityCheckComponentTest { - - private static final int SERVER_PORT = findFreePort(); - private static ConfigurableApplicationContext serverAppContext; - - private static final ObjectMapper objectMapper = new ObjectMapper(); - - @BeforeClass - public static void beforeClass() { - serverAppContext = SpringApplication.run(SanitcyCheckComponentTestApp.class, "--server.port=" + SERVER_PORT); - } - - @AfterClass - public static void afterClass() { - SpringApplication.exit(serverAppContext); - } - - @Before - public void beforeMethod() { - } - - @After - public void afterMethod() { - } - - private void verifyErrorReceived(ExtractableResponse response, ApiError expectedError) { - verifyErrorReceived(response, singleton(expectedError), expectedError.getHttpStatusCode()); - } - - private DefaultErrorDTO findErrorMatching(DefaultErrorContractDTO errorContract, ApiError desiredError) { - for (DefaultErrorDTO error : errorContract.errors) { - if (error.code.equals(desiredError.getErrorCode()) && error.message.equals(desiredError.getMessage())) - return error; - } - - return null; - } - - private void verifyErrorReceived(ExtractableResponse response, Collection expectedErrors, int expectedHttpStatusCode) { - assertThat(response.statusCode()).isEqualTo(expectedHttpStatusCode); - try { - DefaultErrorContractDTO errorContract = objectMapper.readValue(response.asString(), DefaultErrorContractDTO.class); - assertThat(errorContract.error_id).isNotEmpty(); - assertThat(UUID.fromString(errorContract.error_id)).isNotNull(); - assertThat(errorContract.errors).hasSameSizeAs(expectedErrors); - for (ApiError apiError : expectedErrors) { - DefaultErrorDTO matchingError = findErrorMatching(errorContract, apiError); - assertThat(matchingError).isNotNull(); - assertThat(matchingError.code).isEqualTo(apiError.getErrorCode()); - assertThat(matchingError.message).isEqualTo(apiError.getMessage()); - assertThat(matchingError.metadata).isEqualTo(apiError.getMetadata()); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Test - public void verify_non_error_endpoint_responds_without_error() { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .basePath(NON_ERROR_ENDPOINT_PATH) - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - assertThat(response.statusCode()).isEqualTo(200); - assertThat(response.asString()).isEqualTo(NON_ERROR_RESPONSE_PAYLOAD); - } - - @Test - public void verify_ENDPOINT_ERROR_returned_if_error_endpoint_is_called() { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .basePath(ERROR_THROWING_ENDPOINT_PATH) - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SanityCheckProjectApiError.ENDPOINT_ERROR); - } - - @Test - public void verify_NOT_FOUND_returned_if_unknown_path_is_requested() { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .basePath(UUID.randomUUID().toString()) - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleCoreApiError.NOT_FOUND); - } - - @Test - public void verify_ERROR_THROWN_IN_SERVLET_FILTER_OUTSIDE_SPRING_returned_if_servlet_filter_trigger_occurs() { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .basePath(NON_ERROR_ENDPOINT_PATH) - .header("throw-servlet-filter-exception", "true") - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SanityCheckProjectApiError.ERROR_THROWN_IN_SERVLET_FILTER_OUTSIDE_SPRING); - } - - @SpringBootApplication - @Configuration - @Import({ BackstopperSpringboot1Config.class, SanityCheckController.class }) - static class SanitcyCheckComponentTestApp { - @Bean - public ProjectApiErrors getProjectApiErrors() { - return new SanityCheckProjectApiErrorsImpl(); - } - - @Bean - public Validator getJsr303Validator() { - return Validation.buildDefaultValidatorFactory().getValidator(); - } - - @Bean - public FilterRegistrationBean explodingServletFilter() { - FilterRegistrationBean frb = new FilterRegistrationBean(new ExplodingFilter()); - frb.setOrder(Ordered.HIGHEST_PRECEDENCE); - return frb; - } - - public static class ExplodingFilter extends OncePerRequestFilter { - - @Override - protected void doFilterInternal( - HttpServletRequest request, HttpServletResponse response, FilterChain filterChain - ) throws ServletException, IOException { - if ("true".equals(request.getHeader("throw-servlet-filter-exception"))) { - throw ApiException - .newBuilder() - .withApiErrors(SanityCheckProjectApiError.ERROR_THROWN_IN_SERVLET_FILTER_OUTSIDE_SPRING) - .withExceptionMessage("Exception thrown from Servlet Filter outside Spring") - .build(); - } - filterChain.doFilter(request, response); - } - - } - } - - @Controller - static class SanityCheckController { - public static final String NON_ERROR_ENDPOINT_PATH = "/nonErrorEndpoint"; - public static final String ERROR_THROWING_ENDPOINT_PATH = "/throwErrorEndpoint"; - - public static final String NON_ERROR_RESPONSE_PAYLOAD = UUID.randomUUID().toString(); - - @GetMapping(NON_ERROR_ENDPOINT_PATH) - @ResponseBody - public String nonErrorEndpoint() { - return NON_ERROR_RESPONSE_PAYLOAD; - } - - @GetMapping(ERROR_THROWING_ENDPOINT_PATH) - public void throwErrorEndpoint() { - throw new ApiException(SanityCheckProjectApiError.ENDPOINT_ERROR); - } - } - - enum SanityCheckProjectApiError implements ApiError { - ENDPOINT_ERROR(99100, "An error was thrown in the endpoint", HttpStatus.BAD_REQUEST.value()), - ERROR_THROWN_IN_SERVLET_FILTER_OUTSIDE_SPRING( - 99150, "An error occurred in a Servlet Filter outside Spring", HttpStatus.INTERNAL_SERVER_ERROR.value() - ); - - private final ApiError delegate; - - SanityCheckProjectApiError(ApiError delegate) { - this.delegate = delegate; - } - - - SanityCheckProjectApiError(int errorCode, String message, int httpStatusCode) { - this(new ApiErrorBase( - "delegated-to-enum-wrapper-" + UUID.randomUUID().toString(), errorCode, message, httpStatusCode - )); - } - - @Override - public String getName() { - return this.name(); - } - - @Override - public String getErrorCode() { - return delegate.getErrorCode(); - } - - @Override - public String getMessage() { - return delegate.getMessage(); - } - - @Override - public int getHttpStatusCode() { - return delegate.getHttpStatusCode(); - } - - @Override - public Map getMetadata() { - return delegate.getMetadata(); - } - - } - - @Singleton - static class SanityCheckProjectApiErrorsImpl extends SampleProjectApiErrorsBase { - - private static final List projectSpecificApiErrors = - Arrays.asList(SanityCheckProjectApiError.values()); - - // Set the valid range of non-core error codes for this project to be 99100-99200. - private static final ProjectSpecificErrorCodeRange errorCodeRange = - ProjectSpecificErrorCodeRange.ALLOW_ALL_ERROR_CODES; - - @Override - protected List getProjectSpecificApiErrors() { - return projectSpecificApiErrors; - } - - @Override - protected ProjectSpecificErrorCodeRange getProjectSpecificErrorCodeRange() { - return errorCodeRange; - } - - } -} diff --git a/backstopper-spring-boot1/src/test/java/com/nike/backstopper/handler/springboot/config/BackstopperSpringboot1ConfigTest.java b/backstopper-spring-boot1/src/test/java/com/nike/backstopper/handler/springboot/config/BackstopperSpringboot1ConfigTest.java deleted file mode 100644 index d3148f0..0000000 --- a/backstopper-spring-boot1/src/test/java/com/nike/backstopper/handler/springboot/config/BackstopperSpringboot1ConfigTest.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.nike.backstopper.handler.springboot.config; - -import org.junit.Test; - -/** - * Tests the functionality of {@link BackstopperSpringboot1Config}. - * - * @author Nic Munroe - */ -public class BackstopperSpringboot1ConfigTest { - - @Test - public void code_coverage_hoops() { - // jump! - new BackstopperSpringboot1Config(); - } - -} \ No newline at end of file diff --git a/backstopper-spring-boot1/src/test/java/com/nike/backstopper/handler/springboot/controller/BackstopperSpringboot1ContainerErrorControllerTest.java b/backstopper-spring-boot1/src/test/java/com/nike/backstopper/handler/springboot/controller/BackstopperSpringboot1ContainerErrorControllerTest.java deleted file mode 100644 index 5f826a0..0000000 --- a/backstopper-spring-boot1/src/test/java/com/nike/backstopper/handler/springboot/controller/BackstopperSpringboot1ContainerErrorControllerTest.java +++ /dev/null @@ -1,126 +0,0 @@ -package com.nike.backstopper.handler.springboot.controller; - -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.servletapi.UnhandledServletContainerErrorHelper; - -import org.junit.Before; -import org.junit.Test; -import org.springframework.boot.autoconfigure.web.ErrorProperties; -import org.springframework.boot.autoconfigure.web.ServerProperties; - -import java.util.UUID; - -import javax.servlet.ServletRequest; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.catchThrowable; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -/** - * Tests the functionality of {@link BackstopperSpringboot1ContainerErrorController}. - * - * @author Nic Munroe - */ -public class BackstopperSpringboot1ContainerErrorControllerTest { - - private ProjectApiErrors projectApiErrorsMock; - private UnhandledServletContainerErrorHelper unhandledContainerErrorHelperMock; - private ServletRequest servletRequestMock; - private ServerProperties serverPropertiesMock; - private ErrorProperties errorPropertiesMock; - private String errorPath; - - @Before - public void beforeMethod() { - projectApiErrorsMock = mock(ProjectApiErrors.class); - unhandledContainerErrorHelperMock = mock(UnhandledServletContainerErrorHelper.class); - servletRequestMock = mock(ServletRequest.class); - serverPropertiesMock = mock(ServerProperties.class); - errorPropertiesMock = mock(ErrorProperties.class); - errorPath = UUID.randomUUID().toString(); - - doReturn(errorPropertiesMock).when(serverPropertiesMock).getError(); - doReturn(errorPath).when(errorPropertiesMock).getPath(); - } - - @Test - public void constructor_sets_fields_as_expected() { - // when - BackstopperSpringboot1ContainerErrorController impl = new BackstopperSpringboot1ContainerErrorController( - projectApiErrorsMock, unhandledContainerErrorHelperMock, serverPropertiesMock - ); - - // then - assertThat(impl.projectApiErrors).isSameAs(projectApiErrorsMock); - assertThat(impl.unhandledServletContainerErrorHelper).isSameAs(unhandledContainerErrorHelperMock); - assertThat(impl.errorPath).isEqualTo(errorPath); - assertThat(impl.getErrorPath()).isEqualTo(errorPath); - } - - @Test - public void constructor_throws_NPE_if_passed_null_ProjectApiErrors() { - // when - Throwable ex = catchThrowable( - () -> new BackstopperSpringboot1ContainerErrorController( - null, unhandledContainerErrorHelperMock, serverPropertiesMock - ) - ); - - // then - assertThat(ex) - .isInstanceOf(NullPointerException.class) - .hasMessage("ProjectApiErrors cannot be null."); - } - - @Test - public void constructor_throws_NPE_if_passed_null_UnhandledServletContainerErrorHelper() { - // when - Throwable ex = catchThrowable( - () -> new BackstopperSpringboot1ContainerErrorController( - projectApiErrorsMock, null, serverPropertiesMock - ) - ); - - // then - assertThat(ex) - .isInstanceOf(NullPointerException.class) - .hasMessage("UnhandledServletContainerErrorHelper cannot be null."); - } - - @Test - public void constructor_throws_NPE_if_passed_null_ServerProperties() { - // when - Throwable ex = catchThrowable( - () -> new BackstopperSpringboot1ContainerErrorController( - projectApiErrorsMock, unhandledContainerErrorHelperMock, null - ) - ); - - // then - assertThat(ex) - .isInstanceOf(NullPointerException.class) - .hasMessage("ServerProperties cannot be null."); - } - - @Test - public void error_method_throws_result_of_calling_UnhandledServletContainerErrorHelper() { - // given - BackstopperSpringboot1ContainerErrorController impl = new BackstopperSpringboot1ContainerErrorController( - projectApiErrorsMock, unhandledContainerErrorHelperMock, serverPropertiesMock - ); - Throwable expectedEx = new RuntimeException("intentional test exception"); - doReturn(expectedEx).when(unhandledContainerErrorHelperMock) - .extractOrGenerateErrorForRequest(servletRequestMock, projectApiErrorsMock); - - // when - Throwable ex = catchThrowable(() -> impl.error(servletRequestMock)); - - // then - assertThat(ex).isSameAs(expectedEx); - verify(unhandledContainerErrorHelperMock) - .extractOrGenerateErrorForRequest(servletRequestMock, projectApiErrorsMock); - } - -} diff --git a/backstopper-spring-boot1/src/test/resources/logback.xml b/backstopper-spring-boot1/src/test/resources/logback.xml deleted file mode 100644 index 80adb28..0000000 --- a/backstopper-spring-boot1/src/test/resources/logback.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - %d{yyyy-MM-dd_HH:mm:ss.SSS} [%thread] |-%-5level %logger{36} - %msg%n - - - - - - - \ No newline at end of file diff --git a/samples/sample-spring-boot1/README.md b/samples/sample-spring-boot1/README.md deleted file mode 100644 index 5754d5c..0000000 --- a/samples/sample-spring-boot1/README.md +++ /dev/null @@ -1,70 +0,0 @@ -# Backstopper Sample Application - spring-boot1 - -Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 7 and greater. - -This submodule contains a sample application based on Spring Boot 1 that fully integrates Backstopper. - -* Build the sample by running the `./buildSample.sh` script. -* Launch the sample by running the `./runSample.sh` script. It will bind to port 8080 by default. - * You can override the default port by passing in a system property to the run script, - e.g. to bind to port 8181: `./runSample.sh -Dserver.port=8181` - -## Things to try - -All examples here assume the sample app is running on port 8080, so you would hit each path by going to -`http://localhost:8080/[endpoint-path]`. It's recommended that you use a REST client like -[Postman](https://www.getpostman.com/) for making the requests so you can easily specify HTTP method, payloads, -headers, etc, and fully inspect the response. - -Also note that all the following things to try are verified in a component test: -`VerifyExpectedErrorsAreReturnedComponentTest`. If you prefer to experiment via code you can run, debug, and otherwise -explore that test. - -As you are doing the following you should check the logs that are output by the sample application and notice what is -included in the log messages. In particular notice how you can search for an `error_id` that came from an error -response and go directly to the relevant log message in the logs. Also notice how the `ApiError.getName()` value shows -up in the logs for each error represented in a returned error contract (there can be more than one per request). - -* `GET /sample` - Returns the JSON serialization for the `SampleModel` model object. You can copy this into a `POST` -call to experiment with triggering errors. -* `POST /sample` with `ContentType: application/json` header - Using the JSON model retrieved by the `GET` call, you -can trigger numerous different types of errors, all of which get caught by the Backstopper system and converted into -the appropriate error contract. - * Omit the `foo` field. - * Set the value of the `range_0_to_42` field to something outside of the allowed 0-42 range. - * Set the value of the `rgb_color` field to something besides `RED`, `GREEN`, or `BLUE`, or omit it entirely. - Note that the validation and deserialization of this enum field is done in a case insensitive manner - i.e. you - can pass `red`, `Green`, or `bLuE` if you want and it will not throw an error. - * Set two or more invalid values for `foo`, `range_0_to_42`, and `rgb_color` to invalid values all at once - notice - you get back all relevant errors at once in the same error contract. - * Set `throw_manual_error` to true to trigger a manual exception to be thrown inside the normal `POST /sample` - endpoint. - * Note the extra response headers that are included when you do this, and how they relate to the - `.withExtraResponseHeaders(...)` method call on the builder of the exception that is thrown. - * Pass in an empty JSON payload - you should receive a `"Missing expected content"` error back. - * Pass in a junk payload that is not valid JSON - you should receive a `"Malformed request"` error back. -* `GET /sample/coreErrorWrapper` - Triggers an error to be thrown that appears to the caller like a normal generic -service exception, but the `SOME_MEANINGFUL_ERROR_NAME` name from the `ApiError` it represents shows up in the logs to -help you disambiguate what the true cause was. -* `GET /sample/triggerUnhandledError` - Triggers an error that is caught by the unhandled exception handler portion of -Backstopper and converted to a generic service exception. -* `GET /sample/withRequiredQueryParam?requiredQueryParamValue=not-an-int` - Triggers an error in the Spring Boot -framework when it cannot coerce the query param value to the required type (an integer), which results in a -Backstopper `"Type conversion error"`. -* `GET /does-not-exist` - Triggers a framework 404 which Backstopper handles. -* `DELETE /sample` - Triggers a framework 405 which Backstopper handles. -* `GET /sample` with `Accept: application/octet-stream` header - Triggers a framework 406 which Backstopper handles. -* `POST /sample` with `ContentType: text/plain` - Triggers a framework 415 which Backstopper handles. -* Any request with a `throw-servlet-filter-exception` header set to `true`. This will trigger an exception in a -Servlet filter before the request ever hits Spring. The `BackstopperSpringboot1ContainerErrorController` -from the `backstopper-spring-boot1` dependency registers itself to handle these non-Spring container errors, where it -is passed off to Backstopper. - -## More Info - -See the [base project README.md](../../README.md), [User Guide](../../USER_GUIDE.md), and Backstopper repository -source code and javadocs for all further information. - -## License - -Backstopper is released under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/samples/sample-spring-boot1/build.gradle b/samples/sample-spring-boot1/build.gradle deleted file mode 100644 index c7d10e9..0000000 --- a/samples/sample-spring-boot1/build.gradle +++ /dev/null @@ -1,46 +0,0 @@ -evaluationDependsOn(':') - -sourceCompatibility = JavaVersion.VERSION_1_8 -targetCompatibility = JavaVersion.VERSION_1_8 - -ext { - // Springboot requires Servlet API to be at least version 3.1 - servletApiForTestsVersion = '3.1.0' -} - -test { - useJUnitPlatform() -} - -dependencies { - implementation( - project(":backstopper-spring-boot1"), - project(":backstopper-custom-validators"), - "ch.qos.logback:logback-classic:$logbackVersion", - "org.hibernate:hibernate-validator:$hibernateValidatorVersion", - "org.springframework.boot:spring-boot-starter-web:$springboot1Version", - "javax.servlet:javax.servlet-api:$servletApiForTestsVersion", - ) - compileOnly( - "org.jetbrains:annotations:$jetbrainsAnnotationsVersion" - ) - testImplementation( - project(":backstopper-reusable-tests-junit5"), - "org.junit.jupiter:junit-jupiter-api:$junit5Version", - "org.junit.jupiter:junit-jupiter-engine:$junit5Version", - "org.junit.jupiter:junit-jupiter-params:$junit5Version", - "org.mockito:mockito-core:$mockitoVersion", - "ch.qos.logback:logback-classic:$logbackVersion", - "org.assertj:assertj-core:$assertJVersion", - "io.rest-assured:rest-assured:$restAssuredVersion", - // The jaxb-api is needed for building on the java 11 JDK as these classes were moved out of the Java SE libs. - "javax.xml.bind:jaxb-api:$jaxbApiVersion", - ) -} - -apply plugin: "application" -mainClassName = "com.nike.backstopper.springbootsample.Main" - -run { - systemProperties = System.getProperties() -} diff --git a/samples/sample-spring-boot1/buildSample.sh b/samples/sample-spring-boot1/buildSample.sh deleted file mode 100755 index 2effeba..0000000 --- a/samples/sample-spring-boot1/buildSample.sh +++ /dev/null @@ -1,2 +0,0 @@ -echo "../../gradlew clean build" -../../gradlew clean build \ No newline at end of file diff --git a/samples/sample-spring-boot1/runSample.sh b/samples/sample-spring-boot1/runSample.sh deleted file mode 100755 index 65ac084..0000000 --- a/samples/sample-spring-boot1/runSample.sh +++ /dev/null @@ -1,3 +0,0 @@ -echo "../../gradlew run" -echo "NOTE: Type ctrl+c to stop" -../../gradlew run $* \ No newline at end of file diff --git a/samples/sample-spring-boot1/src/main/java/com/nike/backstopper/springbootsample/Main.java b/samples/sample-spring-boot1/src/main/java/com/nike/backstopper/springbootsample/Main.java deleted file mode 100644 index c68869e..0000000 --- a/samples/sample-spring-boot1/src/main/java/com/nike/backstopper/springbootsample/Main.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.nike.backstopper.springbootsample; - -import com.nike.backstopper.springbootsample.config.SampleSpringboot1SpringConfig; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.context.annotation.Import; - -/** - * Starts up the Backstopper Spring Boot 1 Sample server (on port 8080 by default). - * - * @author Nic Munroe - */ -@SpringBootApplication -@Import(SampleSpringboot1SpringConfig.class) -public class Main { - public static void main(String[] args) { - SpringApplication.run(Main.class, args); - } -} diff --git a/samples/sample-spring-boot1/src/main/java/com/nike/backstopper/springbootsample/config/SampleSpringboot1SpringConfig.java b/samples/sample-spring-boot1/src/main/java/com/nike/backstopper/springbootsample/config/SampleSpringboot1SpringConfig.java deleted file mode 100644 index 91b4d8b..0000000 --- a/samples/sample-spring-boot1/src/main/java/com/nike/backstopper/springbootsample/config/SampleSpringboot1SpringConfig.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.nike.backstopper.springbootsample.config; - -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.exception.ApiException; -import com.nike.backstopper.handler.springboot.config.BackstopperSpringboot1Config; -import com.nike.backstopper.handler.springboot.controller.BackstopperSpringboot1ContainerErrorController; -import com.nike.backstopper.springbootsample.error.SampleProjectApiError; -import com.nike.backstopper.springbootsample.error.SampleProjectApiErrorsImpl; - -import org.springframework.boot.web.servlet.FilterRegistrationBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.core.Ordered; -import org.springframework.web.filter.OncePerRequestFilter; - -import java.io.IOException; - -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.validation.Validation; -import javax.validation.Validator; - -/** - * Simple Spring Boot config for the sample app. The {@link ProjectApiErrors} and {@link Validator} beans defined - * in this class are needed for autowiring Backstopper and the {@link ProjectApiErrors} in particular allows you - * to specify project-specific errors and behaviors. - * - *

NOTE: This integrates Backstopper by {@link Import}ing {@link BackstopperSpringboot1Config}. Alternatively, - * you could integrate Backstopper by component scanning all of the {@code com.nike.backstopper} package and its - * subpackages, e.g. by annotating with - * {@link org.springframework.context.annotation.ComponentScan @ComponentScan(basePackages = "com.nike.backstopper")}. - * - * @author Nic Munroe - */ -@Configuration -@Import(BackstopperSpringboot1Config.class) -// Instead of @Import(BackstopperSpringboot1Config.class), you could component scan the com.nike.backstopper -// package like this if you prefer component scanning: @ComponentScan(basePackages = "com.nike.backstopper") -public class SampleSpringboot1SpringConfig { - - /** - * @return The {@link ProjectApiErrors} to use for this sample app. - */ - @Bean - public ProjectApiErrors getProjectApiErrors() { - return new SampleProjectApiErrorsImpl(); - } - - /** - * NOTE: Spring uses its own system for JSR 303 validation, so this {@code @Bean} is only here to satisfy the - * dependency injection requirements of {@link com.nike.backstopper.service.ClientDataValidationService} and - * {@link com.nike.backstopper.service.FailFastServersideValidationService}. With this {@link Validator} defined - * you could inject one of those services into your controllers and use it as advertised. If you were never going - * to use those services you could have this return {@link - * com.nike.backstopper.service.NoOpJsr303Validator#SINGLETON_IMPL} instead and not have to pull in any JSR 303 - * implementation dependency into your project. - */ - @Bean - public Validator getJsr303Validator() { - return Validation.buildDefaultValidatorFactory().getValidator(); - } - - /** - * Registers a custom {@link ExplodingFilter} Servlet filter at the highest precedence that will throw an exception - * when the request contains a special header. This exception will be thrown outside of Springboot, and can - * be used to exercise the {@link BackstopperSpringboot1ContainerErrorController}. You wouldn't want this in - * a real app. - */ - @Bean - public FilterRegistrationBean explodingServletFilter() { - FilterRegistrationBean frb = new FilterRegistrationBean(new ExplodingFilter()); - frb.setOrder(Ordered.HIGHEST_PRECEDENCE); - return frb; - } - - private static class ExplodingFilter extends OncePerRequestFilter { - - @Override - protected void doFilterInternal( - HttpServletRequest request, HttpServletResponse response, FilterChain filterChain - ) throws ServletException, IOException { - if ("true".equals(request.getHeader("throw-servlet-filter-exception"))) { - throw ApiException - .newBuilder() - .withApiErrors(SampleProjectApiError.ERROR_THROWN_IN_SERVLET_FILTER_OUTSIDE_SPRING) - .withExceptionMessage("Exception thrown from Servlet Filter outside Spring") - .build(); - } - filterChain.doFilter(request, response); - } - - } -} diff --git a/samples/sample-spring-boot1/src/main/java/com/nike/backstopper/springbootsample/controller/SampleController.java b/samples/sample-spring-boot1/src/main/java/com/nike/backstopper/springbootsample/controller/SampleController.java deleted file mode 100644 index 53c358d..0000000 --- a/samples/sample-spring-boot1/src/main/java/com/nike/backstopper/springbootsample/controller/SampleController.java +++ /dev/null @@ -1,112 +0,0 @@ -package com.nike.backstopper.springbootsample.controller; - -import com.nike.backstopper.exception.ApiException; -import com.nike.backstopper.service.ClientDataValidationService; -import com.nike.backstopper.springbootsample.error.SampleProjectApiError; -import com.nike.backstopper.springbootsample.model.RgbColor; -import com.nike.backstopper.springbootsample.model.SampleModel; -import com.nike.internal.util.Pair; - -import org.springframework.http.HttpStatus; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseBody; -import org.springframework.web.bind.annotation.ResponseStatus; - -import java.util.Arrays; -import java.util.UUID; - -import javax.validation.Valid; - -import static com.nike.backstopper.springbootsample.controller.SampleController.SAMPLE_PATH; -import static java.util.Collections.singletonList; - -/** - * Contains some sample endpoints. In particular {@link #postSampleModel(SampleModel)} is useful for showing the - * JSR 303 Bean Validation integration in Backstopper - see that method's javadocs and source code for more info. - * - *

The {@code VerifyExpectedErrorsAreReturnedComponentTest} component test launches the server and exercises - * all these endpoints in various ways to verify the expected errors are returned using the expected error contract. - */ -@Controller -@RequestMapping(SAMPLE_PATH) -@SuppressWarnings({"unused", "WeakerAccess"}) -public class SampleController { - - public static final String SAMPLE_PATH = "/sample"; - public static final String CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH = "/coreErrorWrapper"; - public static final String WITH_REQUIRED_QUERY_PARAM_SUBPATH = "/withRequiredQueryParam"; - public static final String TRIGGER_UNHANDLED_ERROR_SUBPATH = "/triggerUnhandledError"; - - public static int nextRangeInt(int lowerBound, int upperBound) { - return (int)Math.round(Math.random() * upperBound) + lowerBound; - } - - public static RgbColor nextRandomColor() { - return RgbColor.values()[nextRangeInt(0, 2)]; - } - - @GetMapping(produces = "application/json") - @ResponseBody - public SampleModel getSampleModel() { - return new SampleModel( - UUID.randomUUID().toString(), String.valueOf(nextRangeInt(0, 42)), nextRandomColor().name(), false - ); - } - - /** - * Note how the {@link Valid} annotation causes Spring Boot to run the given request payload object through - * JSR 303 validation after deserialization and before calling this method. Alternatively you could omit the - * {@code @Valid} annotation, inject a {@link ClientDataValidationService} into this class, and call - * {@link ClientDataValidationService#validateObjectsFailFast(Object...)} passing in the request payload object - - * if it fails validation an appropriate exception would be thrown immediately. Both solutions are - * functionally equivalent. - * - *

In this simple case the {@code @Valid} annotation is easier and simpler, but there are some more complex - * use cases where using {@link ClientDataValidationService} yourself ends up being a better (or the only) - * solution. - */ - @PostMapping(consumes = "application/json", produces = "application/json") - @ResponseBody - @ResponseStatus(HttpStatus.CREATED) - public SampleModel postSampleModel(@Valid @RequestBody SampleModel model) { - // Manually check the throwManualError query param (normally you'd do this with JSR 303 annotations on the - // object, but this shows how you can manually throw exceptions to be picked up by the error handling system). - if (Boolean.TRUE.equals(model.throw_manual_error)) { - throw ApiException.newBuilder() - .withExceptionMessage("Manual error throw was requested") - .withApiErrors(SampleProjectApiError.MANUALLY_THROWN_ERROR) - .withExtraDetailsForLogging(Pair.of("rgb_color_value", model.rgb_color)) - .withExtraResponseHeaders( - Pair.of("rgbColorValue", singletonList(model.rgb_color)), - Pair.of("otherExtraMultivalueHeader", Arrays.asList("foo", "bar")) - ) - .build(); - } - - return model; - } - - @GetMapping(path = CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH) - public void failWithCoreErrorWrapper() { - throw ApiException.newBuilder() - .withExceptionMessage("Throwing error due to 'reasons'") - .withApiErrors(SampleProjectApiError.SOME_MEANINGFUL_ERROR_NAME) - .build(); - } - - @GetMapping(path = WITH_REQUIRED_QUERY_PARAM_SUBPATH, produces = "text/plain") - @ResponseBody - public String withRequiredQueryParam(@RequestParam(name = "requiredQueryParamValue") int someRequiredQueryParam) { - return "You passed in " + someRequiredQueryParam + " for the required query param value"; - } - - @GetMapping(path = TRIGGER_UNHANDLED_ERROR_SUBPATH) - public void triggerUnhandledError() { - throw new RuntimeException("This should be handled by SpringUnhandledExceptionHandler."); - } -} diff --git a/samples/sample-spring-boot1/src/main/java/com/nike/backstopper/springbootsample/error/SampleProjectApiError.java b/samples/sample-spring-boot1/src/main/java/com/nike/backstopper/springbootsample/error/SampleProjectApiError.java deleted file mode 100644 index 1d27b7e..0000000 --- a/samples/sample-spring-boot1/src/main/java/com/nike/backstopper/springbootsample/error/SampleProjectApiError.java +++ /dev/null @@ -1,94 +0,0 @@ -package com.nike.backstopper.springbootsample.error; - -import com.nike.backstopper.apierror.ApiError; -import com.nike.backstopper.apierror.ApiErrorBase; -import com.nike.backstopper.apierror.ApiErrorWithMetadata; -import com.nike.backstopper.apierror.sample.SampleCoreApiError; -import com.nike.backstopper.springbootsample.model.RgbColor; -import com.nike.internal.util.MapBuilder; - -import org.springframework.http.HttpStatus; - -import java.util.Arrays; -import java.util.Map; -import java.util.UUID; - -/** - * Project-specific error definitions for this sample app. Note that the error codes for errors specified here must - * conform to the range specified in {@link SampleProjectApiErrorsImpl#getProjectSpecificErrorCodeRange()} or an - * exception will be thrown on app startup, and unit tests should fail. The one exception to this rule is a "core - * error wrapper" - an instance that shares the same error code, message, and HTTP status code as a - * {@link SampleProjectApiErrorsImpl#getCoreApiErrors()} instance (in this case that means a wrapper around - * {@link SampleCoreApiError}). - * - * @author Nic Munroe - */ -public enum SampleProjectApiError implements ApiError { - FIELD_CANNOT_BE_NULL_OR_BLANK(99100, "Field cannot be null or empty", HttpStatus.BAD_REQUEST.value()), - // FOO_STRING_CANNOT_BE_BLANK shows how you can build off a base/generic error and add metadata. - FOO_STRING_CANNOT_BE_BLANK(FIELD_CANNOT_BE_NULL_OR_BLANK, MapBuilder.builder("field", (Object)"foo").build()), - INVALID_RANGE_VALUE(99110, "The range_0_to_42 field must be between 0 and 42 (inclusive)", - HttpStatus.BAD_REQUEST.value()), - // RGB_COLOR_CANNOT_BE_NULL could build off FIELD_CANNOT_BE_NULL_OR_BLANK like FOO_STRING_CANNOT_BE_BLANK does, - // however this shows how you can make individual field errors with unique code and custom message. - RGB_COLOR_CANNOT_BE_NULL(99120, "The rgb_color field must be defined", HttpStatus.BAD_REQUEST.value()), - NOT_RGB_COLOR_ENUM(99130, "The rgb_color field value must be one of: " + Arrays.toString(RgbColor.values()), - HttpStatus.BAD_REQUEST.value()), - MANUALLY_THROWN_ERROR(99140, "You asked for an error to be thrown", HttpStatus.INTERNAL_SERVER_ERROR.value()), - // This is a wrapper around a core error. It will have the same error code, message, and HTTP status code, - // but will show up in the logs with contributing_errors="SOME_MEANINGFUL_ERROR_NAME", allowing you to - // distinguish the context of the error vs. the core GENERIC_SERVICE_ERROR at a glance. - SOME_MEANINGFUL_ERROR_NAME(SampleCoreApiError.GENERIC_SERVICE_ERROR), - ERROR_THROWN_IN_SERVLET_FILTER_OUTSIDE_SPRING( - 99150, "An error occurred in a Servlet Filter outside Spring", HttpStatus.INTERNAL_SERVER_ERROR.value() - ); - - private final ApiError delegate; - - SampleProjectApiError(ApiError delegate) { - this.delegate = delegate; - } - - SampleProjectApiError(ApiError delegate, Map metadata) { - this(new ApiErrorWithMetadata(delegate, metadata)); - } - - SampleProjectApiError(int errorCode, String message, int httpStatusCode) { - this(new ApiErrorBase( - "delegated-to-enum-wrapper-" + UUID.randomUUID().toString(), errorCode, message, httpStatusCode - )); - } - - @SuppressWarnings("unused") - SampleProjectApiError(int errorCode, String message, int httpStatusCode, Map metadata) { - this(new ApiErrorBase( - "delegated-to-enum-wrapper-" + UUID.randomUUID().toString(), errorCode, message, httpStatusCode, metadata - )); - } - - @Override - public String getName() { - return this.name(); - } - - @Override - public String getErrorCode() { - return delegate.getErrorCode(); - } - - @Override - public String getMessage() { - return delegate.getMessage(); - } - - @Override - public int getHttpStatusCode() { - return delegate.getHttpStatusCode(); - } - - @Override - public Map getMetadata() { - return delegate.getMetadata(); - } - -} \ No newline at end of file diff --git a/samples/sample-spring-boot1/src/main/java/com/nike/backstopper/springbootsample/error/SampleProjectApiErrorsImpl.java b/samples/sample-spring-boot1/src/main/java/com/nike/backstopper/springbootsample/error/SampleProjectApiErrorsImpl.java deleted file mode 100644 index 3c6494c..0000000 --- a/samples/sample-spring-boot1/src/main/java/com/nike/backstopper/springbootsample/error/SampleProjectApiErrorsImpl.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.nike.backstopper.springbootsample.error; - -import com.nike.backstopper.apierror.ApiError; -import com.nike.backstopper.apierror.projectspecificinfo.ProjectSpecificErrorCodeRange; -import com.nike.backstopper.apierror.projectspecificinfo.ProjectSpecificErrorCodeRangeIntegerImpl; -import com.nike.backstopper.apierror.sample.SampleProjectApiErrorsBase; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import javax.inject.Singleton; - -/** - * Returns the project specific errors for this sample application. {@link #getProjectApiErrors()} will return a - * combination of {@link SampleProjectApiErrorsBase#getCoreApiErrors()} and {@link #getProjectSpecificApiErrors()}. - * This means that you have all the enum values of {@link com.nike.backstopper.apierror.sample.SampleCoreApiError} - * and {@link SampleProjectApiError} at your disposal when throwing errors in this sample app. - */ -@Singleton -public class SampleProjectApiErrorsImpl extends SampleProjectApiErrorsBase { - - private static final List projectSpecificApiErrors = - new ArrayList<>(Arrays.asList(SampleProjectApiError.values())); - - // Set the valid range of non-core error codes for this project to be 99100-99200. - private static final ProjectSpecificErrorCodeRange errorCodeRange = new ProjectSpecificErrorCodeRangeIntegerImpl( - 99100, 99200, "SAMPLE_PROJECT_API_ERRORS" - ); - - @Override - protected List getProjectSpecificApiErrors() { - return projectSpecificApiErrors; - } - - @Override - protected ProjectSpecificErrorCodeRange getProjectSpecificErrorCodeRange() { - return errorCodeRange; - } - -} diff --git a/samples/sample-spring-boot1/src/main/java/com/nike/backstopper/springbootsample/model/RgbColor.java b/samples/sample-spring-boot1/src/main/java/com/nike/backstopper/springbootsample/model/RgbColor.java deleted file mode 100644 index 29fd421..0000000 --- a/samples/sample-spring-boot1/src/main/java/com/nike/backstopper/springbootsample/model/RgbColor.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.nike.backstopper.springbootsample.model; - -import com.fasterxml.jackson.annotation.JsonCreator; - -/** - * An enum used by {@link SampleModel} for showing how - * {@link com.nike.backstopper.validation.constraints.StringConvertsToClassType} can work with enums. Note - * the {@link #toRgbColor(String)} annotated with {@link JsonCreator}, which allows callers to pass in lower or - * mixed case versions of the enum values and still have them automatically deserialized to the correct enum. - * This special {@link JsonCreator} method is only necessary if you want to support case-insensitive enum validation - * when deserializing. - */ -public enum RgbColor { - RED, GREEN, BLUE; - - @JsonCreator - @SuppressWarnings("unused") - public static RgbColor toRgbColor(String colorString) { - for (RgbColor color : values()) { - if (color.name().equalsIgnoreCase(colorString)) - return color; - } - throw new IllegalArgumentException( - "Cannot convert the string: \"" + colorString + "\" to a valid RgbColor enum value." - ); - } -} diff --git a/samples/sample-spring-boot1/src/main/java/com/nike/backstopper/springbootsample/model/SampleModel.java b/samples/sample-spring-boot1/src/main/java/com/nike/backstopper/springbootsample/model/SampleModel.java deleted file mode 100644 index 5944e16..0000000 --- a/samples/sample-spring-boot1/src/main/java/com/nike/backstopper/springbootsample/model/SampleModel.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.nike.backstopper.springbootsample.model; - -import com.nike.backstopper.apierror.ApiError; -import com.nike.backstopper.apierror.sample.SampleCoreApiError; -import com.nike.backstopper.springbootsample.error.SampleProjectApiError; -import com.nike.backstopper.springbootsample.error.SampleProjectApiErrorsImpl; -import com.nike.backstopper.validation.constraints.StringConvertsToClassType; - -import org.hibernate.validator.constraints.NotBlank; -import org.hibernate.validator.constraints.Range; - -import javax.validation.constraints.NotNull; - -/** - * Simple model class showing the JSR 303 Bean Validation integration in Backstopper. Each message for a JSR 303 - * annotation must match an {@link ApiError#getName()} from one of the errors returned by this project's - * {@link SampleProjectApiErrorsImpl#getProjectApiErrors()}. In this case that means you can use any of the enum - * names from {@link SampleCoreApiError} or {@link SampleProjectApiError}. - * - *

If you have a typo or forget to add a message that matches an error name then the {@code VerifyJsr303ContractTest} - * unit test will catch your error and the project will fail to build - the test will give you info on exactly which - * classes, fields, and annotations don't conform to the necessary convention. - */ -public class SampleModel { - @NotBlank(message = "FOO_STRING_CANNOT_BE_BLANK") - public final String foo; - - @Range(message = "INVALID_RANGE_VALUE", min = 0, max = 42) - public final String range_0_to_42; - - @NotNull(message = "RGB_COLOR_CANNOT_BE_NULL") - @StringConvertsToClassType( - message = "NOT_RGB_COLOR_ENUM", classType = RgbColor.class, allowCaseInsensitiveEnumMatch = true - ) - public final String rgb_color; - - public final Boolean throw_manual_error; - - @SuppressWarnings("unused") - // Intentionally protected - here for deserialization support. - protected SampleModel() { - this(null, null, null, null); - } - - public SampleModel(String foo, String range_0_to_42, String rgb_color, Boolean throw_manual_error) { - this.foo = foo; - this.range_0_to_42 = range_0_to_42; - this.rgb_color = rgb_color; - this.throw_manual_error = throw_manual_error; - } -} diff --git a/samples/sample-spring-boot1/src/main/resources/application.properties b/samples/sample-spring-boot1/src/main/resources/application.properties deleted file mode 100644 index 4c00e40..0000000 --- a/samples/sample-spring-boot1/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -server.port=8080 diff --git a/samples/sample-spring-boot1/src/main/resources/logback.xml b/samples/sample-spring-boot1/src/main/resources/logback.xml deleted file mode 100644 index 80adb28..0000000 --- a/samples/sample-spring-boot1/src/main/resources/logback.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - %d{yyyy-MM-dd_HH:mm:ss.SSS} [%thread] |-%-5level %logger{36} - %msg%n - - - - - - - \ No newline at end of file diff --git a/samples/sample-spring-boot1/src/test/java/com/nike/backstopper/springbootsample/componenttest/VerifyExpectedErrorsAreReturnedComponentTest.java b/samples/sample-spring-boot1/src/test/java/com/nike/backstopper/springbootsample/componenttest/VerifyExpectedErrorsAreReturnedComponentTest.java deleted file mode 100644 index a7446c1..0000000 --- a/samples/sample-spring-boot1/src/test/java/com/nike/backstopper/springbootsample/componenttest/VerifyExpectedErrorsAreReturnedComponentTest.java +++ /dev/null @@ -1,474 +0,0 @@ -package com.nike.backstopper.springbootsample.componenttest; - -import com.nike.backstopper.apierror.ApiError; -import com.nike.backstopper.apierror.ApiErrorWithMetadata; -import com.nike.backstopper.apierror.sample.SampleCoreApiError; -import com.nike.backstopper.model.DefaultErrorContractDTO; -import com.nike.backstopper.model.DefaultErrorDTO; -import com.nike.backstopper.springbootsample.Main; -import com.nike.backstopper.springbootsample.error.SampleProjectApiError; -import com.nike.backstopper.springbootsample.model.RgbColor; -import com.nike.backstopper.springbootsample.model.SampleModel; -import com.nike.internal.util.MapBuilder; -import com.nike.internal.util.Pair; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; - -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; -import org.springframework.boot.SpringApplication; -import org.springframework.context.ConfigurableApplicationContext; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -import io.restassured.http.ContentType; -import io.restassured.response.ExtractableResponse; - -import static com.nike.backstopper.springbootsample.controller.SampleController.CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH; -import static com.nike.backstopper.springbootsample.controller.SampleController.SAMPLE_PATH; -import static com.nike.backstopper.springbootsample.controller.SampleController.TRIGGER_UNHANDLED_ERROR_SUBPATH; -import static com.nike.backstopper.springbootsample.controller.SampleController.WITH_REQUIRED_QUERY_PARAM_SUBPATH; -import static com.nike.backstopper.springbootsample.controller.SampleController.nextRandomColor; -import static com.nike.backstopper.springbootsample.controller.SampleController.nextRangeInt; -import static com.nike.backstopper.springbootsample.error.SampleProjectApiError.INVALID_RANGE_VALUE; -import static com.nike.backstopper.springbootsample.error.SampleProjectApiError.NOT_RGB_COLOR_ENUM; -import static com.nike.backstopper.springbootsample.error.SampleProjectApiError.RGB_COLOR_CANNOT_BE_NULL; -import static com.nike.internal.util.testing.TestUtils.findFreePort; -import static io.restassured.RestAssured.given; -import static java.util.Collections.singleton; -import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Component test that starts up the sample server and hits it with requests that should generate specific errors. - * - * @author Nic Munroe - */ -public class VerifyExpectedErrorsAreReturnedComponentTest { - - private static final int SERVER_PORT = findFreePort(); - private static ConfigurableApplicationContext serverAppContext; - - private static final ObjectMapper objectMapper = new ObjectMapper(); - - @BeforeAll - public static void beforeClass() { - serverAppContext = SpringApplication.run(Main.class, "--server.port=" + SERVER_PORT); - } - - @AfterAll - public static void afterClass() { - SpringApplication.exit(serverAppContext); - } - - @BeforeEach - public void beforeMethod() { - } - - @AfterEach - public void afterMethod() { - } - - private void verifyErrorReceived(ExtractableResponse response, ApiError expectedError) { - verifyErrorReceived(response, singleton(expectedError), expectedError.getHttpStatusCode()); - } - - private DefaultErrorDTO findErrorMatching(DefaultErrorContractDTO errorContract, ApiError desiredError) { - for (DefaultErrorDTO error : errorContract.errors) { - if (error.code.equals(desiredError.getErrorCode()) && error.message.equals(desiredError.getMessage())) - return error; - } - - return null; - } - - private void verifyErrorReceived(ExtractableResponse response, Collection expectedErrors, int expectedHttpStatusCode) { - assertThat(response.statusCode()).isEqualTo(expectedHttpStatusCode); - try { - DefaultErrorContractDTO errorContract = objectMapper.readValue(response.asString(), DefaultErrorContractDTO.class); - assertThat(errorContract.error_id).isNotEmpty(); - assertThat(UUID.fromString(errorContract.error_id)).isNotNull(); - assertThat(errorContract.errors).hasSameSizeAs(expectedErrors); - for (ApiError apiError : expectedErrors) { - DefaultErrorDTO matchingError = findErrorMatching(errorContract, apiError); - assertThat(matchingError).isNotNull(); - assertThat(matchingError.code).isEqualTo(apiError.getErrorCode()); - assertThat(matchingError.message).isEqualTo(apiError.getMessage()); - assertThat(matchingError.metadata).isEqualTo(apiError.getMetadata()); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private SampleModel randomizedSampleModel() { - return new SampleModel(UUID.randomUUID().toString(), String.valueOf(nextRangeInt(0, 42)), nextRandomColor().name(), false); - } - - // *************** SUCCESSFUL (NON ERROR) CALLS ****************** - @Test - public void verify_basic_sample_get() throws IOException { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .basePath(SAMPLE_PATH) - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - assertThat(response.statusCode()).isEqualTo(200); - SampleModel responseBody = objectMapper.readValue(response.asString(), SampleModel.class); - assertThat(responseBody).isNotNull(); - assertThat(responseBody.foo).isNotEmpty(); - assertThat(responseBody.range_0_to_42).isNotEmpty(); - assertThat(Integer.parseInt(responseBody.range_0_to_42)).isBetween(0, 42); - assertThat(responseBody.rgb_color).isNotEmpty(); - assertThat(RgbColor.toRgbColor(responseBody.rgb_color)).isNotNull(); - assertThat(responseBody.throw_manual_error).isFalse(); - } - - @Test - public void verify_basic_sample_post() throws IOException { - SampleModel requestPayload = randomizedSampleModel(); - String requestPayloadAsString = objectMapper.writeValueAsString(requestPayload); - - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .contentType(ContentType.JSON) - .basePath(SAMPLE_PATH) - .body(requestPayloadAsString) - .log().all() - .when() - .post() - .then() - .log().all() - .extract(); - - assertThat(response.statusCode()).isEqualTo(201); - SampleModel responseBody = objectMapper.readValue(response.asString(), SampleModel.class); - assertThat(responseBody).isNotNull(); - assertThat(responseBody.foo).isEqualTo(requestPayload.foo); - assertThat(responseBody.range_0_to_42).isEqualTo(requestPayload.range_0_to_42); - assertThat(responseBody.rgb_color).isEqualTo(requestPayload.rgb_color); - assertThat(responseBody.throw_manual_error).isEqualTo(requestPayload.throw_manual_error); - } - - // *************** JSR 303 AND ENDPOINT ERRORS ****************** - - @CsvSource( - value = { - "null | 42 | GREEN | FOO_STRING_CANNOT_BE_BLANK | 400", - "bar | -1 | GREEN | INVALID_RANGE_VALUE | 400", - "bar | 42 | null | RGB_COLOR_CANNOT_BE_NULL | 400", - "bar | 42 | car | NOT_RGB_COLOR_ENUM | 400", - " | 99 | tree | FOO_STRING_CANNOT_BE_BLANK,INVALID_RANGE_VALUE,NOT_RGB_COLOR_ENUM | 400", - }, - delimiter = '|', - nullValues = { "null" } - ) - @ParameterizedTest - public void verify_jsr303_validation_errors( - String fooString, String rangeString, String rgbColorString, - String expectedErrorsComboString, int expectedResponseHttpStatusCode) throws JsonProcessingException - { - SampleModel requestPayload = new SampleModel(fooString, rangeString, rgbColorString, false); - String requestPayloadAsString = objectMapper.writeValueAsString(requestPayload); - - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .contentType(ContentType.JSON) - .basePath(SAMPLE_PATH) - .body(requestPayloadAsString) - .log().all() - .when() - .post() - .then() - .log().all() - .extract(); - - String[] expectedErrorsArray = expectedErrorsComboString.split(","); - List expectedErrors = new ArrayList<>(); - for (String errorStr : expectedErrorsArray) { - ApiError apiError = SampleProjectApiError.valueOf(errorStr); - String extraMetadataFieldValue = null; - - if (INVALID_RANGE_VALUE.equals(apiError)) - extraMetadataFieldValue = "range_0_to_42"; - else if (RGB_COLOR_CANNOT_BE_NULL.equals(apiError) || NOT_RGB_COLOR_ENUM.equals(apiError)) - extraMetadataFieldValue = "rgb_color"; - - if (extraMetadataFieldValue != null) - apiError = new ApiErrorWithMetadata(apiError, MapBuilder.builder("field", (Object)extraMetadataFieldValue).build()); - - expectedErrors.add(apiError); - } - verifyErrorReceived(response, expectedErrors, expectedResponseHttpStatusCode); - } - - @Test - public void verify_MANUALLY_THROWN_ERROR_is_thrown_when_requested() throws IOException { - SampleModel requestPayload = new SampleModel("bar", "42", "RED", true); - String requestPayloadAsString = objectMapper.writeValueAsString(requestPayload); - - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .contentType(ContentType.JSON) - .basePath(SAMPLE_PATH) - .body(requestPayloadAsString) - .log().all() - .when() - .post() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleProjectApiError.MANUALLY_THROWN_ERROR); - // This code path also should add some custom headers to the response - assertThat(response.headers().getValues("rgbColorValue")).isEqualTo(singletonList(requestPayload.rgb_color)); - assertThat(response.headers().getValues("otherExtraMultivalueHeader")).isEqualTo(Arrays.asList("foo", "bar")); - } - - @Test - public void verify_SOME_MEANINGFUL_ERROR_NAME_is_thrown_when_correct_endpoint_is_hit() { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .basePath(SAMPLE_PATH + CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH) - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleProjectApiError.SOME_MEANINGFUL_ERROR_NAME); - } - - @Test - public void verify_GENERIC_SERVICE_ERROR_is_thrown_when_correct_endpoint_is_hit() { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .basePath(SAMPLE_PATH + TRIGGER_UNHANDLED_ERROR_SUBPATH) - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleCoreApiError.GENERIC_SERVICE_ERROR); - } - - // *************** FRAMEWORK/CONTAINER ERRORS ****************** - - @Test - public void verify_NOT_FOUND_returned_if_unknown_path_is_requested() { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .basePath(UUID.randomUUID().toString()) - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleCoreApiError.NOT_FOUND); - } - - @Test - public void verify_ERROR_THROWN_IN_SERVLET_FILTER_OUTSIDE_SPRING_returned_if_servlet_filter_trigger_occurs() { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .basePath(SAMPLE_PATH) - .header("throw-servlet-filter-exception", "true") - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleProjectApiError.ERROR_THROWN_IN_SERVLET_FILTER_OUTSIDE_SPRING); - } - - @Test - public void verify_METHOD_NOT_ALLOWED_returned_if_known_path_is_requested_with_invalid_http_method() { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .basePath(SAMPLE_PATH) - .log().all() - .when() - .delete() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleCoreApiError.METHOD_NOT_ALLOWED); - } - - @Test - public void verify_sample_get_fails_with_NO_ACCEPTABLE_REPRESENTATION_if_passed_invalid_accept_header() { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .basePath(SAMPLE_PATH) - .accept(ContentType.BINARY) - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleCoreApiError.NO_ACCEPTABLE_REPRESENTATION); - } - - @Test - public void verify_sample_post_fails_with_UNSUPPORTED_MEDIA_TYPE_if_passed_invalid_content_type() throws IOException { - SampleModel requestPayload = randomizedSampleModel(); - String requestPayloadAsString = objectMapper.writeValueAsString(requestPayload); - - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .basePath(SAMPLE_PATH) - .body(requestPayloadAsString) - .contentType(ContentType.TEXT) - .log().all() - .when() - .post() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleCoreApiError.UNSUPPORTED_MEDIA_TYPE); - } - - @Test - public void verify_MALFORMED_REQUEST_is_thrown_when_required_data_is_missing() { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .basePath(SAMPLE_PATH + WITH_REQUIRED_QUERY_PARAM_SUBPATH) - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - verifyErrorReceived( - response, - new ApiErrorWithMetadata( - SampleCoreApiError.MALFORMED_REQUEST, - Pair.of("missing_param_type", "int"), - Pair.of("missing_param_name", "requiredQueryParamValue") - ) - ); - } - - @Test - public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert_type() { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .basePath(SAMPLE_PATH + WITH_REQUIRED_QUERY_PARAM_SUBPATH) - .queryParam("requiredQueryParamValue", "not-an-integer") - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, new ApiErrorWithMetadata( - SampleCoreApiError.TYPE_CONVERSION_ERROR, - MapBuilder.builder("bad_property_name", (Object)"requiredQueryParamValue") - .put("bad_property_value", "not-an-integer") - .put("required_type", "int") - .build() - )); - } - - @Test - public void verify_sample_post_fails_with_MISSING_EXPECTED_CONTENT_if_passed_empty_body() { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .basePath(SAMPLE_PATH) - .contentType(ContentType.JSON) - .body("") - .log().all() - .when() - .post() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleCoreApiError.MISSING_EXPECTED_CONTENT); - } - - @Test - public void verify_sample_post_fails_with_MALFORMED_REQUEST_if_passed_bad_json_body() throws IOException { - SampleModel originalValidPayloadObj = randomizedSampleModel(); - String originalValidPayloadAsString = objectMapper.writeValueAsString(originalValidPayloadObj); - @SuppressWarnings("unchecked") - Map badRequestPayloadAsMap = objectMapper.readValue(originalValidPayloadAsString, Map.class); - badRequestPayloadAsMap.put("throw_manual_error", "not-a-boolean"); - String badJsonPayloadAsString = objectMapper.writeValueAsString(badRequestPayloadAsMap); - - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(SERVER_PORT) - .basePath(SAMPLE_PATH) - .contentType(ContentType.JSON) - .body(badJsonPayloadAsString) - .log().all() - .when() - .post() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleCoreApiError.MALFORMED_REQUEST); - } - -} diff --git a/samples/sample-spring-boot1/src/test/java/com/nike/backstopper/springbootsample/error/SampleProjectApiErrorsImplTest.java b/samples/sample-spring-boot1/src/test/java/com/nike/backstopper/springbootsample/error/SampleProjectApiErrorsImplTest.java deleted file mode 100644 index ccf4ec0..0000000 --- a/samples/sample-spring-boot1/src/test/java/com/nike/backstopper/springbootsample/error/SampleProjectApiErrorsImplTest.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.nike.backstopper.springbootsample.error; - -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrorsTestBase; - -/** - * Extends {@link ProjectApiErrorsTestBase} in order to inherit tests that will verify the correctness of this - * project's {@link SampleProjectApiErrorsImpl}. - * - * @author Nic Munroe - */ -public class SampleProjectApiErrorsImplTest extends ProjectApiErrorsTestBase { - - ProjectApiErrors projectApiErrors = new SampleProjectApiErrorsImpl(); - - @Override - protected ProjectApiErrors getProjectApiErrors() { - return projectApiErrors; - } -} \ No newline at end of file diff --git a/samples/sample-spring-boot1/src/test/java/jsr303convention/ApplicationJsr303AnnotationTroller.java b/samples/sample-spring-boot1/src/test/java/jsr303convention/ApplicationJsr303AnnotationTroller.java deleted file mode 100644 index 0ae4200..0000000 --- a/samples/sample-spring-boot1/src/test/java/jsr303convention/ApplicationJsr303AnnotationTroller.java +++ /dev/null @@ -1,47 +0,0 @@ -package jsr303convention; - -import com.nike.backstopper.apierror.contract.jsr303convention.ReflectionBasedJsr303AnnotationTrollerBase; -import com.nike.internal.util.Pair; - -import java.lang.annotation.Annotation; -import java.lang.reflect.AnnotatedElement; -import java.util.List; -import java.util.function.Predicate; - -/** - * Extension of {@link ReflectionBasedJsr303AnnotationTrollerBase} for use with this sample project. This is used by JSR - * 303 annotation convention enforcement tests (e.g. {@link VerifyJsr303ContractTest} and {@link - * VerifyStringConvertsToClassTypeAnnotationsAreValidTest}). - * - *

NOTE: If you want to exclude classes or specific JSR 303 annotations from triggering convention violation errors - * in those tests you can do so by populating the return values of the {@link #ignoreAllAnnotationsAssociatedWithTheseProjectClasses()} - * and {@link #specificAnnotationDeclarationExclusionsForProject()} methods. IMPORTANT - this should only be done - * if you *really* know what you're doing. Usually it's only done for unit test classes that are intended to violate the - * convention. It should not be done for production code under normal circumstances. See the javadocs for the super - * class for those methods if you need to use them. - * - * @author Nic Munroe - */ -public final class ApplicationJsr303AnnotationTroller extends ReflectionBasedJsr303AnnotationTrollerBase { - - public static final ApplicationJsr303AnnotationTroller INSTANCE = new ApplicationJsr303AnnotationTroller(); - - public static ApplicationJsr303AnnotationTroller getInstance() { - return INSTANCE; - } - - // Intentionally private - use {@code getInstance()} to retrieve the singleton instance of this class. - private ApplicationJsr303AnnotationTroller() { - super(); - } - - @Override - protected List> ignoreAllAnnotationsAssociatedWithTheseProjectClasses() { - return null; - } - - @Override - protected List>> specificAnnotationDeclarationExclusionsForProject() { - return null; - } -} diff --git a/samples/sample-spring-boot1/src/test/java/jsr303convention/VerifyJsr303ContractTest.java b/samples/sample-spring-boot1/src/test/java/jsr303convention/VerifyJsr303ContractTest.java deleted file mode 100644 index a34a860..0000000 --- a/samples/sample-spring-boot1/src/test/java/jsr303convention/VerifyJsr303ContractTest.java +++ /dev/null @@ -1,34 +0,0 @@ -package jsr303convention; - -import com.nike.backstopper.apierror.contract.jsr303convention.ReflectionBasedJsr303AnnotationTrollerBase; -import com.nike.backstopper.apierror.contract.jsr303convention.VerifyJsr303ValidationMessagesPointToApiErrorsTest; -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.springbootsample.error.SampleProjectApiErrorsImpl; - -/** - * Verifies that *ALL* non-excluded JSR 303 validation annotations in this project have a message defined that maps to a - * {@link com.nike.backstopper.apierror.ApiError} enum name from this project's {@link ProjectApiErrors}. This is how - * the JSR 303 Bean Validation system is connected to the Backstopper error handling system and you should NOT disable - * these tests. - * - *

You can exclude annotation declarations (e.g. for unit test classes that are intended to violate the naming - * convention) by making sure that the {@link ApplicationJsr303AnnotationTroller#ignoreAllAnnotationsAssociatedWithTheseProjectClasses()} - * and {@link ApplicationJsr303AnnotationTroller#specificAnnotationDeclarationExclusionsForProject()} methods return - * what you need, but you should not exclude any annotations in production code under normal circumstances. - * - * @author Nic Munroe - */ -public class VerifyJsr303ContractTest extends VerifyJsr303ValidationMessagesPointToApiErrorsTest { - - private static final ProjectApiErrors PROJECT_API_ERRORS = new SampleProjectApiErrorsImpl(); - - @Override - protected ReflectionBasedJsr303AnnotationTrollerBase getAnnotationTroller() { - return ApplicationJsr303AnnotationTroller.getInstance(); - } - - @Override - protected ProjectApiErrors getProjectApiErrors() { - return PROJECT_API_ERRORS; - } -} diff --git a/samples/sample-spring-boot1/src/test/java/jsr303convention/VerifyStringConvertsToClassTypeAnnotationsAreValidTest.java b/samples/sample-spring-boot1/src/test/java/jsr303convention/VerifyStringConvertsToClassTypeAnnotationsAreValidTest.java deleted file mode 100644 index d127306..0000000 --- a/samples/sample-spring-boot1/src/test/java/jsr303convention/VerifyStringConvertsToClassTypeAnnotationsAreValidTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package jsr303convention; - -import com.nike.backstopper.apierror.contract.jsr303convention.ReflectionBasedJsr303AnnotationTrollerBase; -import com.nike.backstopper.apierror.contract.jsr303convention.VerifyEnumsReferencedByStringConvertsToClassTypeJsr303AnnotationsAreJacksonCaseInsensitiveTest; -import com.nike.backstopper.validation.constraints.StringConvertsToClassType; - -/** - * Makes sure that any Enums referenced by {@link StringConvertsToClassType} JSR 303 annotations are case insensitive if - * they are marked with {@link StringConvertsToClassType#allowCaseInsensitiveEnumMatch()} set to true. - * - *

You can exclude annotation declarations (e.g. for unit test classes that are intended to violate the naming - * convention) by making sure that the {@link ApplicationJsr303AnnotationTroller#ignoreAllAnnotationsAssociatedWithTheseProjectClasses()} - * and {@link ApplicationJsr303AnnotationTroller#specificAnnotationDeclarationExclusionsForProject()} methods return - * what you need, but you should not exclude any annotations in production code under normal circumstances. - * - * @author Nic Munroe - */ -public class VerifyStringConvertsToClassTypeAnnotationsAreValidTest - extends VerifyEnumsReferencedByStringConvertsToClassTypeJsr303AnnotationsAreJacksonCaseInsensitiveTest { - - @Override - protected ReflectionBasedJsr303AnnotationTrollerBase getAnnotationTroller() { - return ApplicationJsr303AnnotationTroller.getInstance(); - } -} From c99d6c69be08fe51b1ce7c5f297b2cf316d2ef91 Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Wed, 11 Sep 2024 15:58:39 -0700 Subject: [PATCH 22/42] Remove spring 4 and springboot 1 testonly modules --- testonly/testonly-spring4-webmvc/README.md | 16 - testonly/testonly-spring4-webmvc/build.gradle | 31 -- ...BackstopperSpring4WebMvcComponentTest.java | 513 ------------------ .../Spring4WebMvcClasspathScanConfig.java | 40 -- .../Spring4WebMvcDirectImportConfig.java | 42 -- .../src/test/resources/logback.xml | 11 - testonly/testonly-springboot1/README.md | 16 - testonly/testonly-springboot1/build.gradle | 36 -- .../BackstopperSpringboot1ComponentTest.java | 513 ------------------ .../Springboot1ClasspathScanConfig.java | 48 -- .../Springboot1DirectImportConfig.java | 50 -- .../src/test/resources/logback.xml | 11 - 12 files changed, 1327 deletions(-) delete mode 100644 testonly/testonly-spring4-webmvc/README.md delete mode 100644 testonly/testonly-spring4-webmvc/build.gradle delete mode 100644 testonly/testonly-spring4-webmvc/src/test/java/com/nike/backstopper/testonly/BackstopperSpring4WebMvcComponentTest.java delete mode 100644 testonly/testonly-spring4-webmvc/src/test/java/serverconfig/classpathscan/Spring4WebMvcClasspathScanConfig.java delete mode 100644 testonly/testonly-spring4-webmvc/src/test/java/serverconfig/directimport/Spring4WebMvcDirectImportConfig.java delete mode 100644 testonly/testonly-spring4-webmvc/src/test/resources/logback.xml delete mode 100644 testonly/testonly-springboot1/README.md delete mode 100644 testonly/testonly-springboot1/build.gradle delete mode 100644 testonly/testonly-springboot1/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot1ComponentTest.java delete mode 100644 testonly/testonly-springboot1/src/test/java/serverconfig/classpathscan/Springboot1ClasspathScanConfig.java delete mode 100644 testonly/testonly-springboot1/src/test/java/serverconfig/directimport/Springboot1DirectImportConfig.java delete mode 100644 testonly/testonly-springboot1/src/test/resources/logback.xml diff --git a/testonly/testonly-spring4-webmvc/README.md b/testonly/testonly-spring4-webmvc/README.md deleted file mode 100644 index ada47de..0000000 --- a/testonly/testonly-spring4-webmvc/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# Backstopper - testonly-spring4-webmvc - -Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 7 and greater. - -This submodule contains tests to verify that the [backstopper-spring-web-mvc](../../backstopper-spring-web-mvc) -module's functionality works as expected in Spring 4 environments, for both classpath-scanning and direct-import -Backstopper configuration use cases. - -## More Info - -See the [base project README.md](../../README.md), [User Guide](../../USER_GUIDE.md), and Backstopper repository -source code and javadocs for all further information. - -## License - -Backstopper is released under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/testonly/testonly-spring4-webmvc/build.gradle b/testonly/testonly-spring4-webmvc/build.gradle deleted file mode 100644 index 5418dd4..0000000 --- a/testonly/testonly-spring4-webmvc/build.gradle +++ /dev/null @@ -1,31 +0,0 @@ -evaluationDependsOn(':') - -sourceCompatibility = JavaVersion.VERSION_1_8 -targetCompatibility = JavaVersion.VERSION_1_8 - -test { - useJUnitPlatform() -} - -dependencies { - implementation( - project(":backstopper-spring-web-mvc"), - project(":backstopper-custom-validators"), - "org.springframework:spring-webmvc:$spring4Version", - "ch.qos.logback:logback-classic:$logbackVersion", - "org.hibernate:hibernate-validator:$hibernateValidatorVersion", - "org.eclipse.jetty:jetty-webapp:$jettyVersion", - ) - testImplementation( - project(":backstopper-reusable-tests-junit5"), - project(":testonly:testonly-spring-reusable-test-support"), - "org.junit.jupiter:junit-jupiter-api:$junit5Version", - "org.junit.jupiter:junit-jupiter-engine:$junit5Version", - "org.junit.jupiter:junit-jupiter-params:$junit5Version", - "org.mockito:mockito-core:$mockitoVersion", - "org.assertj:assertj-core:$assertJVersion", - "io.rest-assured:rest-assured:$restAssuredVersion", - // The jaxb-api is needed for building on the java 11 JDK as these classes were moved out of the Java SE libs. - "javax.xml.bind:jaxb-api:$jaxbApiVersion", - ) -} diff --git a/testonly/testonly-spring4-webmvc/src/test/java/com/nike/backstopper/testonly/BackstopperSpring4WebMvcComponentTest.java b/testonly/testonly-spring4-webmvc/src/test/java/com/nike/backstopper/testonly/BackstopperSpring4WebMvcComponentTest.java deleted file mode 100644 index 984d728..0000000 --- a/testonly/testonly-spring4-webmvc/src/test/java/com/nike/backstopper/testonly/BackstopperSpring4WebMvcComponentTest.java +++ /dev/null @@ -1,513 +0,0 @@ -package com.nike.backstopper.testonly; - -import com.nike.backstopper.apierror.ApiError; -import com.nike.backstopper.apierror.ApiErrorWithMetadata; -import com.nike.backstopper.apierror.sample.SampleCoreApiError; -import com.nike.internal.util.MapBuilder; -import com.nike.internal.util.Pair; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; - -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.EnumSource; -import org.junit.jupiter.params.provider.MethodSource; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -import io.restassured.http.ContentType; -import io.restassured.response.ExtractableResponse; -import serverconfig.classpathscan.Spring4WebMvcClasspathScanConfig; -import serverconfig.directimport.Spring4WebMvcDirectImportConfig; -import testonly.componenttest.spring.reusable.error.SampleProjectApiError; -import testonly.componenttest.spring.reusable.jettyserver.SpringMvcJettyComponentTestServer; -import testonly.componenttest.spring.reusable.model.RgbColor; -import testonly.componenttest.spring.reusable.model.SampleModel; - -import static com.nike.internal.util.testing.TestUtils.findFreePort; -import static io.restassured.RestAssured.given; -import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; -import static testonly.componenttest.spring.reusable.controller.SampleController.CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH; -import static testonly.componenttest.spring.reusable.controller.SampleController.SAMPLE_PATH; -import static testonly.componenttest.spring.reusable.controller.SampleController.TRIGGER_UNHANDLED_ERROR_SUBPATH; -import static testonly.componenttest.spring.reusable.controller.SampleController.WITH_REQUIRED_QUERY_PARAM_SUBPATH; -import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.FOO_STRING_CANNOT_BE_BLANK; -import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.INVALID_RANGE_VALUE; -import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.NOT_RGB_COLOR_ENUM; -import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.RGB_COLOR_CANNOT_BE_NULL; -import static testonly.componenttest.spring.reusable.testutil.TestUtils.randomizedSampleModel; -import static testonly.componenttest.spring.reusable.testutil.TestUtils.verifyErrorReceived; - -/** - * Component test to verify that the functionality of {@code backstopper-spring-web-mvc} works as expected in a - * Spring 4 environment, for both classpath-scanning and direct-import Backstopper configuration use cases. - * - * @author Nic Munroe - */ -public class BackstopperSpring4WebMvcComponentTest { - - private static final int CLASSPATH_SCAN_SERVER_PORT = findFreePort(); - private static final int DIRECT_IMPORT_SERVER_PORT = findFreePort(); - private static final ObjectMapper objectMapper = new ObjectMapper(); - - private static final SpringMvcJettyComponentTestServer classpathScanServer = new SpringMvcJettyComponentTestServer( - CLASSPATH_SCAN_SERVER_PORT, Spring4WebMvcClasspathScanConfig.class - ); - - private static final SpringMvcJettyComponentTestServer directImportServer = new SpringMvcJettyComponentTestServer( - DIRECT_IMPORT_SERVER_PORT, Spring4WebMvcDirectImportConfig.class - ); - - @BeforeAll - public static void beforeClass() throws Exception { - assertThat(CLASSPATH_SCAN_SERVER_PORT).isNotEqualTo(DIRECT_IMPORT_SERVER_PORT); - classpathScanServer.startServer(); - directImportServer.startServer(); - } - - @AfterAll - public static void afterClass() throws Exception { - classpathScanServer.shutdownServer(); - directImportServer.shutdownServer(); - } - - @SuppressWarnings("unused") - private enum ServerScenario { - CLASSPATH_SCAN_SERVER(CLASSPATH_SCAN_SERVER_PORT), - DIRECT_IMPORT_SERVER(DIRECT_IMPORT_SERVER_PORT); - - public final int serverPort; - - ServerScenario(int serverPort) { - this.serverPort = serverPort; - } - } - - // *************** SUCCESSFUL (NON ERROR) CALLS ****************** - @EnumSource(ServerScenario.class) - @ParameterizedTest - public void verify_basic_sample_get(ServerScenario scenario) throws IOException { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(scenario.serverPort) - .basePath(SAMPLE_PATH) - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - assertThat(response.statusCode()).isEqualTo(200); - SampleModel responseBody = objectMapper.readValue(response.asString(), SampleModel.class); - assertThat(responseBody).isNotNull(); - assertThat(responseBody.foo).isNotEmpty(); - assertThat(responseBody.range_0_to_42).isNotEmpty(); - assertThat(Integer.parseInt(responseBody.range_0_to_42)).isBetween(0, 42); - assertThat(responseBody.rgb_color).isNotEmpty(); - assertThat(RgbColor.toRgbColor(responseBody.rgb_color)).isNotNull(); - assertThat(responseBody.throw_manual_error).isFalse(); - } - - @EnumSource(ServerScenario.class) - @ParameterizedTest - public void verify_basic_sample_post(ServerScenario scenario) throws IOException { - SampleModel requestPayload = randomizedSampleModel(); - String requestPayloadAsString = objectMapper.writeValueAsString(requestPayload); - - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(scenario.serverPort) - .contentType(ContentType.JSON) - .basePath(SAMPLE_PATH) - .body(requestPayloadAsString) - .log().all() - .when() - .post() - .then() - .log().all() - .extract(); - - assertThat(response.statusCode()).isEqualTo(201); - SampleModel responseBody = objectMapper.readValue(response.asString(), SampleModel.class); - assertThat(responseBody).isNotNull(); - assertThat(responseBody.foo).isEqualTo(requestPayload.foo); - assertThat(responseBody.range_0_to_42).isEqualTo(requestPayload.range_0_to_42); - assertThat(responseBody.rgb_color).isEqualTo(requestPayload.rgb_color); - assertThat(responseBody.throw_manual_error).isEqualTo(requestPayload.throw_manual_error); - } - - // *************** JSR 303 AND ENDPOINT ERRORS ****************** - - @SuppressWarnings("unused") - private enum Jsr303SampleModelValidationScenario { - BLANK_FIELD_VIOLATION( - new SampleModel("", "42", "GREEN", false), - singletonList(FOO_STRING_CANNOT_BE_BLANK) - ), - INVALID_RANGE_VIOLATION( - new SampleModel("bar", "-1", "GREEN", false), - singletonList(INVALID_RANGE_VALUE) - ), - NULL_FIELD_VIOLATION( - new SampleModel("bar", "42", null, false), - singletonList(RGB_COLOR_CANNOT_BE_NULL) - ), - STRING_CONVERTS_TO_CLASSTYPE_VIOLATION( - new SampleModel("bar", "42", "car", false), - singletonList(NOT_RGB_COLOR_ENUM) - ), - MULTIPLE_VIOLATIONS( - new SampleModel(" \n\r\t ", "99", "tree", false), - Arrays.asList(FOO_STRING_CANNOT_BE_BLANK, INVALID_RANGE_VALUE, NOT_RGB_COLOR_ENUM) - ); - - public final SampleModel model; - public final List expectedErrors; - - Jsr303SampleModelValidationScenario( - SampleModel model, List expectedErrors - ) { - this.model = model; - this.expectedErrors = expectedErrors; - } - } - - public static List jsr303ValidationErrorScenariosDataProvider() { - List result = new ArrayList<>(); - for (Jsr303SampleModelValidationScenario violationScenario : Jsr303SampleModelValidationScenario.values()) { - for (ServerScenario serverScenario : ServerScenario.values()) { - result.add(new Object[]{violationScenario, serverScenario}); - } - } - return result; - } - - @MethodSource("jsr303ValidationErrorScenariosDataProvider") - @ParameterizedTest - public void verify_jsr303_validation_errors( - Jsr303SampleModelValidationScenario violationScenario, ServerScenario serverScenario - ) throws JsonProcessingException { - String requestPayloadAsString = objectMapper.writeValueAsString(violationScenario.model); - - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(serverScenario.serverPort) - .contentType(ContentType.JSON) - .basePath(SAMPLE_PATH) - .body(requestPayloadAsString) - .log().all() - .when() - .post() - .then() - .log().all() - .extract(); - - List expectedErrors = new ArrayList<>(); - for (ApiError expectedApiError : violationScenario.expectedErrors) { - String extraMetadataFieldValue = null; - - if (INVALID_RANGE_VALUE.equals(expectedApiError)) { - extraMetadataFieldValue = "range_0_to_42"; - } - else if (RGB_COLOR_CANNOT_BE_NULL.equals(expectedApiError) || NOT_RGB_COLOR_ENUM.equals(expectedApiError)) { - extraMetadataFieldValue = "rgb_color"; - } - - if (extraMetadataFieldValue != null) { - expectedApiError = new ApiErrorWithMetadata( - expectedApiError, - MapBuilder.builder("field", (Object) extraMetadataFieldValue).build() - ); - } - - expectedErrors.add(expectedApiError); - } - verifyErrorReceived(response, expectedErrors, 400); - } - - @EnumSource(ServerScenario.class) - @ParameterizedTest - public void verify_MANUALLY_THROWN_ERROR_is_thrown_when_requested(ServerScenario scenario) throws IOException { - SampleModel requestPayload = new SampleModel("bar", "42", "RED", true); - String requestPayloadAsString = objectMapper.writeValueAsString(requestPayload); - - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(scenario.serverPort) - .contentType(ContentType.JSON) - .basePath(SAMPLE_PATH) - .body(requestPayloadAsString) - .log().all() - .when() - .post() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleProjectApiError.MANUALLY_THROWN_ERROR); - // This code path also should add some custom headers to the response - assertThat(response.headers().getValues("rgbColorValue")).isEqualTo(singletonList(requestPayload.rgb_color)); - assertThat(response.headers().getValues("otherExtraMultivalueHeader")).isEqualTo(Arrays.asList("foo", "bar")); - } - - @EnumSource(ServerScenario.class) - @ParameterizedTest - public void verify_SOME_MEANINGFUL_ERROR_NAME_is_thrown_when_correct_endpoint_is_hit(ServerScenario scenario) { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(scenario.serverPort) - .basePath(SAMPLE_PATH + CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH) - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleProjectApiError.SOME_MEANINGFUL_ERROR_NAME); - } - - @EnumSource(ServerScenario.class) - @ParameterizedTest - public void verify_GENERIC_SERVICE_ERROR_is_thrown_when_correct_endpoint_is_hit(ServerScenario scenario) { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(scenario.serverPort) - .basePath(SAMPLE_PATH + TRIGGER_UNHANDLED_ERROR_SUBPATH) - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleCoreApiError.GENERIC_SERVICE_ERROR); - } - - // *************** FRAMEWORK ERRORS ****************** - - @EnumSource(ServerScenario.class) - @ParameterizedTest - public void verify_NOT_FOUND_returned_if_unknown_path_is_requested(ServerScenario scenario) { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(scenario.serverPort) - .basePath(UUID.randomUUID().toString()) - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleCoreApiError.NOT_FOUND); - } - - @EnumSource(ServerScenario.class) - @ParameterizedTest - public void verify_ERROR_THROWN_IN_SERVLET_FILTER_OUTSIDE_SPRING_returned_if_servlet_filter_trigger_occurs( - ServerScenario scenario - ) { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(scenario.serverPort) - .basePath(SAMPLE_PATH) - .header("throw-servlet-filter-exception", "true") - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleProjectApiError.ERROR_THROWN_IN_SERVLET_FILTER_OUTSIDE_SPRING); - } - - @EnumSource(ServerScenario.class) - @ParameterizedTest - public void verify_METHOD_NOT_ALLOWED_returned_if_known_path_is_requested_with_invalid_http_method( - ServerScenario scenario - ) { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(scenario.serverPort) - .basePath(SAMPLE_PATH) - .log().all() - .when() - .delete() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleCoreApiError.METHOD_NOT_ALLOWED); - } - - @EnumSource(ServerScenario.class) - @ParameterizedTest - public void verify_sample_get_fails_with_NO_ACCEPTABLE_REPRESENTATION_if_passed_invalid_accept_header( - ServerScenario scenario - ) { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(scenario.serverPort) - .basePath(SAMPLE_PATH) - .accept(ContentType.BINARY) - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleCoreApiError.NO_ACCEPTABLE_REPRESENTATION); - } - - @EnumSource(ServerScenario.class) - @ParameterizedTest - public void verify_sample_post_fails_with_UNSUPPORTED_MEDIA_TYPE_if_passed_invalid_content_type( - ServerScenario scenario - ) throws IOException { - SampleModel requestPayload = randomizedSampleModel(); - String requestPayloadAsString = objectMapper.writeValueAsString(requestPayload); - - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(scenario.serverPort) - .basePath(SAMPLE_PATH) - .body(requestPayloadAsString) - .contentType(ContentType.TEXT) - .log().all() - .when() - .post() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleCoreApiError.UNSUPPORTED_MEDIA_TYPE); - } - - @EnumSource(ServerScenario.class) - @ParameterizedTest - public void verify_MALFORMED_REQUEST_is_thrown_when_required_data_is_missing( - ServerScenario scenario - ) { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(scenario.serverPort) - .basePath(SAMPLE_PATH + WITH_REQUIRED_QUERY_PARAM_SUBPATH) - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - verifyErrorReceived( - response, - new ApiErrorWithMetadata( - SampleCoreApiError.MALFORMED_REQUEST, - Pair.of("missing_param_type", "int"), - Pair.of("missing_param_name", "requiredQueryParamValue") - ) - ); - } - - @EnumSource(ServerScenario.class) - @ParameterizedTest - public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert_type( - ServerScenario scenario - ) { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(scenario.serverPort) - .basePath(SAMPLE_PATH + WITH_REQUIRED_QUERY_PARAM_SUBPATH) - .queryParam("requiredQueryParamValue", "not-an-integer") - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, new ApiErrorWithMetadata( - SampleCoreApiError.TYPE_CONVERSION_ERROR, - MapBuilder.builder("bad_property_name", (Object)"requiredQueryParamValue") - .put("bad_property_value", "not-an-integer") - .put("required_type", "int") - .build() - )); - } - - @EnumSource(ServerScenario.class) - @ParameterizedTest - public void verify_sample_post_fails_with_MISSING_EXPECTED_CONTENT_if_passed_empty_body( - ServerScenario scenario - ) { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(scenario.serverPort) - .basePath(SAMPLE_PATH) - .contentType(ContentType.JSON) - .body("") - .log().all() - .when() - .post() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleCoreApiError.MISSING_EXPECTED_CONTENT); - } - - @EnumSource(ServerScenario.class) - @ParameterizedTest - public void verify_sample_post_fails_with_MALFORMED_REQUEST_if_passed_bad_json_body( - ServerScenario scenario - ) throws IOException { - SampleModel originalValidPayloadObj = randomizedSampleModel(); - String originalValidPayloadAsString = objectMapper.writeValueAsString(originalValidPayloadObj); - @SuppressWarnings("unchecked") - Map badRequestPayloadAsMap = objectMapper.readValue(originalValidPayloadAsString, Map.class); - badRequestPayloadAsMap.put("throw_manual_error", "not-a-boolean"); - String badJsonPayloadAsString = objectMapper.writeValueAsString(badRequestPayloadAsMap); - - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(scenario.serverPort) - .basePath(SAMPLE_PATH) - .contentType(ContentType.JSON) - .body(badJsonPayloadAsString) - .log().all() - .when() - .post() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleCoreApiError.MALFORMED_REQUEST); - } -} diff --git a/testonly/testonly-spring4-webmvc/src/test/java/serverconfig/classpathscan/Spring4WebMvcClasspathScanConfig.java b/testonly/testonly-spring4-webmvc/src/test/java/serverconfig/classpathscan/Spring4WebMvcClasspathScanConfig.java deleted file mode 100644 index c37c4d1..0000000 --- a/testonly/testonly-spring4-webmvc/src/test/java/serverconfig/classpathscan/Spring4WebMvcClasspathScanConfig.java +++ /dev/null @@ -1,40 +0,0 @@ -package serverconfig.classpathscan; - -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; - -import javax.validation.Validation; -import javax.validation.Validator; - -import testonly.componenttest.spring.reusable.error.SampleProjectApiErrorsImpl; - -/** - * Spring config that uses {@link ComponentScan} to integrate Backstopper via classpath scanning of the - * {@code com.nike.backstopper} package. - * - * @author Nic Munroe - */ -@Configuration -@ComponentScan(basePackages = { - // Component scan the core Backstopper+Spring support. - "com.nike.backstopper", - // Component scan the controller. - "testonly.componenttest.spring.reusable.controller" -}) -@EnableWebMvc -public class Spring4WebMvcClasspathScanConfig { - - @Bean - public ProjectApiErrors getProjectApiErrors() { - return new SampleProjectApiErrorsImpl(); - } - - @Bean - public Validator getJsr303Validator() { - return Validation.buildDefaultValidatorFactory().getValidator(); - } -} diff --git a/testonly/testonly-spring4-webmvc/src/test/java/serverconfig/directimport/Spring4WebMvcDirectImportConfig.java b/testonly/testonly-spring4-webmvc/src/test/java/serverconfig/directimport/Spring4WebMvcDirectImportConfig.java deleted file mode 100644 index 7ee15f4..0000000 --- a/testonly/testonly-spring4-webmvc/src/test/java/serverconfig/directimport/Spring4WebMvcDirectImportConfig.java +++ /dev/null @@ -1,42 +0,0 @@ -package serverconfig.directimport; - -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.handler.spring.config.BackstopperSpringWebMvcConfig; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; - -import javax.validation.Validation; -import javax.validation.Validator; - -import testonly.componenttest.spring.reusable.controller.SampleController; -import testonly.componenttest.spring.reusable.error.SampleProjectApiErrorsImpl; - -/** - * Spring config that uses {@link Import} to integrate Backstopper via direct import of - * {@link BackstopperSpringWebMvcConfig}. - * - * @author Nic Munroe - */ -@Configuration -@Import({ - // Import core Backstopper+Spring support. - BackstopperSpringWebMvcConfig.class, - // Import the controller. - SampleController.class -}) -@EnableWebMvc -public class Spring4WebMvcDirectImportConfig { - - @Bean - public ProjectApiErrors getProjectApiErrors() { - return new SampleProjectApiErrorsImpl(); - } - - @Bean - public Validator getJsr303Validator() { - return Validation.buildDefaultValidatorFactory().getValidator(); - } -} diff --git a/testonly/testonly-spring4-webmvc/src/test/resources/logback.xml b/testonly/testonly-spring4-webmvc/src/test/resources/logback.xml deleted file mode 100644 index 80adb28..0000000 --- a/testonly/testonly-spring4-webmvc/src/test/resources/logback.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - %d{yyyy-MM-dd_HH:mm:ss.SSS} [%thread] |-%-5level %logger{36} - %msg%n - - - - - - - \ No newline at end of file diff --git a/testonly/testonly-springboot1/README.md b/testonly/testonly-springboot1/README.md deleted file mode 100644 index 85c267b..0000000 --- a/testonly/testonly-springboot1/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# Backstopper - testonly-springboot1 - -Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 7 and greater. - -This submodule contains tests to verify that the [backstopper-spring-boot1](../../backstopper-spring-boot1) -module's functionality works as expected in Spring Boot 1 environments, for both classpath-scanning and direct-import -Backstopper configuration use cases. - -## More Info - -See the [base project README.md](../../README.md), [User Guide](../../USER_GUIDE.md), and Backstopper repository -source code and javadocs for all further information. - -## License - -Backstopper is released under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/testonly/testonly-springboot1/build.gradle b/testonly/testonly-springboot1/build.gradle deleted file mode 100644 index 6289f48..0000000 --- a/testonly/testonly-springboot1/build.gradle +++ /dev/null @@ -1,36 +0,0 @@ -evaluationDependsOn(':') - -sourceCompatibility = JavaVersion.VERSION_1_8 -targetCompatibility = JavaVersion.VERSION_1_8 - -ext { - // Springboot requires Servlet API to be at least version 3.1 - servletApiForTestsVersion = '3.1.0' -} - -test { - useJUnitPlatform() -} - -dependencies { - implementation( - project(":backstopper-spring-boot1"), - project(":backstopper-custom-validators"), - "ch.qos.logback:logback-classic:$logbackVersion", - "org.hibernate:hibernate-validator:$hibernateValidatorVersion", - "org.springframework.boot:spring-boot-starter-web:$springboot1Version", - "javax.servlet:javax.servlet-api:$servletApiForTestsVersion", - ) - testImplementation( - project(":backstopper-reusable-tests-junit5"), - project(":testonly:testonly-spring-reusable-test-support"), - "org.junit.jupiter:junit-jupiter-api:$junit5Version", - "org.junit.jupiter:junit-jupiter-engine:$junit5Version", - "org.junit.jupiter:junit-jupiter-params:$junit5Version", - "org.mockito:mockito-core:$mockitoVersion", - "org.assertj:assertj-core:$assertJVersion", - "io.rest-assured:rest-assured:$restAssuredVersion", - // The jaxb-api is needed for building on the java 11 JDK as these classes were moved out of the Java SE libs. - "javax.xml.bind:jaxb-api:$jaxbApiVersion", - ) -} diff --git a/testonly/testonly-springboot1/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot1ComponentTest.java b/testonly/testonly-springboot1/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot1ComponentTest.java deleted file mode 100644 index 63ab074..0000000 --- a/testonly/testonly-springboot1/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot1ComponentTest.java +++ /dev/null @@ -1,513 +0,0 @@ -package com.nike.backstopper.testonly; - -import com.nike.backstopper.apierror.ApiError; -import com.nike.backstopper.apierror.ApiErrorWithMetadata; -import com.nike.backstopper.apierror.sample.SampleCoreApiError; -import com.nike.internal.util.MapBuilder; -import com.nike.internal.util.Pair; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; - -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.EnumSource; -import org.junit.jupiter.params.provider.MethodSource; -import org.springframework.boot.SpringApplication; -import org.springframework.context.ConfigurableApplicationContext; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -import io.restassured.http.ContentType; -import io.restassured.response.ExtractableResponse; -import serverconfig.classpathscan.Springboot1ClasspathScanConfig; -import serverconfig.directimport.Springboot1DirectImportConfig; -import testonly.componenttest.spring.reusable.error.SampleProjectApiError; -import testonly.componenttest.spring.reusable.model.RgbColor; -import testonly.componenttest.spring.reusable.model.SampleModel; - -import static com.nike.internal.util.testing.TestUtils.findFreePort; -import static io.restassured.RestAssured.given; -import static java.util.Collections.singletonList; -import static org.assertj.core.api.Assertions.assertThat; -import static testonly.componenttest.spring.reusable.controller.SampleController.CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH; -import static testonly.componenttest.spring.reusable.controller.SampleController.SAMPLE_PATH; -import static testonly.componenttest.spring.reusable.controller.SampleController.TRIGGER_UNHANDLED_ERROR_SUBPATH; -import static testonly.componenttest.spring.reusable.controller.SampleController.WITH_REQUIRED_QUERY_PARAM_SUBPATH; -import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.FOO_STRING_CANNOT_BE_BLANK; -import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.INVALID_RANGE_VALUE; -import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.NOT_RGB_COLOR_ENUM; -import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.RGB_COLOR_CANNOT_BE_NULL; -import static testonly.componenttest.spring.reusable.testutil.TestUtils.randomizedSampleModel; -import static testonly.componenttest.spring.reusable.testutil.TestUtils.verifyErrorReceived; - -/** - * Component test to verify that the functionality of {@code backstopper-spring-boot1} works as expected in a - * Spring Boot 1 environment, for both classpath-scanning and direct-import Backstopper configuration use cases. - * - * @author Nic Munroe - */ -public class BackstopperSpringboot1ComponentTest { - - private static final int CLASSPATH_SCAN_SERVER_PORT = findFreePort(); - private static final int DIRECT_IMPORT_SERVER_PORT = findFreePort(); - private static final ObjectMapper objectMapper = new ObjectMapper(); - - private static ConfigurableApplicationContext classpathScanServerAppContext; - private static ConfigurableApplicationContext directImportServerAppContext; - - @BeforeAll - public static void beforeClass() { - assertThat(CLASSPATH_SCAN_SERVER_PORT).isNotEqualTo(DIRECT_IMPORT_SERVER_PORT); - classpathScanServerAppContext = SpringApplication.run( - Springboot1ClasspathScanConfig.class, "--server.port=" + CLASSPATH_SCAN_SERVER_PORT - ); - directImportServerAppContext = SpringApplication.run( - Springboot1DirectImportConfig.class, "--server.port=" + DIRECT_IMPORT_SERVER_PORT - ); - } - - @AfterAll - public static void afterClass() { - SpringApplication.exit(classpathScanServerAppContext); - SpringApplication.exit(directImportServerAppContext); - } - - @SuppressWarnings("unused") - private enum ServerScenario { - CLASSPATH_SCAN_SERVER(CLASSPATH_SCAN_SERVER_PORT), - DIRECT_IMPORT_SERVER(DIRECT_IMPORT_SERVER_PORT); - - public final int serverPort; - - ServerScenario(int serverPort) { - this.serverPort = serverPort; - } - } - - // *************** SUCCESSFUL (NON ERROR) CALLS ****************** - @EnumSource(ServerScenario.class) - @ParameterizedTest - public void verify_basic_sample_get(ServerScenario scenario) throws IOException { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(scenario.serverPort) - .basePath(SAMPLE_PATH) - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - assertThat(response.statusCode()).isEqualTo(200); - SampleModel responseBody = objectMapper.readValue(response.asString(), SampleModel.class); - assertThat(responseBody).isNotNull(); - assertThat(responseBody.foo).isNotEmpty(); - assertThat(responseBody.range_0_to_42).isNotEmpty(); - assertThat(Integer.parseInt(responseBody.range_0_to_42)).isBetween(0, 42); - assertThat(responseBody.rgb_color).isNotEmpty(); - assertThat(RgbColor.toRgbColor(responseBody.rgb_color)).isNotNull(); - assertThat(responseBody.throw_manual_error).isFalse(); - } - - @EnumSource(ServerScenario.class) - @ParameterizedTest - public void verify_basic_sample_post(ServerScenario scenario) throws IOException { - SampleModel requestPayload = randomizedSampleModel(); - String requestPayloadAsString = objectMapper.writeValueAsString(requestPayload); - - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(scenario.serverPort) - .contentType(ContentType.JSON) - .basePath(SAMPLE_PATH) - .body(requestPayloadAsString) - .log().all() - .when() - .post() - .then() - .log().all() - .extract(); - - assertThat(response.statusCode()).isEqualTo(201); - SampleModel responseBody = objectMapper.readValue(response.asString(), SampleModel.class); - assertThat(responseBody).isNotNull(); - assertThat(responseBody.foo).isEqualTo(requestPayload.foo); - assertThat(responseBody.range_0_to_42).isEqualTo(requestPayload.range_0_to_42); - assertThat(responseBody.rgb_color).isEqualTo(requestPayload.rgb_color); - assertThat(responseBody.throw_manual_error).isEqualTo(requestPayload.throw_manual_error); - } - - // *************** JSR 303 AND ENDPOINT ERRORS ****************** - - @SuppressWarnings("unused") - private enum Jsr303SampleModelValidationScenario { - BLANK_FIELD_VIOLATION( - new SampleModel("", "42", "GREEN", false), - singletonList(FOO_STRING_CANNOT_BE_BLANK) - ), - INVALID_RANGE_VIOLATION( - new SampleModel("bar", "-1", "GREEN", false), - singletonList(INVALID_RANGE_VALUE) - ), - NULL_FIELD_VIOLATION( - new SampleModel("bar", "42", null, false), - singletonList(RGB_COLOR_CANNOT_BE_NULL) - ), - STRING_CONVERTS_TO_CLASSTYPE_VIOLATION( - new SampleModel("bar", "42", "car", false), - singletonList(NOT_RGB_COLOR_ENUM) - ), - MULTIPLE_VIOLATIONS( - new SampleModel(" \n\r\t ", "99", "tree", false), - Arrays.asList(FOO_STRING_CANNOT_BE_BLANK, INVALID_RANGE_VALUE, NOT_RGB_COLOR_ENUM) - ); - - public final SampleModel model; - public final List expectedErrors; - - Jsr303SampleModelValidationScenario( - SampleModel model, List expectedErrors - ) { - this.model = model; - this.expectedErrors = expectedErrors; - } - } - - public static List jsr303ValidationErrorScenariosDataProvider() { - List result = new ArrayList<>(); - for (Jsr303SampleModelValidationScenario violationScenario : Jsr303SampleModelValidationScenario.values()) { - for (ServerScenario serverScenario : ServerScenario.values()) { - result.add(new Object[]{violationScenario, serverScenario}); - } - } - return result; - } - - @MethodSource("jsr303ValidationErrorScenariosDataProvider") - @ParameterizedTest - public void verify_jsr303_validation_errors( - Jsr303SampleModelValidationScenario violationScenario, ServerScenario serverScenario - ) throws JsonProcessingException { - String requestPayloadAsString = objectMapper.writeValueAsString(violationScenario.model); - - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(serverScenario.serverPort) - .contentType(ContentType.JSON) - .basePath(SAMPLE_PATH) - .body(requestPayloadAsString) - .log().all() - .when() - .post() - .then() - .log().all() - .extract(); - - List expectedErrors = new ArrayList<>(); - for (ApiError expectedApiError : violationScenario.expectedErrors) { - String extraMetadataFieldValue = null; - - if (INVALID_RANGE_VALUE.equals(expectedApiError)) { - extraMetadataFieldValue = "range_0_to_42"; - } - else if (RGB_COLOR_CANNOT_BE_NULL.equals(expectedApiError) || NOT_RGB_COLOR_ENUM.equals(expectedApiError)) { - extraMetadataFieldValue = "rgb_color"; - } - - if (extraMetadataFieldValue != null) { - expectedApiError = new ApiErrorWithMetadata( - expectedApiError, - MapBuilder.builder("field", (Object) extraMetadataFieldValue).build() - ); - } - - expectedErrors.add(expectedApiError); - } - verifyErrorReceived(response, expectedErrors, 400); - } - - @EnumSource(ServerScenario.class) - @ParameterizedTest - public void verify_MANUALLY_THROWN_ERROR_is_thrown_when_requested(ServerScenario scenario) throws IOException { - SampleModel requestPayload = new SampleModel("bar", "42", "RED", true); - String requestPayloadAsString = objectMapper.writeValueAsString(requestPayload); - - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(scenario.serverPort) - .contentType(ContentType.JSON) - .basePath(SAMPLE_PATH) - .body(requestPayloadAsString) - .log().all() - .when() - .post() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleProjectApiError.MANUALLY_THROWN_ERROR); - // This code path also should add some custom headers to the response - assertThat(response.headers().getValues("rgbColorValue")).isEqualTo(singletonList(requestPayload.rgb_color)); - assertThat(response.headers().getValues("otherExtraMultivalueHeader")).isEqualTo(Arrays.asList("foo", "bar")); - } - - @EnumSource(ServerScenario.class) - @ParameterizedTest - public void verify_SOME_MEANINGFUL_ERROR_NAME_is_thrown_when_correct_endpoint_is_hit(ServerScenario scenario) { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(scenario.serverPort) - .basePath(SAMPLE_PATH + CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH) - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleProjectApiError.SOME_MEANINGFUL_ERROR_NAME); - } - - @EnumSource(ServerScenario.class) - @ParameterizedTest - public void verify_GENERIC_SERVICE_ERROR_is_thrown_when_correct_endpoint_is_hit(ServerScenario scenario) { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(scenario.serverPort) - .basePath(SAMPLE_PATH + TRIGGER_UNHANDLED_ERROR_SUBPATH) - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleCoreApiError.GENERIC_SERVICE_ERROR); - } - - // *************** FRAMEWORK ERRORS ****************** - - @EnumSource(ServerScenario.class) - @ParameterizedTest - public void verify_NOT_FOUND_returned_if_unknown_path_is_requested(ServerScenario scenario) { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(scenario.serverPort) - .basePath(UUID.randomUUID().toString()) - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleCoreApiError.NOT_FOUND); - } - - @EnumSource(ServerScenario.class) - @ParameterizedTest - public void verify_ERROR_THROWN_IN_SERVLET_FILTER_OUTSIDE_SPRING_returned_if_servlet_filter_trigger_occurs( - ServerScenario scenario - ) { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(scenario.serverPort) - .basePath(SAMPLE_PATH) - .header("throw-servlet-filter-exception", "true") - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleProjectApiError.ERROR_THROWN_IN_SERVLET_FILTER_OUTSIDE_SPRING); - } - - @EnumSource(ServerScenario.class) - @ParameterizedTest - public void verify_METHOD_NOT_ALLOWED_returned_if_known_path_is_requested_with_invalid_http_method( - ServerScenario scenario - ) { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(scenario.serverPort) - .basePath(SAMPLE_PATH) - .log().all() - .when() - .delete() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleCoreApiError.METHOD_NOT_ALLOWED); - } - - @EnumSource(ServerScenario.class) - @ParameterizedTest - public void verify_sample_get_fails_with_NO_ACCEPTABLE_REPRESENTATION_if_passed_invalid_accept_header( - ServerScenario scenario - ) { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(scenario.serverPort) - .basePath(SAMPLE_PATH) - .accept(ContentType.BINARY) - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleCoreApiError.NO_ACCEPTABLE_REPRESENTATION); - } - - @EnumSource(ServerScenario.class) - @ParameterizedTest - public void verify_sample_post_fails_with_UNSUPPORTED_MEDIA_TYPE_if_passed_invalid_content_type( - ServerScenario scenario - ) throws IOException { - SampleModel requestPayload = randomizedSampleModel(); - String requestPayloadAsString = objectMapper.writeValueAsString(requestPayload); - - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(scenario.serverPort) - .basePath(SAMPLE_PATH) - .body(requestPayloadAsString) - .contentType(ContentType.TEXT) - .log().all() - .when() - .post() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleCoreApiError.UNSUPPORTED_MEDIA_TYPE); - } - - @EnumSource(ServerScenario.class) - @ParameterizedTest - public void verify_MALFORMED_REQUEST_is_thrown_when_required_data_is_missing( - ServerScenario scenario - ) { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(scenario.serverPort) - .basePath(SAMPLE_PATH + WITH_REQUIRED_QUERY_PARAM_SUBPATH) - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - verifyErrorReceived( - response, - new ApiErrorWithMetadata( - SampleCoreApiError.MALFORMED_REQUEST, - Pair.of("missing_param_type", "int"), - Pair.of("missing_param_name", "requiredQueryParamValue") - ) - ); - } - - @EnumSource(ServerScenario.class) - @ParameterizedTest - public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert_type( - ServerScenario scenario - ) { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(scenario.serverPort) - .basePath(SAMPLE_PATH + WITH_REQUIRED_QUERY_PARAM_SUBPATH) - .queryParam("requiredQueryParamValue", "not-an-integer") - .log().all() - .when() - .get() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, new ApiErrorWithMetadata( - SampleCoreApiError.TYPE_CONVERSION_ERROR, - MapBuilder.builder("bad_property_name", (Object)"requiredQueryParamValue") - .put("bad_property_value", "not-an-integer") - .put("required_type", "int") - .build() - )); - } - - @EnumSource(ServerScenario.class) - @ParameterizedTest - public void verify_sample_post_fails_with_MISSING_EXPECTED_CONTENT_if_passed_empty_body( - ServerScenario scenario - ) { - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(scenario.serverPort) - .basePath(SAMPLE_PATH) - .contentType(ContentType.JSON) - .body("") - .log().all() - .when() - .post() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleCoreApiError.MISSING_EXPECTED_CONTENT); - } - - @EnumSource(ServerScenario.class) - @ParameterizedTest - public void verify_sample_post_fails_with_MALFORMED_REQUEST_if_passed_bad_json_body( - ServerScenario scenario - ) throws IOException { - SampleModel originalValidPayloadObj = randomizedSampleModel(); - String originalValidPayloadAsString = objectMapper.writeValueAsString(originalValidPayloadObj); - @SuppressWarnings("unchecked") - Map badRequestPayloadAsMap = objectMapper.readValue(originalValidPayloadAsString, Map.class); - badRequestPayloadAsMap.put("throw_manual_error", "not-a-boolean"); - String badJsonPayloadAsString = objectMapper.writeValueAsString(badRequestPayloadAsMap); - - ExtractableResponse response = - given() - .baseUri("http://localhost") - .port(scenario.serverPort) - .basePath(SAMPLE_PATH) - .contentType(ContentType.JSON) - .body(badJsonPayloadAsString) - .log().all() - .when() - .post() - .then() - .log().all() - .extract(); - - verifyErrorReceived(response, SampleCoreApiError.MALFORMED_REQUEST); - } -} diff --git a/testonly/testonly-springboot1/src/test/java/serverconfig/classpathscan/Springboot1ClasspathScanConfig.java b/testonly/testonly-springboot1/src/test/java/serverconfig/classpathscan/Springboot1ClasspathScanConfig.java deleted file mode 100644 index f3afd2b..0000000 --- a/testonly/testonly-springboot1/src/test/java/serverconfig/classpathscan/Springboot1ClasspathScanConfig.java +++ /dev/null @@ -1,48 +0,0 @@ -package serverconfig.classpathscan; - -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; - -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.web.servlet.FilterRegistrationBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.core.Ordered; - -import javax.validation.Validation; -import javax.validation.Validator; - -import testonly.componenttest.spring.reusable.error.SampleProjectApiErrorsImpl; -import testonly.componenttest.spring.reusable.testutil.ExplodingServletFilter; - -/** - * Springboot config that uses {@link ComponentScan} to integrate Backstopper via classpath scanning of the - * {@code com.nike.backstopper} package. - * - * @author Nic Munroe - */ -@SpringBootApplication -@ComponentScan(basePackages = { - // Component scan the core Backstopper+Springboot1 support. - "com.nike.backstopper", - // Component scan the controller. - "testonly.componenttest.spring.reusable.controller" -}) -public class Springboot1ClasspathScanConfig { - - @Bean - public ProjectApiErrors getProjectApiErrors() { - return new SampleProjectApiErrorsImpl(); - } - - @Bean - public Validator getJsr303Validator() { - return Validation.buildDefaultValidatorFactory().getValidator(); - } - - @Bean - public FilterRegistrationBean explodingServletFilter() { - FilterRegistrationBean frb = new FilterRegistrationBean(new ExplodingServletFilter()); - frb.setOrder(Ordered.HIGHEST_PRECEDENCE); - return frb; - } -} diff --git a/testonly/testonly-springboot1/src/test/java/serverconfig/directimport/Springboot1DirectImportConfig.java b/testonly/testonly-springboot1/src/test/java/serverconfig/directimport/Springboot1DirectImportConfig.java deleted file mode 100644 index 0871074..0000000 --- a/testonly/testonly-springboot1/src/test/java/serverconfig/directimport/Springboot1DirectImportConfig.java +++ /dev/null @@ -1,50 +0,0 @@ -package serverconfig.directimport; - -import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.handler.springboot.config.BackstopperSpringboot1Config; - -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.web.servlet.FilterRegistrationBean; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Import; -import org.springframework.core.Ordered; - -import javax.validation.Validation; -import javax.validation.Validator; - -import testonly.componenttest.spring.reusable.controller.SampleController; -import testonly.componenttest.spring.reusable.error.SampleProjectApiErrorsImpl; -import testonly.componenttest.spring.reusable.testutil.ExplodingServletFilter; - -/** - * Springboot config that uses {@link Import} to integrate Backstopper via direct import of - * {@link BackstopperSpringboot1Config}. - * - * @author Nic Munroe - */ -@SpringBootApplication -@Import({ - // Import core Backstopper+Springboot1 support. - BackstopperSpringboot1Config.class, - // Import the controller. - SampleController.class -}) -public class Springboot1DirectImportConfig { - - @Bean - public ProjectApiErrors getProjectApiErrors() { - return new SampleProjectApiErrorsImpl(); - } - - @Bean - public Validator getJsr303Validator() { - return Validation.buildDefaultValidatorFactory().getValidator(); - } - - @Bean - public FilterRegistrationBean explodingServletFilter() { - FilterRegistrationBean frb = new FilterRegistrationBean(new ExplodingServletFilter()); - frb.setOrder(Ordered.HIGHEST_PRECEDENCE); - return frb; - } -} diff --git a/testonly/testonly-springboot1/src/test/resources/logback.xml b/testonly/testonly-springboot1/src/test/resources/logback.xml deleted file mode 100644 index 80adb28..0000000 --- a/testonly/testonly-springboot1/src/test/resources/logback.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - %d{yyyy-MM-dd_HH:mm:ss.SSS} [%thread] |-%-5level %logger{36} - %msg%n - - - - - - - \ No newline at end of file From 991314a212b02aed61df18737fc142316d64ca4b Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Wed, 11 Sep 2024 15:59:51 -0700 Subject: [PATCH 23/42] Migrate testonly-spring-reusable-test-support from javax to jakarta, and add in spring6-webmvc testonly module --- settings.gradle | 10 +-- .../build.gradle | 11 +-- .../reusable/controller/SampleController.java | 11 ++- .../error/SampleProjectApiErrorsImpl.java | 2 +- .../SpringMvcJettyComponentTestServer.java | 2 +- .../spring/reusable/model/SampleModel.java | 4 +- .../testutil/ExplodingServletFilter.java | 8 +- testonly/testonly-spring5-webmvc/README.md | 16 ---- testonly/testonly-spring6-webmvc/README.md | 20 +++++ .../build.gradle | 26 ++---- ...topperSpring_6_0_WebMvcComponentTest.java} | 83 ++++++++++++++++--- ...Spring_6_0_WebMvcClasspathScanConfig.java} | 8 +- .../Spring_6_0_WebMvcDirectImportConfig.java} | 9 +- .../src/test/resources/logback.xml | 0 14 files changed, 131 insertions(+), 79 deletions(-) delete mode 100644 testonly/testonly-spring5-webmvc/README.md create mode 100644 testonly/testonly-spring6-webmvc/README.md rename testonly/{testonly-spring5-webmvc => testonly-spring6-webmvc}/build.gradle (51%) rename testonly/{testonly-spring5-webmvc/src/test/java/com/nike/backstopper/testonly/BackstopperSpring5WebMvcComponentTest.java => testonly-spring6-webmvc/src/test/java/com/nike/backstopper/testonly/BackstopperSpring_6_0_WebMvcComponentTest.java} (86%) rename testonly/{testonly-spring5-webmvc/src/test/java/serverconfig/classpathscan/Spring5WebMvcClasspathScanConfig.java => testonly-spring6-webmvc/src/test/java/serverconfig/classpathscan/Spring_6_0_WebMvcClasspathScanConfig.java} (85%) rename testonly/{testonly-spring5-webmvc/src/test/java/serverconfig/directimport/Spring5WebMvcDirectImportConfig.java => testonly-spring6-webmvc/src/test/java/serverconfig/directimport/Spring_6_0_WebMvcDirectImportConfig.java} (86%) rename testonly/{testonly-spring5-webmvc => testonly-spring6-webmvc}/src/test/resources/logback.xml (100%) diff --git a/settings.gradle b/settings.gradle index f58293c..0282185 100644 --- a/settings.gradle +++ b/settings.gradle @@ -11,14 +11,12 @@ include "nike-internal-util", "backstopper-spring-web-mvc", "backstopper-spring-web-flux", "backstopper-spring-boot3-webmvc", -// // Test-only modules (not published) -// "testonly:testonly-spring-reusable-test-support", -// "testonly:testonly-spring4-webmvc", -// "testonly:testonly-spring5-webmvc", -// "testonly:testonly-springboot1", + // Test-only modules (not published) + "testonly:testonly-spring-reusable-test-support", + "testonly:testonly-spring6-webmvc", // "testonly:testonly-springboot2-webmvc", // "testonly:testonly-springboot2-webflux", -// // Sample modules (not published) + // Sample modules (not published) "samples:sample-spring-web-mvc", "samples:sample-spring-boot3-webmvc", "samples:sample-spring-boot3-webflux" \ No newline at end of file diff --git a/testonly/testonly-spring-reusable-test-support/build.gradle b/testonly/testonly-spring-reusable-test-support/build.gradle index 188007c..fffe038 100644 --- a/testonly/testonly-spring-reusable-test-support/build.gradle +++ b/testonly/testonly-spring-reusable-test-support/build.gradle @@ -1,8 +1,5 @@ evaluationDependsOn(':') -sourceCompatibility = JavaVersion.VERSION_1_8 -targetCompatibility = JavaVersion.VERSION_1_8 - test { useJUnitPlatform() } @@ -11,11 +8,11 @@ dependencies { compileOnly( project(":backstopper-spring-web-mvc"), project(":backstopper-custom-validators"), - "org.springframework:spring-webmvc:$spring4Version", + "org.springframework:spring-webmvc:$spring6Version", "org.eclipse.jetty:jetty-webapp:$jettyVersion", - "org.hibernate:hibernate-validator:$hibernateValidatorVersion", + "org.hibernate.validator:hibernate-validator:$hibernateValidatorVersion", "org.jetbrains:annotations:$jetbrainsAnnotationsVersion", - "javax.servlet:javax.servlet-api:$servletApiVersion", + "jakarta.servlet:jakarta.servlet-api:$servletApiVersion", "com.fasterxml.jackson.core:jackson-core:$jacksonVersion", "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion", "io.rest-assured:rest-assured:$restAssuredVersion", @@ -23,6 +20,6 @@ dependencies { ) testImplementation( project(":backstopper-reusable-tests-junit5"), - "org.springframework:spring-webmvc:$spring4Version", + "org.springframework:spring-webmvc:$spring6Version", ) } diff --git a/testonly/testonly-spring-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/controller/SampleController.java b/testonly/testonly-spring-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/controller/SampleController.java index 8a12068..b99db46 100644 --- a/testonly/testonly-spring-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/controller/SampleController.java +++ b/testonly/testonly-spring-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/controller/SampleController.java @@ -8,6 +8,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; @@ -16,8 +17,7 @@ import java.util.Arrays; import java.util.UUID; -import javax.validation.Valid; - +import jakarta.validation.Valid; import testonly.componenttest.spring.reusable.error.SampleProjectApiError; import testonly.componenttest.spring.reusable.model.RgbColor; import testonly.componenttest.spring.reusable.model.SampleModel; @@ -39,6 +39,7 @@ public class SampleController { public static final String SAMPLE_PATH = "/sample"; public static final String CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH = "/coreErrorWrapper"; public static final String WITH_REQUIRED_QUERY_PARAM_SUBPATH = "/withRequiredQueryParam"; + public static final String WITH_REQUIRED_HEADER_SUBPATH = "/withRequiredHeader"; public static final String TRIGGER_UNHANDLED_ERROR_SUBPATH = "/triggerUnhandledError"; public static int nextRangeInt(int lowerBound, int upperBound) { @@ -96,6 +97,12 @@ public String withRequiredQueryParam(@RequestParam(name = "requiredQueryParamVal return "You passed in " + someRequiredQueryParam + " for the required query param value"; } + @GetMapping(path = WITH_REQUIRED_HEADER_SUBPATH, produces = "text/plain") + @ResponseBody + public String withRequiredHeader(@RequestHeader(name = "requiredHeaderValue") int someRequiredHeader) { + return "You passed in " + someRequiredHeader + " for the required header value"; + } + @GetMapping(path = TRIGGER_UNHANDLED_ERROR_SUBPATH) public void triggerUnhandledError() { throw new RuntimeException("This should be handled by SpringUnhandledExceptionHandler."); diff --git a/testonly/testonly-spring-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/error/SampleProjectApiErrorsImpl.java b/testonly/testonly-spring-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/error/SampleProjectApiErrorsImpl.java index 48e4c65..df2d1a3 100644 --- a/testonly/testonly-spring-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/error/SampleProjectApiErrorsImpl.java +++ b/testonly/testonly-spring-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/error/SampleProjectApiErrorsImpl.java @@ -9,7 +9,7 @@ import java.util.Arrays; import java.util.List; -import javax.inject.Singleton; +import jakarta.inject.Singleton; /** * Returns the project specific errors for the {@code testonly-spring*} component tests. diff --git a/testonly/testonly-spring-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/jettyserver/SpringMvcJettyComponentTestServer.java b/testonly/testonly-spring-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/jettyserver/SpringMvcJettyComponentTestServer.java index 70ba78f..c259524 100644 --- a/testonly/testonly-spring-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/jettyserver/SpringMvcJettyComponentTestServer.java +++ b/testonly/testonly-spring-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/jettyserver/SpringMvcJettyComponentTestServer.java @@ -12,7 +12,7 @@ import java.util.EnumSet; -import javax.servlet.DispatcherType; +import jakarta.servlet.DispatcherType; import testonly.componenttest.spring.reusable.testutil.ExplodingServletFilter; diff --git a/testonly/testonly-spring-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/model/SampleModel.java b/testonly/testonly-spring-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/model/SampleModel.java index 3fc2613..3396a43 100644 --- a/testonly/testonly-spring-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/model/SampleModel.java +++ b/testonly/testonly-spring-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/model/SampleModel.java @@ -4,10 +4,10 @@ import com.nike.backstopper.apierror.sample.SampleCoreApiError; import com.nike.backstopper.validation.constraints.StringConvertsToClassType; -import org.hibernate.validator.constraints.NotBlank; +import jakarta.validation.constraints.NotBlank; import org.hibernate.validator.constraints.Range; -import javax.validation.constraints.NotNull; +import jakarta.validation.constraints.NotNull; import testonly.componenttest.spring.reusable.error.SampleProjectApiError; import testonly.componenttest.spring.reusable.error.SampleProjectApiErrorsImpl; diff --git a/testonly/testonly-spring-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/testutil/ExplodingServletFilter.java b/testonly/testonly-spring-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/testutil/ExplodingServletFilter.java index 6157f81..0a2f650 100644 --- a/testonly/testonly-spring-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/testutil/ExplodingServletFilter.java +++ b/testonly/testonly-spring-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/testutil/ExplodingServletFilter.java @@ -6,10 +6,10 @@ import java.io.IOException; -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import testonly.componenttest.spring.reusable.error.SampleProjectApiError; diff --git a/testonly/testonly-spring5-webmvc/README.md b/testonly/testonly-spring5-webmvc/README.md deleted file mode 100644 index 89091fa..0000000 --- a/testonly/testonly-spring5-webmvc/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# Backstopper - testonly-spring5-webmvc - -Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 7 and greater. - -This submodule contains tests to verify that the [backstopper-spring-web-mvc](../../backstopper-spring-web-mvc) -module's functionality works as expected in Spring 5 environments, for both classpath-scanning and direct-import -Backstopper configuration use cases. - -## More Info - -See the [base project README.md](../../README.md), [User Guide](../../USER_GUIDE.md), and Backstopper repository -source code and javadocs for all further information. - -## License - -Backstopper is released under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/testonly/testonly-spring6-webmvc/README.md b/testonly/testonly-spring6-webmvc/README.md new file mode 100644 index 0000000..0ceb5bb --- /dev/null +++ b/testonly/testonly-spring6-webmvc/README.md @@ -0,0 +1,20 @@ +# Backstopper - testonly-spring6-webmvc + +Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 17 and greater. + +(NOTE: The [Backstopper 1.x branch](https://github.com/Nike-Inc/backstopper/tree/v1.x) contains a version of +Backstopper for Java 7+, and for the `javax` ecosystem. The current Backstopper supports Java 17+ and the `jakarta` +ecosystem. The Backstopper 1.x releases also contain support for Spring 4 and 5, and Springboot 1 and 2.) + +This submodule contains tests to verify that the [backstopper-spring-web-mvc](../../backstopper-spring-web-mvc) +module's functionality works as expected in Spring 6.0.x environments, for both classpath-scanning and direct-import +Backstopper configuration use cases. + +## More Info + +See the [base project README.md](../../README.md), [User Guide](../../USER_GUIDE.md), and Backstopper repository +source code and javadocs for all further information. + +## License + +Backstopper is released under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/testonly/testonly-spring5-webmvc/build.gradle b/testonly/testonly-spring6-webmvc/build.gradle similarity index 51% rename from testonly/testonly-spring5-webmvc/build.gradle rename to testonly/testonly-spring6-webmvc/build.gradle index 3fc9e18..3464437 100644 --- a/testonly/testonly-spring5-webmvc/build.gradle +++ b/testonly/testonly-spring6-webmvc/build.gradle @@ -1,18 +1,5 @@ evaluationDependsOn(':') -sourceCompatibility = JavaVersion.VERSION_1_8 -targetCompatibility = JavaVersion.VERSION_1_8 - -ext { - // Spring 5 has some requirements around jackson and hibernate/JSR 303 validation that mean we need to upgrade - // versions for a few libraries. - jacksonVersionForSpring5 = '2.9.9' - javaxValidationVersionForSpring5 = '2.0.1.Final' - hibernateValidatorVersionForSpring5 = '6.0.16.Final' - elApiVersion = '3.0.1-b06' - elImplVersion = '3.0.1-b11' -} - test { useJUnitPlatform() } @@ -21,13 +8,12 @@ dependencies { implementation( project(":backstopper-spring-web-mvc"), project(":backstopper-custom-validators"), - "org.springframework:spring-webmvc:$spring5Version", + "org.springframework:spring-webmvc:$spring6Version", "ch.qos.logback:logback-classic:$logbackVersion", - "com.fasterxml.jackson.core:jackson-core:$jacksonVersionForSpring5", - "com.fasterxml.jackson.core:jackson-databind:$jacksonVersionForSpring5", - "org.hibernate:hibernate-validator:$hibernateValidatorVersionForSpring5", - "javax.el:javax.el-api:$elApiVersion", - "org.glassfish:javax.el:$elImplVersion", + "com.fasterxml.jackson.core:jackson-core:$jacksonVersion", + "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion", + "org.hibernate.validator:hibernate-validator:$hibernateValidatorVersion", + "org.glassfish.expressly:expressly:$glassfishExpresslyVersion", "org.eclipse.jetty:jetty-webapp:$jettyVersion", ) testImplementation( @@ -39,7 +25,5 @@ dependencies { "org.mockito:mockito-core:$mockitoVersion", "org.assertj:assertj-core:$assertJVersion", "io.rest-assured:rest-assured:$restAssuredVersion", - // The jaxb-api is needed for building on the java 11 JDK as these classes were moved out of the Java SE libs. - "javax.xml.bind:jaxb-api:$jaxbApiVersion", ) } diff --git a/testonly/testonly-spring5-webmvc/src/test/java/com/nike/backstopper/testonly/BackstopperSpring5WebMvcComponentTest.java b/testonly/testonly-spring6-webmvc/src/test/java/com/nike/backstopper/testonly/BackstopperSpring_6_0_WebMvcComponentTest.java similarity index 86% rename from testonly/testonly-spring5-webmvc/src/test/java/com/nike/backstopper/testonly/BackstopperSpring5WebMvcComponentTest.java rename to testonly/testonly-spring6-webmvc/src/test/java/com/nike/backstopper/testonly/BackstopperSpring_6_0_WebMvcComponentTest.java index 1a4e4bd..dfb7909 100644 --- a/testonly/testonly-spring5-webmvc/src/test/java/com/nike/backstopper/testonly/BackstopperSpring5WebMvcComponentTest.java +++ b/testonly/testonly-spring6-webmvc/src/test/java/com/nike/backstopper/testonly/BackstopperSpring_6_0_WebMvcComponentTest.java @@ -24,8 +24,8 @@ import io.restassured.http.ContentType; import io.restassured.response.ExtractableResponse; -import serverconfig.classpathscan.Spring5WebMvcClasspathScanConfig; -import serverconfig.directimport.Spring5WebMvcDirectImportConfig; +import serverconfig.classpathscan.Spring_6_0_WebMvcClasspathScanConfig; +import serverconfig.directimport.Spring_6_0_WebMvcDirectImportConfig; import testonly.componenttest.spring.reusable.error.SampleProjectApiError; import testonly.componenttest.spring.reusable.jettyserver.SpringMvcJettyComponentTestServer; import testonly.componenttest.spring.reusable.model.RgbColor; @@ -38,6 +38,7 @@ import static testonly.componenttest.spring.reusable.controller.SampleController.CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH; import static testonly.componenttest.spring.reusable.controller.SampleController.SAMPLE_PATH; import static testonly.componenttest.spring.reusable.controller.SampleController.TRIGGER_UNHANDLED_ERROR_SUBPATH; +import static testonly.componenttest.spring.reusable.controller.SampleController.WITH_REQUIRED_HEADER_SUBPATH; import static testonly.componenttest.spring.reusable.controller.SampleController.WITH_REQUIRED_QUERY_PARAM_SUBPATH; import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.FOO_STRING_CANNOT_BE_BLANK; import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.INVALID_RANGE_VALUE; @@ -48,22 +49,23 @@ /** * Component test to verify that the functionality of {@code backstopper-spring-web-mvc} works as expected in a - * Spring 5 environment, for both classpath-scanning and direct-import Backstopper configuration use cases. + * Spring 6.0.x environment, for both classpath-scanning and direct-import Backstopper configuration use cases. * * @author Nic Munroe */ -public class BackstopperSpring5WebMvcComponentTest { +@SuppressWarnings({"NewClassNamingConvention", "ClassEscapesDefinedScope"}) +public class BackstopperSpring_6_0_WebMvcComponentTest { private static final int CLASSPATH_SCAN_SERVER_PORT = findFreePort(); private static final int DIRECT_IMPORT_SERVER_PORT = findFreePort(); private static final ObjectMapper objectMapper = new ObjectMapper(); private static final SpringMvcJettyComponentTestServer classpathScanServer = new SpringMvcJettyComponentTestServer( - CLASSPATH_SCAN_SERVER_PORT, Spring5WebMvcClasspathScanConfig.class + CLASSPATH_SCAN_SERVER_PORT, Spring_6_0_WebMvcClasspathScanConfig.class ); private static final SpringMvcJettyComponentTestServer directImportServer = new SpringMvcJettyComponentTestServer( - DIRECT_IMPORT_SERVER_PORT, Spring5WebMvcDirectImportConfig.class + DIRECT_IMPORT_SERVER_PORT, Spring_6_0_WebMvcDirectImportConfig.class ); @BeforeAll @@ -79,7 +81,6 @@ public static void afterClass() throws Exception { directImportServer.shutdownServer(); } - @SuppressWarnings("unused") private enum ServerScenario { CLASSPATH_SCAN_SERVER(CLASSPATH_SCAN_SERVER_PORT), DIRECT_IMPORT_SERVER(DIRECT_IMPORT_SERVER_PORT); @@ -408,7 +409,7 @@ public void verify_sample_post_fails_with_UNSUPPORTED_MEDIA_TYPE_if_passed_inval @EnumSource(ServerScenario.class) @ParameterizedTest - public void verify_MALFORMED_REQUEST_is_thrown_when_required_data_is_missing( + public void verify_MALFORMED_REQUEST_is_thrown_when_required_query_param_is_missing( ServerScenario scenario ) { ExtractableResponse response = @@ -427,15 +428,16 @@ public void verify_MALFORMED_REQUEST_is_thrown_when_required_data_is_missing( response, new ApiErrorWithMetadata( SampleCoreApiError.MALFORMED_REQUEST, + Pair.of("missing_param_name", "requiredQueryParamValue"), Pair.of("missing_param_type", "int"), - Pair.of("missing_param_name", "requiredQueryParamValue") + Pair.of("required_location", "query_param") ) ); } @EnumSource(ServerScenario.class) @ParameterizedTest - public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert_type( + public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert_type_for_query_param( ServerScenario scenario ) { ExtractableResponse response = @@ -453,8 +455,65 @@ public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert verifyErrorReceived(response, new ApiErrorWithMetadata( SampleCoreApiError.TYPE_CONVERSION_ERROR, - MapBuilder.builder("bad_property_name", (Object)"requiredQueryParamValue") - .put("bad_property_value", "not-an-integer") + MapBuilder.builder("bad_property_name", (Object) "requiredQueryParamValue") + .put("bad_property_value","not-an-integer") + .put("required_location","query_param") + .put("required_type", "int") + .build() + )); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_MALFORMED_REQUEST_is_thrown_when_required_header_is_missing( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + WITH_REQUIRED_HEADER_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived( + response, + new ApiErrorWithMetadata( + SampleCoreApiError.MALFORMED_REQUEST, + Pair.of("missing_param_name", "requiredHeaderValue"), + Pair.of("missing_param_type", "int"), + Pair.of("required_location", "header") + ) + ); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert_type_for_header( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + WITH_REQUIRED_HEADER_SUBPATH) + .header("requiredHeaderValue", "not-an-integer") + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, new ApiErrorWithMetadata( + SampleCoreApiError.TYPE_CONVERSION_ERROR, + MapBuilder.builder("bad_property_name", (Object) "requiredHeaderValue") + .put("bad_property_value","not-an-integer") + .put("required_location","header") .put("required_type", "int") .build() )); diff --git a/testonly/testonly-spring5-webmvc/src/test/java/serverconfig/classpathscan/Spring5WebMvcClasspathScanConfig.java b/testonly/testonly-spring6-webmvc/src/test/java/serverconfig/classpathscan/Spring_6_0_WebMvcClasspathScanConfig.java similarity index 85% rename from testonly/testonly-spring5-webmvc/src/test/java/serverconfig/classpathscan/Spring5WebMvcClasspathScanConfig.java rename to testonly/testonly-spring6-webmvc/src/test/java/serverconfig/classpathscan/Spring_6_0_WebMvcClasspathScanConfig.java index 8e12589..c194b14 100644 --- a/testonly/testonly-spring5-webmvc/src/test/java/serverconfig/classpathscan/Spring5WebMvcClasspathScanConfig.java +++ b/testonly/testonly-spring6-webmvc/src/test/java/serverconfig/classpathscan/Spring_6_0_WebMvcClasspathScanConfig.java @@ -7,8 +7,8 @@ import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.EnableWebMvc; -import javax.validation.Validation; -import javax.validation.Validator; +import jakarta.validation.Validation; +import jakarta.validation.Validator; import testonly.componenttest.spring.reusable.error.SampleProjectApiErrorsImpl; @@ -26,7 +26,8 @@ "testonly.componenttest.spring.reusable.controller" }) @EnableWebMvc -public class Spring5WebMvcClasspathScanConfig { +@SuppressWarnings("unused") +public class Spring_6_0_WebMvcClasspathScanConfig { @Bean public ProjectApiErrors getProjectApiErrors() { @@ -35,6 +36,7 @@ public ProjectApiErrors getProjectApiErrors() { @Bean public Validator getJsr303Validator() { + //noinspection resource return Validation.buildDefaultValidatorFactory().getValidator(); } } diff --git a/testonly/testonly-spring5-webmvc/src/test/java/serverconfig/directimport/Spring5WebMvcDirectImportConfig.java b/testonly/testonly-spring6-webmvc/src/test/java/serverconfig/directimport/Spring_6_0_WebMvcDirectImportConfig.java similarity index 86% rename from testonly/testonly-spring5-webmvc/src/test/java/serverconfig/directimport/Spring5WebMvcDirectImportConfig.java rename to testonly/testonly-spring6-webmvc/src/test/java/serverconfig/directimport/Spring_6_0_WebMvcDirectImportConfig.java index 2367e78..603ea95 100644 --- a/testonly/testonly-spring5-webmvc/src/test/java/serverconfig/directimport/Spring5WebMvcDirectImportConfig.java +++ b/testonly/testonly-spring6-webmvc/src/test/java/serverconfig/directimport/Spring_6_0_WebMvcDirectImportConfig.java @@ -8,9 +8,8 @@ import org.springframework.context.annotation.Import; import org.springframework.web.servlet.config.annotation.EnableWebMvc; -import javax.validation.Validation; -import javax.validation.Validator; - +import jakarta.validation.Validation; +import jakarta.validation.Validator; import testonly.componenttest.spring.reusable.controller.SampleController; import testonly.componenttest.spring.reusable.error.SampleProjectApiErrorsImpl; @@ -28,7 +27,8 @@ SampleController.class }) @EnableWebMvc -public class Spring5WebMvcDirectImportConfig { +@SuppressWarnings("unused") +public class Spring_6_0_WebMvcDirectImportConfig { @Bean public ProjectApiErrors getProjectApiErrors() { @@ -37,6 +37,7 @@ public ProjectApiErrors getProjectApiErrors() { @Bean public Validator getJsr303Validator() { + //noinspection resource return Validation.buildDefaultValidatorFactory().getValidator(); } } diff --git a/testonly/testonly-spring5-webmvc/src/test/resources/logback.xml b/testonly/testonly-spring6-webmvc/src/test/resources/logback.xml similarity index 100% rename from testonly/testonly-spring5-webmvc/src/test/resources/logback.xml rename to testonly/testonly-spring6-webmvc/src/test/resources/logback.xml From 0c99819e1ee486a01ee890e24ac5936c712647f5 Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Wed, 11 Sep 2024 16:21:12 -0700 Subject: [PATCH 24/42] Make a split between spring 6.0 and 6.1 and add a testonly module for 6.1 --- backstopper-spring-boot3-webmvc/build.gradle | 6 +- backstopper-spring-web-flux/build.gradle | 6 +- backstopper-spring-web-mvc/build.gradle | 6 +- backstopper-spring-web/build.gradle | 10 +- build.gradle | 7 +- .../sample-spring-boot3-webflux/build.gradle | 4 +- .../sample-spring-boot3-webmvc/build.gradle | 4 +- samples/sample-spring-web-mvc/build.gradle | 2 +- settings.gradle | 3 +- .../README.md | 0 .../build.gradle | 2 +- ...stopperSpring_6_0_WebMvcComponentTest.java | 0 .../Spring_6_0_WebMvcClasspathScanConfig.java | 0 .../Spring_6_0_WebMvcDirectImportConfig.java | 0 .../src/test/resources/logback.xml | 0 testonly/testonly-spring-6_1-webmvc/README.md | 20 + .../testonly-spring-6_1-webmvc/build.gradle | 29 + ...stopperSpring_6_1_WebMvcComponentTest.java | 572 ++++++++++++++++++ .../Spring_6_1_WebMvcClasspathScanConfig.java | 42 ++ .../Spring_6_1_WebMvcDirectImportConfig.java | 43 ++ .../src/test/resources/logback.xml | 11 + .../build.gradle | 4 +- 22 files changed, 745 insertions(+), 26 deletions(-) rename testonly/{testonly-spring6-webmvc => testonly-spring-6_0-webmvc}/README.md (100%) rename testonly/{testonly-spring6-webmvc => testonly-spring-6_0-webmvc}/build.gradle (94%) rename testonly/{testonly-spring6-webmvc => testonly-spring-6_0-webmvc}/src/test/java/com/nike/backstopper/testonly/BackstopperSpring_6_0_WebMvcComponentTest.java (100%) rename testonly/{testonly-spring6-webmvc => testonly-spring-6_0-webmvc}/src/test/java/serverconfig/classpathscan/Spring_6_0_WebMvcClasspathScanConfig.java (100%) rename testonly/{testonly-spring6-webmvc => testonly-spring-6_0-webmvc}/src/test/java/serverconfig/directimport/Spring_6_0_WebMvcDirectImportConfig.java (100%) rename testonly/{testonly-spring6-webmvc => testonly-spring-6_0-webmvc}/src/test/resources/logback.xml (100%) create mode 100644 testonly/testonly-spring-6_1-webmvc/README.md create mode 100644 testonly/testonly-spring-6_1-webmvc/build.gradle create mode 100644 testonly/testonly-spring-6_1-webmvc/src/test/java/com/nike/backstopper/testonly/BackstopperSpring_6_1_WebMvcComponentTest.java create mode 100644 testonly/testonly-spring-6_1-webmvc/src/test/java/serverconfig/classpathscan/Spring_6_1_WebMvcClasspathScanConfig.java create mode 100644 testonly/testonly-spring-6_1-webmvc/src/test/java/serverconfig/directimport/Spring_6_1_WebMvcDirectImportConfig.java create mode 100644 testonly/testonly-spring-6_1-webmvc/src/test/resources/logback.xml diff --git a/backstopper-spring-boot3-webmvc/build.gradle b/backstopper-spring-boot3-webmvc/build.gradle index 80cb41e..71ada5e 100644 --- a/backstopper-spring-boot3-webmvc/build.gradle +++ b/backstopper-spring-boot3-webmvc/build.gradle @@ -6,8 +6,8 @@ dependencies { ) compileOnly( "org.jetbrains:annotations:$jetbrainsAnnotationsVersion", - "org.springframework.boot:spring-boot-autoconfigure:$springboot3Version", - "org.springframework:spring-webmvc:$spring6Version", + "org.springframework.boot:spring-boot-autoconfigure:$springboot3_3Version", + "org.springframework:spring-webmvc:$spring6_0Version", "jakarta.servlet:jakarta.servlet-api:$servletApiVersion", ) testImplementation( @@ -19,7 +19,7 @@ dependencies { "jakarta.servlet:jakarta.servlet-api:$servletApiVersion", "io.rest-assured:rest-assured:$restAssuredVersion", "jakarta.servlet:jakarta.servlet-api:$servletApiVersion", - "org.springframework.boot:spring-boot-starter-web:$springboot3Version", + "org.springframework.boot:spring-boot-starter-web:$springboot3_3Version", "org.hibernate.validator:hibernate-validator:$hibernateValidatorVersion", "org.glassfish.expressly:expressly:$glassfishExpresslyVersion", ) diff --git a/backstopper-spring-web-flux/build.gradle b/backstopper-spring-web-flux/build.gradle index 2ce61e8..896edd2 100644 --- a/backstopper-spring-web-flux/build.gradle +++ b/backstopper-spring-web-flux/build.gradle @@ -7,8 +7,8 @@ dependencies { ) compileOnly( "org.jetbrains:annotations:$jetbrainsAnnotationsVersion", - "org.springframework:spring-webflux:$spring6Version", - "org.springframework:spring-context:$spring6Version", + "org.springframework:spring-webflux:$spring6_0Version", + "org.springframework:spring-context:$spring6_0Version", ) testImplementation( project(":backstopper-core").sourceSets.test.output, @@ -20,7 +20,7 @@ dependencies { "org.assertj:assertj-core:$assertJVersion", "com.tngtech.java:junit-dataprovider:$junitDataproviderVersion", "io.rest-assured:rest-assured:$restAssuredVersion", - "org.springframework.boot:spring-boot-starter-webflux:$springboot3Version", + "org.springframework.boot:spring-boot-starter-webflux:$springboot3_3Version", "jakarta.validation:jakarta.validation-api:$jakartaValidationVersion", "org.hibernate.validator:hibernate-validator:$hibernateValidatorVersion", "org.glassfish.expressly:expressly:$glassfishExpresslyVersion", diff --git a/backstopper-spring-web-mvc/build.gradle b/backstopper-spring-web-mvc/build.gradle index 1cb0dd5..cddb62c 100644 --- a/backstopper-spring-web-mvc/build.gradle +++ b/backstopper-spring-web-mvc/build.gradle @@ -8,7 +8,7 @@ dependencies { ) compileOnly( "org.jetbrains:annotations:$jetbrainsAnnotationsVersion", - "org.springframework:spring-webmvc:$spring6Version", + "org.springframework:spring-webmvc:$spring6_0Version", "jakarta.servlet:jakarta.servlet-api:$servletApiVersion", ) testImplementation( @@ -20,11 +20,11 @@ dependencies { "org.assertj:assertj-core:$assertJVersion", "com.tngtech.java:junit-dataprovider:$junitDataproviderVersion", "org.hamcrest:hamcrest-all:$hamcrestVersion", - "org.springframework:spring-test:$spring6Version", + "org.springframework:spring-test:$spring6_0Version", "org.hibernate.validator:hibernate-validator:$hibernateValidatorVersion", "org.glassfish.expressly:expressly:$glassfishExpresslyVersion", "jakarta.servlet:jakarta.servlet-api:$servletApiVersion", - "org.springframework:spring-webmvc:$spring6Version", + "org.springframework:spring-webmvc:$spring6_0Version", "org.springframework.security:spring-security-core:$springSecurityVersion", ) } diff --git a/backstopper-spring-web/build.gradle b/backstopper-spring-web/build.gradle index bea7da1..f5afe0a 100644 --- a/backstopper-spring-web/build.gradle +++ b/backstopper-spring-web/build.gradle @@ -6,8 +6,8 @@ dependencies { ) compileOnly( "org.jetbrains:annotations:$jetbrainsAnnotationsVersion", - "org.springframework:spring-web:$spring6Version", - "org.springframework:spring-context:$spring6Version", + "org.springframework:spring-web:$spring6_0Version", + "org.springframework:spring-context:$spring6_0Version", ) testImplementation( project(":backstopper-core").sourceSets.test.output, @@ -20,10 +20,10 @@ dependencies { "com.fasterxml.jackson.core:jackson-core:$jacksonVersion", "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion", "org.hibernate.validator:hibernate-validator:$hibernateValidatorVersion", - "org.springframework:spring-web:$spring6Version", - "org.springframework:spring-context:$spring6Version", + "org.springframework:spring-web:$spring6_0Version", + "org.springframework:spring-context:$spring6_0Version", "org.springframework.security:spring-security-core:$springSecurityVersion", - "org.springframework:spring-webmvc:$spring6Version", + "org.springframework:spring-webmvc:$spring6_0Version", "jakarta.servlet:jakarta.servlet-api:$servletApiVersion", ) } diff --git a/build.gradle b/build.gradle index 3793863..7e8c836 100644 --- a/build.gradle +++ b/build.gradle @@ -86,9 +86,10 @@ ext { jakartaInjectVersion = '2.0.1' jakartaValidationVersion = '3.0.2' servletApiVersion = '6.0.0' // Compatible with Jakarta EE 10 - spring6Version = '6.0.23' // Compatible with Jakarta EE 9/10 - springSecurityVersion = '6.1.9' // Closest spring secrity version to our pinned spring6Version, but without going over to avoid versions being transitively bumped above what we want for testing. - springboot3Version = '3.3.3' + spring6_0Version = '6.0.23' // Compatible with Jakarta EE 9/10 + spring6_1Version = '6.1.12' // Compatible with Jakarta EE 9/10 + springSecurityVersion = '6.1.9' // Closest spring security version to our pinned spring6_0Version, but without going over to avoid versions being transitively bumped above what we want for testing. + springboot3_3Version = '3.3.3' jersey1Version = '1.19.2' jersey2Version = '2.23.2' jaxRsVersion = '2.0.1' diff --git a/samples/sample-spring-boot3-webflux/build.gradle b/samples/sample-spring-boot3-webflux/build.gradle index 4e72b8f..38a68ec 100644 --- a/samples/sample-spring-boot3-webflux/build.gradle +++ b/samples/sample-spring-boot3-webflux/build.gradle @@ -5,7 +5,7 @@ buildscript { mavenCentral() } dependencies { - classpath("org.springframework.boot:spring-boot-gradle-plugin:${springboot3Version}") + classpath("org.springframework.boot:spring-boot-gradle-plugin:${springboot3_3Version}") } } @@ -21,7 +21,7 @@ dependencies { project(":backstopper-spring-web-flux"), project(":backstopper-custom-validators"), "ch.qos.logback:logback-classic", - "org.springframework.boot:spring-boot-dependencies:$springboot3Version", + "org.springframework.boot:spring-boot-dependencies:$springboot3_3Version", "org.springframework.boot:spring-boot-starter-webflux", "org.hibernate.validator:hibernate-validator", "org.glassfish.expressly:expressly:$glassfishExpresslyVersion", diff --git a/samples/sample-spring-boot3-webmvc/build.gradle b/samples/sample-spring-boot3-webmvc/build.gradle index dbeb23d..d79b286 100644 --- a/samples/sample-spring-boot3-webmvc/build.gradle +++ b/samples/sample-spring-boot3-webmvc/build.gradle @@ -5,7 +5,7 @@ buildscript { mavenCentral() } dependencies { - classpath("org.springframework.boot:spring-boot-gradle-plugin:${springboot3Version}") + classpath("org.springframework.boot:spring-boot-gradle-plugin:${springboot3_3Version}") } } @@ -21,7 +21,7 @@ dependencies { project(":backstopper-spring-boot3-webmvc"), project(":backstopper-custom-validators"), "ch.qos.logback:logback-classic", - "org.springframework.boot:spring-boot-dependencies:$springboot3Version", + "org.springframework.boot:spring-boot-dependencies:$springboot3_3Version", "org.springframework.boot:spring-boot-starter-web", "org.hibernate.validator:hibernate-validator", "org.glassfish.expressly:expressly:$glassfishExpresslyVersion", diff --git a/samples/sample-spring-web-mvc/build.gradle b/samples/sample-spring-web-mvc/build.gradle index 87173cb..beb1550 100644 --- a/samples/sample-spring-web-mvc/build.gradle +++ b/samples/sample-spring-web-mvc/build.gradle @@ -8,7 +8,7 @@ dependencies { implementation( project(":backstopper-spring-web-mvc"), project(":backstopper-custom-validators"), - "org.springframework:spring-webmvc:$spring6Version", + "org.springframework:spring-webmvc:$spring6_0Version", "ch.qos.logback:logback-classic:$logbackVersion", "org.hibernate.validator:hibernate-validator:$hibernateValidatorVersion", "org.glassfish.expressly:expressly:$glassfishExpresslyVersion", diff --git a/settings.gradle b/settings.gradle index 0282185..caf2734 100644 --- a/settings.gradle +++ b/settings.gradle @@ -13,7 +13,8 @@ include "nike-internal-util", "backstopper-spring-boot3-webmvc", // Test-only modules (not published) "testonly:testonly-spring-reusable-test-support", - "testonly:testonly-spring6-webmvc", + "testonly:testonly-spring-6_0-webmvc", + "testonly:testonly-spring-6_1-webmvc", // "testonly:testonly-springboot2-webmvc", // "testonly:testonly-springboot2-webflux", // Sample modules (not published) diff --git a/testonly/testonly-spring6-webmvc/README.md b/testonly/testonly-spring-6_0-webmvc/README.md similarity index 100% rename from testonly/testonly-spring6-webmvc/README.md rename to testonly/testonly-spring-6_0-webmvc/README.md diff --git a/testonly/testonly-spring6-webmvc/build.gradle b/testonly/testonly-spring-6_0-webmvc/build.gradle similarity index 94% rename from testonly/testonly-spring6-webmvc/build.gradle rename to testonly/testonly-spring-6_0-webmvc/build.gradle index 3464437..65b9a27 100644 --- a/testonly/testonly-spring6-webmvc/build.gradle +++ b/testonly/testonly-spring-6_0-webmvc/build.gradle @@ -8,7 +8,7 @@ dependencies { implementation( project(":backstopper-spring-web-mvc"), project(":backstopper-custom-validators"), - "org.springframework:spring-webmvc:$spring6Version", + "org.springframework:spring-webmvc:$spring6_0Version", "ch.qos.logback:logback-classic:$logbackVersion", "com.fasterxml.jackson.core:jackson-core:$jacksonVersion", "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion", diff --git a/testonly/testonly-spring6-webmvc/src/test/java/com/nike/backstopper/testonly/BackstopperSpring_6_0_WebMvcComponentTest.java b/testonly/testonly-spring-6_0-webmvc/src/test/java/com/nike/backstopper/testonly/BackstopperSpring_6_0_WebMvcComponentTest.java similarity index 100% rename from testonly/testonly-spring6-webmvc/src/test/java/com/nike/backstopper/testonly/BackstopperSpring_6_0_WebMvcComponentTest.java rename to testonly/testonly-spring-6_0-webmvc/src/test/java/com/nike/backstopper/testonly/BackstopperSpring_6_0_WebMvcComponentTest.java diff --git a/testonly/testonly-spring6-webmvc/src/test/java/serverconfig/classpathscan/Spring_6_0_WebMvcClasspathScanConfig.java b/testonly/testonly-spring-6_0-webmvc/src/test/java/serverconfig/classpathscan/Spring_6_0_WebMvcClasspathScanConfig.java similarity index 100% rename from testonly/testonly-spring6-webmvc/src/test/java/serverconfig/classpathscan/Spring_6_0_WebMvcClasspathScanConfig.java rename to testonly/testonly-spring-6_0-webmvc/src/test/java/serverconfig/classpathscan/Spring_6_0_WebMvcClasspathScanConfig.java diff --git a/testonly/testonly-spring6-webmvc/src/test/java/serverconfig/directimport/Spring_6_0_WebMvcDirectImportConfig.java b/testonly/testonly-spring-6_0-webmvc/src/test/java/serverconfig/directimport/Spring_6_0_WebMvcDirectImportConfig.java similarity index 100% rename from testonly/testonly-spring6-webmvc/src/test/java/serverconfig/directimport/Spring_6_0_WebMvcDirectImportConfig.java rename to testonly/testonly-spring-6_0-webmvc/src/test/java/serverconfig/directimport/Spring_6_0_WebMvcDirectImportConfig.java diff --git a/testonly/testonly-spring6-webmvc/src/test/resources/logback.xml b/testonly/testonly-spring-6_0-webmvc/src/test/resources/logback.xml similarity index 100% rename from testonly/testonly-spring6-webmvc/src/test/resources/logback.xml rename to testonly/testonly-spring-6_0-webmvc/src/test/resources/logback.xml diff --git a/testonly/testonly-spring-6_1-webmvc/README.md b/testonly/testonly-spring-6_1-webmvc/README.md new file mode 100644 index 0000000..38a9cd2 --- /dev/null +++ b/testonly/testonly-spring-6_1-webmvc/README.md @@ -0,0 +1,20 @@ +# Backstopper - testonly-spring6-webmvc + +Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 17 and greater. + +(NOTE: The [Backstopper 1.x branch](https://github.com/Nike-Inc/backstopper/tree/v1.x) contains a version of +Backstopper for Java 7+, and for the `javax` ecosystem. The current Backstopper supports Java 17+ and the `jakarta` +ecosystem. The Backstopper 1.x releases also contain support for Spring 4 and 5, and Springboot 1 and 2.) + +This submodule contains tests to verify that the [backstopper-spring-web-mvc](../../backstopper-spring-web-mvc) +module's functionality works as expected in Spring 6.1.x environments, for both classpath-scanning and direct-import +Backstopper configuration use cases. + +## More Info + +See the [base project README.md](../../README.md), [User Guide](../../USER_GUIDE.md), and Backstopper repository +source code and javadocs for all further information. + +## License + +Backstopper is released under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/testonly/testonly-spring-6_1-webmvc/build.gradle b/testonly/testonly-spring-6_1-webmvc/build.gradle new file mode 100644 index 0000000..4262b2c --- /dev/null +++ b/testonly/testonly-spring-6_1-webmvc/build.gradle @@ -0,0 +1,29 @@ +evaluationDependsOn(':') + +test { + useJUnitPlatform() +} + +dependencies { + implementation( + project(":backstopper-spring-web-mvc"), + project(":backstopper-custom-validators"), + "org.springframework:spring-webmvc:$spring6_1Version", + "ch.qos.logback:logback-classic:$logbackVersion", + "com.fasterxml.jackson.core:jackson-core:$jacksonVersion", + "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion", + "org.hibernate.validator:hibernate-validator:$hibernateValidatorVersion", + "org.glassfish.expressly:expressly:$glassfishExpresslyVersion", + "org.eclipse.jetty:jetty-webapp:$jettyVersion", + ) + testImplementation( + project(":backstopper-reusable-tests-junit5"), + project(":testonly:testonly-spring-reusable-test-support"), + "org.junit.jupiter:junit-jupiter-api:$junit5Version", + "org.junit.jupiter:junit-jupiter-engine:$junit5Version", + "org.junit.jupiter:junit-jupiter-params:$junit5Version", + "org.mockito:mockito-core:$mockitoVersion", + "org.assertj:assertj-core:$assertJVersion", + "io.rest-assured:rest-assured:$restAssuredVersion", + ) +} diff --git a/testonly/testonly-spring-6_1-webmvc/src/test/java/com/nike/backstopper/testonly/BackstopperSpring_6_1_WebMvcComponentTest.java b/testonly/testonly-spring-6_1-webmvc/src/test/java/com/nike/backstopper/testonly/BackstopperSpring_6_1_WebMvcComponentTest.java new file mode 100644 index 0000000..9b01ca2 --- /dev/null +++ b/testonly/testonly-spring-6_1-webmvc/src/test/java/com/nike/backstopper/testonly/BackstopperSpring_6_1_WebMvcComponentTest.java @@ -0,0 +1,572 @@ +package com.nike.backstopper.testonly; + +import com.nike.backstopper.apierror.ApiError; +import com.nike.backstopper.apierror.ApiErrorWithMetadata; +import com.nike.backstopper.apierror.sample.SampleCoreApiError; +import com.nike.internal.util.MapBuilder; +import com.nike.internal.util.Pair; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import io.restassured.http.ContentType; +import io.restassured.response.ExtractableResponse; +import serverconfig.classpathscan.Spring_6_1_WebMvcClasspathScanConfig; +import serverconfig.directimport.Spring_6_1_WebMvcDirectImportConfig; +import testonly.componenttest.spring.reusable.error.SampleProjectApiError; +import testonly.componenttest.spring.reusable.jettyserver.SpringMvcJettyComponentTestServer; +import testonly.componenttest.spring.reusable.model.RgbColor; +import testonly.componenttest.spring.reusable.model.SampleModel; + +import static com.nike.internal.util.testing.TestUtils.findFreePort; +import static io.restassured.RestAssured.given; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static testonly.componenttest.spring.reusable.controller.SampleController.CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH; +import static testonly.componenttest.spring.reusable.controller.SampleController.SAMPLE_PATH; +import static testonly.componenttest.spring.reusable.controller.SampleController.TRIGGER_UNHANDLED_ERROR_SUBPATH; +import static testonly.componenttest.spring.reusable.controller.SampleController.WITH_REQUIRED_HEADER_SUBPATH; +import static testonly.componenttest.spring.reusable.controller.SampleController.WITH_REQUIRED_QUERY_PARAM_SUBPATH; +import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.FOO_STRING_CANNOT_BE_BLANK; +import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.INVALID_RANGE_VALUE; +import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.NOT_RGB_COLOR_ENUM; +import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.RGB_COLOR_CANNOT_BE_NULL; +import static testonly.componenttest.spring.reusable.testutil.TestUtils.randomizedSampleModel; +import static testonly.componenttest.spring.reusable.testutil.TestUtils.verifyErrorReceived; + +/** + * Component test to verify that the functionality of {@code backstopper-spring-web-mvc} works as expected in a + * Spring 6.1.x environment, for both classpath-scanning and direct-import Backstopper configuration use cases. + * + * @author Nic Munroe + */ +@SuppressWarnings({"NewClassNamingConvention", "ClassEscapesDefinedScope"}) +public class BackstopperSpring_6_1_WebMvcComponentTest { + + private static final int CLASSPATH_SCAN_SERVER_PORT = findFreePort(); + private static final int DIRECT_IMPORT_SERVER_PORT = findFreePort(); + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private static final SpringMvcJettyComponentTestServer classpathScanServer = new SpringMvcJettyComponentTestServer( + CLASSPATH_SCAN_SERVER_PORT, Spring_6_1_WebMvcClasspathScanConfig.class + ); + + private static final SpringMvcJettyComponentTestServer directImportServer = new SpringMvcJettyComponentTestServer( + DIRECT_IMPORT_SERVER_PORT, Spring_6_1_WebMvcDirectImportConfig.class + ); + + @BeforeAll + public static void beforeClass() throws Exception { + assertThat(CLASSPATH_SCAN_SERVER_PORT).isNotEqualTo(DIRECT_IMPORT_SERVER_PORT); + classpathScanServer.startServer(); + directImportServer.startServer(); + } + + @AfterAll + public static void afterClass() throws Exception { + classpathScanServer.shutdownServer(); + directImportServer.shutdownServer(); + } + + private enum ServerScenario { + CLASSPATH_SCAN_SERVER(CLASSPATH_SCAN_SERVER_PORT), + DIRECT_IMPORT_SERVER(DIRECT_IMPORT_SERVER_PORT); + + public final int serverPort; + + ServerScenario(int serverPort) { + this.serverPort = serverPort; + } + } + + // *************** SUCCESSFUL (NON ERROR) CALLS ****************** + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_basic_sample_get(ServerScenario scenario) throws IOException { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + assertThat(response.statusCode()).isEqualTo(200); + SampleModel responseBody = objectMapper.readValue(response.asString(), SampleModel.class); + assertThat(responseBody).isNotNull(); + assertThat(responseBody.foo).isNotEmpty(); + assertThat(responseBody.range_0_to_42).isNotEmpty(); + assertThat(Integer.parseInt(responseBody.range_0_to_42)).isBetween(0, 42); + assertThat(responseBody.rgb_color).isNotEmpty(); + assertThat(RgbColor.toRgbColor(responseBody.rgb_color)).isNotNull(); + assertThat(responseBody.throw_manual_error).isFalse(); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_basic_sample_post(ServerScenario scenario) throws IOException { + SampleModel requestPayload = randomizedSampleModel(); + String requestPayloadAsString = objectMapper.writeValueAsString(requestPayload); + + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .contentType(ContentType.JSON) + .basePath(SAMPLE_PATH) + .body(requestPayloadAsString) + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + assertThat(response.statusCode()).isEqualTo(201); + SampleModel responseBody = objectMapper.readValue(response.asString(), SampleModel.class); + assertThat(responseBody).isNotNull(); + assertThat(responseBody.foo).isEqualTo(requestPayload.foo); + assertThat(responseBody.range_0_to_42).isEqualTo(requestPayload.range_0_to_42); + assertThat(responseBody.rgb_color).isEqualTo(requestPayload.rgb_color); + assertThat(responseBody.throw_manual_error).isEqualTo(requestPayload.throw_manual_error); + } + + // *************** JSR 303 AND ENDPOINT ERRORS ****************** + + @SuppressWarnings("unused") + private enum Jsr303SampleModelValidationScenario { + BLANK_FIELD_VIOLATION( + new SampleModel("", "42", "GREEN", false), + singletonList(FOO_STRING_CANNOT_BE_BLANK) + ), + INVALID_RANGE_VIOLATION( + new SampleModel("bar", "-1", "GREEN", false), + singletonList(INVALID_RANGE_VALUE) + ), + NULL_FIELD_VIOLATION( + new SampleModel("bar", "42", null, false), + singletonList(RGB_COLOR_CANNOT_BE_NULL) + ), + STRING_CONVERTS_TO_CLASSTYPE_VIOLATION( + new SampleModel("bar", "42", "car", false), + singletonList(NOT_RGB_COLOR_ENUM) + ), + MULTIPLE_VIOLATIONS( + new SampleModel(" \n\r\t ", "99", "tree", false), + Arrays.asList(FOO_STRING_CANNOT_BE_BLANK, INVALID_RANGE_VALUE, NOT_RGB_COLOR_ENUM) + ); + + public final SampleModel model; + public final List expectedErrors; + + Jsr303SampleModelValidationScenario( + SampleModel model, List expectedErrors + ) { + this.model = model; + this.expectedErrors = expectedErrors; + } + } + + public static List jsr303ValidationErrorScenariosDataProvider() { + List result = new ArrayList<>(); + for (Jsr303SampleModelValidationScenario violationScenario : Jsr303SampleModelValidationScenario.values()) { + for (ServerScenario serverScenario : ServerScenario.values()) { + result.add(new Object[]{violationScenario, serverScenario}); + } + } + return result; + } + + @MethodSource("jsr303ValidationErrorScenariosDataProvider") + @ParameterizedTest + public void verify_jsr303_validation_errors( + Jsr303SampleModelValidationScenario violationScenario, ServerScenario serverScenario + ) throws JsonProcessingException { + String requestPayloadAsString = objectMapper.writeValueAsString(violationScenario.model); + + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(serverScenario.serverPort) + .contentType(ContentType.JSON) + .basePath(SAMPLE_PATH) + .body(requestPayloadAsString) + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + List expectedErrors = new ArrayList<>(); + for (ApiError expectedApiError : violationScenario.expectedErrors) { + String extraMetadataFieldValue = null; + + if (INVALID_RANGE_VALUE.equals(expectedApiError)) { + extraMetadataFieldValue = "range_0_to_42"; + } + else if (RGB_COLOR_CANNOT_BE_NULL.equals(expectedApiError) || NOT_RGB_COLOR_ENUM.equals(expectedApiError)) { + extraMetadataFieldValue = "rgb_color"; + } + + if (extraMetadataFieldValue != null) { + expectedApiError = new ApiErrorWithMetadata( + expectedApiError, + MapBuilder.builder("field", (Object) extraMetadataFieldValue).build() + ); + } + + expectedErrors.add(expectedApiError); + } + verifyErrorReceived(response, expectedErrors, 400); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_MANUALLY_THROWN_ERROR_is_thrown_when_requested(ServerScenario scenario) throws IOException { + SampleModel requestPayload = new SampleModel("bar", "42", "RED", true); + String requestPayloadAsString = objectMapper.writeValueAsString(requestPayload); + + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .contentType(ContentType.JSON) + .basePath(SAMPLE_PATH) + .body(requestPayloadAsString) + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleProjectApiError.MANUALLY_THROWN_ERROR); + // This code path also should add some custom headers to the response + assertThat(response.headers().getValues("rgbColorValue")).isEqualTo(singletonList(requestPayload.rgb_color)); + assertThat(response.headers().getValues("otherExtraMultivalueHeader")).isEqualTo(Arrays.asList("foo", "bar")); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_SOME_MEANINGFUL_ERROR_NAME_is_thrown_when_correct_endpoint_is_hit(ServerScenario scenario) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleProjectApiError.SOME_MEANINGFUL_ERROR_NAME); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_GENERIC_SERVICE_ERROR_is_thrown_when_correct_endpoint_is_hit(ServerScenario scenario) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + TRIGGER_UNHANDLED_ERROR_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.GENERIC_SERVICE_ERROR); + } + + // *************** FRAMEWORK ERRORS ****************** + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_NOT_FOUND_returned_if_unknown_path_is_requested(ServerScenario scenario) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(UUID.randomUUID().toString()) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.NOT_FOUND); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_ERROR_THROWN_IN_SERVLET_FILTER_OUTSIDE_SPRING_returned_if_servlet_filter_trigger_occurs( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .header("throw-servlet-filter-exception", "true") + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleProjectApiError.ERROR_THROWN_IN_SERVLET_FILTER_OUTSIDE_SPRING); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_METHOD_NOT_ALLOWED_returned_if_known_path_is_requested_with_invalid_http_method( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .log().all() + .when() + .delete() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.METHOD_NOT_ALLOWED); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_sample_get_fails_with_NO_ACCEPTABLE_REPRESENTATION_if_passed_invalid_accept_header( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .accept(ContentType.BINARY) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.NO_ACCEPTABLE_REPRESENTATION); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_sample_post_fails_with_UNSUPPORTED_MEDIA_TYPE_if_passed_invalid_content_type( + ServerScenario scenario + ) throws IOException { + SampleModel requestPayload = randomizedSampleModel(); + String requestPayloadAsString = objectMapper.writeValueAsString(requestPayload); + + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .body(requestPayloadAsString) + .contentType(ContentType.TEXT) + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.UNSUPPORTED_MEDIA_TYPE); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_MALFORMED_REQUEST_is_thrown_when_required_query_param_is_missing( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + WITH_REQUIRED_QUERY_PARAM_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived( + response, + new ApiErrorWithMetadata( + SampleCoreApiError.MALFORMED_REQUEST, + Pair.of("missing_param_name", "requiredQueryParamValue"), + Pair.of("missing_param_type", "int"), + Pair.of("required_location", "query_param") + ) + ); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert_type_for_query_param( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + WITH_REQUIRED_QUERY_PARAM_SUBPATH) + .queryParam("requiredQueryParamValue", "not-an-integer") + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, new ApiErrorWithMetadata( + SampleCoreApiError.TYPE_CONVERSION_ERROR, + MapBuilder.builder("bad_property_name", (Object) "requiredQueryParamValue") + .put("bad_property_value","not-an-integer") + .put("required_location","query_param") + .put("required_type", "int") + .build() + )); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_MALFORMED_REQUEST_is_thrown_when_required_header_is_missing( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + WITH_REQUIRED_HEADER_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived( + response, + new ApiErrorWithMetadata( + SampleCoreApiError.MALFORMED_REQUEST, + Pair.of("missing_param_name", "requiredHeaderValue"), + Pair.of("missing_param_type", "int"), + Pair.of("required_location", "header") + ) + ); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert_type_for_header( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + WITH_REQUIRED_HEADER_SUBPATH) + .header("requiredHeaderValue", "not-an-integer") + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, new ApiErrorWithMetadata( + SampleCoreApiError.TYPE_CONVERSION_ERROR, + MapBuilder.builder("bad_property_name", (Object) "requiredHeaderValue") + .put("bad_property_value","not-an-integer") + .put("required_location","header") + .put("required_type", "int") + .build() + )); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_sample_post_fails_with_MISSING_EXPECTED_CONTENT_if_passed_empty_body( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .contentType(ContentType.JSON) + .body("") + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.MISSING_EXPECTED_CONTENT); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_sample_post_fails_with_MALFORMED_REQUEST_if_passed_bad_json_body( + ServerScenario scenario + ) throws IOException { + SampleModel originalValidPayloadObj = randomizedSampleModel(); + String originalValidPayloadAsString = objectMapper.writeValueAsString(originalValidPayloadObj); + @SuppressWarnings("unchecked") + Map badRequestPayloadAsMap = objectMapper.readValue(originalValidPayloadAsString, Map.class); + badRequestPayloadAsMap.put("throw_manual_error", "not-a-boolean"); + String badJsonPayloadAsString = objectMapper.writeValueAsString(badRequestPayloadAsMap); + + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .contentType(ContentType.JSON) + .body(badJsonPayloadAsString) + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.MALFORMED_REQUEST); + } +} diff --git a/testonly/testonly-spring-6_1-webmvc/src/test/java/serverconfig/classpathscan/Spring_6_1_WebMvcClasspathScanConfig.java b/testonly/testonly-spring-6_1-webmvc/src/test/java/serverconfig/classpathscan/Spring_6_1_WebMvcClasspathScanConfig.java new file mode 100644 index 0000000..3a4e209 --- /dev/null +++ b/testonly/testonly-spring-6_1-webmvc/src/test/java/serverconfig/classpathscan/Spring_6_1_WebMvcClasspathScanConfig.java @@ -0,0 +1,42 @@ +package serverconfig.classpathscan; + +import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import jakarta.validation.Validation; +import jakarta.validation.Validator; + +import testonly.componenttest.spring.reusable.error.SampleProjectApiErrorsImpl; + +/** + * Spring config that uses {@link ComponentScan} to integrate Backstopper via classpath scanning of the + * {@code com.nike.backstopper} package. + * + * @author Nic Munroe + */ +@Configuration +@ComponentScan(basePackages = { + // Component scan the core Backstopper+Spring support. + "com.nike.backstopper", + // Component scan the controller. + "testonly.componenttest.spring.reusable.controller" +}) +@EnableWebMvc +@SuppressWarnings("unused") +public class Spring_6_1_WebMvcClasspathScanConfig { + + @Bean + public ProjectApiErrors getProjectApiErrors() { + return new SampleProjectApiErrorsImpl(); + } + + @Bean + public Validator getJsr303Validator() { + //noinspection resource + return Validation.buildDefaultValidatorFactory().getValidator(); + } +} diff --git a/testonly/testonly-spring-6_1-webmvc/src/test/java/serverconfig/directimport/Spring_6_1_WebMvcDirectImportConfig.java b/testonly/testonly-spring-6_1-webmvc/src/test/java/serverconfig/directimport/Spring_6_1_WebMvcDirectImportConfig.java new file mode 100644 index 0000000..b11632d --- /dev/null +++ b/testonly/testonly-spring-6_1-webmvc/src/test/java/serverconfig/directimport/Spring_6_1_WebMvcDirectImportConfig.java @@ -0,0 +1,43 @@ +package serverconfig.directimport; + +import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; +import com.nike.backstopper.handler.spring.config.BackstopperSpringWebMvcConfig; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import testonly.componenttest.spring.reusable.controller.SampleController; +import testonly.componenttest.spring.reusable.error.SampleProjectApiErrorsImpl; + +/** + * Spring config that uses {@link Import} to integrate Backstopper via direct import of + * {@link BackstopperSpringWebMvcConfig}. + * + * @author Nic Munroe + */ +@Configuration +@Import({ + // Import core Backstopper+Spring support. + BackstopperSpringWebMvcConfig.class, + // Import the controller. + SampleController.class +}) +@EnableWebMvc +@SuppressWarnings("unused") +public class Spring_6_1_WebMvcDirectImportConfig { + + @Bean + public ProjectApiErrors getProjectApiErrors() { + return new SampleProjectApiErrorsImpl(); + } + + @Bean + public Validator getJsr303Validator() { + //noinspection resource + return Validation.buildDefaultValidatorFactory().getValidator(); + } +} diff --git a/testonly/testonly-spring-6_1-webmvc/src/test/resources/logback.xml b/testonly/testonly-spring-6_1-webmvc/src/test/resources/logback.xml new file mode 100644 index 0000000..80adb28 --- /dev/null +++ b/testonly/testonly-spring-6_1-webmvc/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd_HH:mm:ss.SSS} [%thread] |-%-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/testonly/testonly-spring-reusable-test-support/build.gradle b/testonly/testonly-spring-reusable-test-support/build.gradle index fffe038..a8fb3e8 100644 --- a/testonly/testonly-spring-reusable-test-support/build.gradle +++ b/testonly/testonly-spring-reusable-test-support/build.gradle @@ -8,7 +8,7 @@ dependencies { compileOnly( project(":backstopper-spring-web-mvc"), project(":backstopper-custom-validators"), - "org.springframework:spring-webmvc:$spring6Version", + "org.springframework:spring-webmvc:$spring6_0Version", "org.eclipse.jetty:jetty-webapp:$jettyVersion", "org.hibernate.validator:hibernate-validator:$hibernateValidatorVersion", "org.jetbrains:annotations:$jetbrainsAnnotationsVersion", @@ -20,6 +20,6 @@ dependencies { ) testImplementation( project(":backstopper-reusable-tests-junit5"), - "org.springframework:spring-webmvc:$spring6Version", + "org.springframework:spring-webmvc:$spring6_0Version", ) } From 6120b164270548082bfbbd811c81cec8cf1c5b36 Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Wed, 11 Sep 2024 17:59:17 -0700 Subject: [PATCH 25/42] Make spring-reusable-test-support mvc explicit and add testonly-springboot3_3-webmvc module --- build.gradle | 3 + settings.gradle | 5 +- .../testonly-spring-6_0-webmvc/build.gradle | 2 +- .../testonly-spring-6_1-webmvc/build.gradle | 2 +- .../build.gradle | 0 .../reusable/controller/SampleController.java | 0 .../reusable/error/SampleProjectApiError.java | 0 .../error/SampleProjectApiErrorsImpl.java | 0 .../SpringMvcJettyComponentTestServer.java | 0 .../spring/reusable/model/RgbColor.java | 0 .../spring/reusable/model/SampleModel.java | 0 .../testutil/ExplodingServletFilter.java | 0 .../spring/reusable/testutil/TestUtils.java | 0 .../ApplicationJsr303AnnotationTroller.java | 0 .../VerifyJsr303ContractTest.java | 0 ...rtsToClassTypeAnnotationsAreValidTest.java | 0 .../error/SampleProjectApiErrorsImplTest.java | 0 .../testonly-springboot2-webmvc/README.md | 17 ---- .../testonly-springboot2-webmvc/build.gradle | 53 ------------ .../testonly-springboot3_3-webmvc/README.md | 21 +++++ .../build.gradle | 43 ++++++++++ ...pperSpringboot3_3WebMvcComponentTest.java} | 85 ++++++++++++++++--- ...ringboot3_3WebMvcClasspathScanConfig.java} | 13 +-- ...pringboot3_3WebMvcDirectImportConfig.java} | 19 +++-- .../src/test/resources/logback.xml | 0 25 files changed, 161 insertions(+), 102 deletions(-) rename testonly/{testonly-spring-reusable-test-support => testonly-spring-webmvc-reusable-test-support}/build.gradle (100%) rename testonly/{testonly-spring-reusable-test-support => testonly-spring-webmvc-reusable-test-support}/src/main/java/testonly/componenttest/spring/reusable/controller/SampleController.java (100%) rename testonly/{testonly-spring-reusable-test-support => testonly-spring-webmvc-reusable-test-support}/src/main/java/testonly/componenttest/spring/reusable/error/SampleProjectApiError.java (100%) rename testonly/{testonly-spring-reusable-test-support => testonly-spring-webmvc-reusable-test-support}/src/main/java/testonly/componenttest/spring/reusable/error/SampleProjectApiErrorsImpl.java (100%) rename testonly/{testonly-spring-reusable-test-support => testonly-spring-webmvc-reusable-test-support}/src/main/java/testonly/componenttest/spring/reusable/jettyserver/SpringMvcJettyComponentTestServer.java (100%) rename testonly/{testonly-spring-reusable-test-support => testonly-spring-webmvc-reusable-test-support}/src/main/java/testonly/componenttest/spring/reusable/model/RgbColor.java (100%) rename testonly/{testonly-spring-reusable-test-support => testonly-spring-webmvc-reusable-test-support}/src/main/java/testonly/componenttest/spring/reusable/model/SampleModel.java (100%) rename testonly/{testonly-spring-reusable-test-support => testonly-spring-webmvc-reusable-test-support}/src/main/java/testonly/componenttest/spring/reusable/testutil/ExplodingServletFilter.java (100%) rename testonly/{testonly-spring-reusable-test-support => testonly-spring-webmvc-reusable-test-support}/src/main/java/testonly/componenttest/spring/reusable/testutil/TestUtils.java (100%) rename testonly/{testonly-spring-reusable-test-support => testonly-spring-webmvc-reusable-test-support}/src/test/java/jsr303convention/ApplicationJsr303AnnotationTroller.java (100%) rename testonly/{testonly-spring-reusable-test-support => testonly-spring-webmvc-reusable-test-support}/src/test/java/jsr303convention/VerifyJsr303ContractTest.java (100%) rename testonly/{testonly-spring-reusable-test-support => testonly-spring-webmvc-reusable-test-support}/src/test/java/jsr303convention/VerifyStringConvertsToClassTypeAnnotationsAreValidTest.java (100%) rename testonly/{testonly-spring-reusable-test-support => testonly-spring-webmvc-reusable-test-support}/src/test/java/testonly/componenttest/spring/reusable/error/SampleProjectApiErrorsImplTest.java (100%) delete mode 100644 testonly/testonly-springboot2-webmvc/README.md delete mode 100644 testonly/testonly-springboot2-webmvc/build.gradle create mode 100644 testonly/testonly-springboot3_3-webmvc/README.md create mode 100644 testonly/testonly-springboot3_3-webmvc/build.gradle rename testonly/{testonly-springboot2-webmvc/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot2WebMvcComponentTest.java => testonly-springboot3_3-webmvc/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot3_3WebMvcComponentTest.java} (86%) rename testonly/{testonly-springboot2-webmvc/src/test/java/serverconfig/classpathscan/Springboot2WebMvcClasspathScanConfig.java => testonly-springboot3_3-webmvc/src/test/java/serverconfig/classpathscan/Springboot3_3WebMvcClasspathScanConfig.java} (79%) rename testonly/{testonly-springboot2-webmvc/src/test/java/serverconfig/directimport/Springboot2WebMvcDirectImportConfig.java => testonly-springboot3_3-webmvc/src/test/java/serverconfig/directimport/Springboot3_3WebMvcDirectImportConfig.java} (73%) rename testonly/{testonly-springboot2-webmvc => testonly-springboot3_3-webmvc}/src/test/resources/logback.xml (100%) diff --git a/build.gradle b/build.gradle index 7e8c836..34315b6 100644 --- a/build.gradle +++ b/build.gradle @@ -89,6 +89,9 @@ ext { spring6_0Version = '6.0.23' // Compatible with Jakarta EE 9/10 spring6_1Version = '6.1.12' // Compatible with Jakarta EE 9/10 springSecurityVersion = '6.1.9' // Closest spring security version to our pinned spring6_0Version, but without going over to avoid versions being transitively bumped above what we want for testing. + springboot3_0Version = '3.0.13' + springboot3_1Version = '3.1.12' + springboot3_2Version = '3.2.9' springboot3_3Version = '3.3.3' jersey1Version = '1.19.2' jersey2Version = '2.23.2' diff --git a/settings.gradle b/settings.gradle index caf2734..84d4495 100644 --- a/settings.gradle +++ b/settings.gradle @@ -12,11 +12,10 @@ include "nike-internal-util", "backstopper-spring-web-flux", "backstopper-spring-boot3-webmvc", // Test-only modules (not published) - "testonly:testonly-spring-reusable-test-support", + "testonly:testonly-spring-webmvc-reusable-test-support", "testonly:testonly-spring-6_0-webmvc", "testonly:testonly-spring-6_1-webmvc", -// "testonly:testonly-springboot2-webmvc", -// "testonly:testonly-springboot2-webflux", + "testonly:testonly-springboot3_3-webmvc", // Sample modules (not published) "samples:sample-spring-web-mvc", "samples:sample-spring-boot3-webmvc", diff --git a/testonly/testonly-spring-6_0-webmvc/build.gradle b/testonly/testonly-spring-6_0-webmvc/build.gradle index 65b9a27..a5586df 100644 --- a/testonly/testonly-spring-6_0-webmvc/build.gradle +++ b/testonly/testonly-spring-6_0-webmvc/build.gradle @@ -18,7 +18,7 @@ dependencies { ) testImplementation( project(":backstopper-reusable-tests-junit5"), - project(":testonly:testonly-spring-reusable-test-support"), + project(":testonly:testonly-spring-webmvc-reusable-test-support"), "org.junit.jupiter:junit-jupiter-api:$junit5Version", "org.junit.jupiter:junit-jupiter-engine:$junit5Version", "org.junit.jupiter:junit-jupiter-params:$junit5Version", diff --git a/testonly/testonly-spring-6_1-webmvc/build.gradle b/testonly/testonly-spring-6_1-webmvc/build.gradle index 4262b2c..09f1fb0 100644 --- a/testonly/testonly-spring-6_1-webmvc/build.gradle +++ b/testonly/testonly-spring-6_1-webmvc/build.gradle @@ -18,7 +18,7 @@ dependencies { ) testImplementation( project(":backstopper-reusable-tests-junit5"), - project(":testonly:testonly-spring-reusable-test-support"), + project(":testonly:testonly-spring-webmvc-reusable-test-support"), "org.junit.jupiter:junit-jupiter-api:$junit5Version", "org.junit.jupiter:junit-jupiter-engine:$junit5Version", "org.junit.jupiter:junit-jupiter-params:$junit5Version", diff --git a/testonly/testonly-spring-reusable-test-support/build.gradle b/testonly/testonly-spring-webmvc-reusable-test-support/build.gradle similarity index 100% rename from testonly/testonly-spring-reusable-test-support/build.gradle rename to testonly/testonly-spring-webmvc-reusable-test-support/build.gradle diff --git a/testonly/testonly-spring-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/controller/SampleController.java b/testonly/testonly-spring-webmvc-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/controller/SampleController.java similarity index 100% rename from testonly/testonly-spring-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/controller/SampleController.java rename to testonly/testonly-spring-webmvc-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/controller/SampleController.java diff --git a/testonly/testonly-spring-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/error/SampleProjectApiError.java b/testonly/testonly-spring-webmvc-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/error/SampleProjectApiError.java similarity index 100% rename from testonly/testonly-spring-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/error/SampleProjectApiError.java rename to testonly/testonly-spring-webmvc-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/error/SampleProjectApiError.java diff --git a/testonly/testonly-spring-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/error/SampleProjectApiErrorsImpl.java b/testonly/testonly-spring-webmvc-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/error/SampleProjectApiErrorsImpl.java similarity index 100% rename from testonly/testonly-spring-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/error/SampleProjectApiErrorsImpl.java rename to testonly/testonly-spring-webmvc-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/error/SampleProjectApiErrorsImpl.java diff --git a/testonly/testonly-spring-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/jettyserver/SpringMvcJettyComponentTestServer.java b/testonly/testonly-spring-webmvc-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/jettyserver/SpringMvcJettyComponentTestServer.java similarity index 100% rename from testonly/testonly-spring-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/jettyserver/SpringMvcJettyComponentTestServer.java rename to testonly/testonly-spring-webmvc-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/jettyserver/SpringMvcJettyComponentTestServer.java diff --git a/testonly/testonly-spring-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/model/RgbColor.java b/testonly/testonly-spring-webmvc-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/model/RgbColor.java similarity index 100% rename from testonly/testonly-spring-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/model/RgbColor.java rename to testonly/testonly-spring-webmvc-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/model/RgbColor.java diff --git a/testonly/testonly-spring-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/model/SampleModel.java b/testonly/testonly-spring-webmvc-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/model/SampleModel.java similarity index 100% rename from testonly/testonly-spring-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/model/SampleModel.java rename to testonly/testonly-spring-webmvc-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/model/SampleModel.java diff --git a/testonly/testonly-spring-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/testutil/ExplodingServletFilter.java b/testonly/testonly-spring-webmvc-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/testutil/ExplodingServletFilter.java similarity index 100% rename from testonly/testonly-spring-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/testutil/ExplodingServletFilter.java rename to testonly/testonly-spring-webmvc-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/testutil/ExplodingServletFilter.java diff --git a/testonly/testonly-spring-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/testutil/TestUtils.java b/testonly/testonly-spring-webmvc-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/testutil/TestUtils.java similarity index 100% rename from testonly/testonly-spring-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/testutil/TestUtils.java rename to testonly/testonly-spring-webmvc-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/testutil/TestUtils.java diff --git a/testonly/testonly-spring-reusable-test-support/src/test/java/jsr303convention/ApplicationJsr303AnnotationTroller.java b/testonly/testonly-spring-webmvc-reusable-test-support/src/test/java/jsr303convention/ApplicationJsr303AnnotationTroller.java similarity index 100% rename from testonly/testonly-spring-reusable-test-support/src/test/java/jsr303convention/ApplicationJsr303AnnotationTroller.java rename to testonly/testonly-spring-webmvc-reusable-test-support/src/test/java/jsr303convention/ApplicationJsr303AnnotationTroller.java diff --git a/testonly/testonly-spring-reusable-test-support/src/test/java/jsr303convention/VerifyJsr303ContractTest.java b/testonly/testonly-spring-webmvc-reusable-test-support/src/test/java/jsr303convention/VerifyJsr303ContractTest.java similarity index 100% rename from testonly/testonly-spring-reusable-test-support/src/test/java/jsr303convention/VerifyJsr303ContractTest.java rename to testonly/testonly-spring-webmvc-reusable-test-support/src/test/java/jsr303convention/VerifyJsr303ContractTest.java diff --git a/testonly/testonly-spring-reusable-test-support/src/test/java/jsr303convention/VerifyStringConvertsToClassTypeAnnotationsAreValidTest.java b/testonly/testonly-spring-webmvc-reusable-test-support/src/test/java/jsr303convention/VerifyStringConvertsToClassTypeAnnotationsAreValidTest.java similarity index 100% rename from testonly/testonly-spring-reusable-test-support/src/test/java/jsr303convention/VerifyStringConvertsToClassTypeAnnotationsAreValidTest.java rename to testonly/testonly-spring-webmvc-reusable-test-support/src/test/java/jsr303convention/VerifyStringConvertsToClassTypeAnnotationsAreValidTest.java diff --git a/testonly/testonly-spring-reusable-test-support/src/test/java/testonly/componenttest/spring/reusable/error/SampleProjectApiErrorsImplTest.java b/testonly/testonly-spring-webmvc-reusable-test-support/src/test/java/testonly/componenttest/spring/reusable/error/SampleProjectApiErrorsImplTest.java similarity index 100% rename from testonly/testonly-spring-reusable-test-support/src/test/java/testonly/componenttest/spring/reusable/error/SampleProjectApiErrorsImplTest.java rename to testonly/testonly-spring-webmvc-reusable-test-support/src/test/java/testonly/componenttest/spring/reusable/error/SampleProjectApiErrorsImplTest.java diff --git a/testonly/testonly-springboot2-webmvc/README.md b/testonly/testonly-springboot2-webmvc/README.md deleted file mode 100644 index cf5ce03..0000000 --- a/testonly/testonly-springboot2-webmvc/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# Backstopper - testonly-springboot2-webmvc - -Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 7 and greater. - -This submodule contains tests to verify that the -[backstopper-spring-boot2-webmvc](../../backstopper-spring-boot2-webmvc) module's functionality works as expected in -Spring Boot 2 Web MVC (Servlet) environments, for both classpath-scanning and direct-import Backstopper configuration -use cases. - -## More Info - -See the [base project README.md](../../README.md), [User Guide](../../USER_GUIDE.md), and Backstopper repository -source code and javadocs for all further information. - -## License - -Backstopper is released under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/testonly/testonly-springboot2-webmvc/build.gradle b/testonly/testonly-springboot2-webmvc/build.gradle deleted file mode 100644 index 9e5fc40..0000000 --- a/testonly/testonly-springboot2-webmvc/build.gradle +++ /dev/null @@ -1,53 +0,0 @@ -evaluationDependsOn(':') - -buildscript { - repositories { - mavenCentral() - } - dependencies { - classpath("org.springframework.boot:spring-boot-gradle-plugin:${springboot2Version}") - } -} - -sourceCompatibility = JavaVersion.VERSION_1_8 -targetCompatibility = JavaVersion.VERSION_1_8 - -apply plugin: 'org.springframework.boot' -apply plugin: "io.spring.dependency-management" - -test { - useJUnitPlatform() -} - -dependencies { - implementation( - project(":backstopper-spring-boot2-webmvc"), - project(":backstopper-custom-validators"), - "ch.qos.logback:logback-classic:$logbackVersion", - "org.springframework.boot:spring-boot-dependencies:$springboot2Version", - "org.springframework.boot:spring-boot-starter-web", - "org.hibernate:hibernate-validator:$hibernateValidatorVersionForNewerSpring", - "javax.el:javax.el-api:$elApiVersion", // The el-api and el-impl are needed for the JSR 303 validation - "org.glassfish:javax.el:$elImplVersion", - ) - testImplementation( - project(":backstopper-reusable-tests-junit5"), - project(":testonly:testonly-spring-reusable-test-support"), - "org.junit.jupiter:junit-jupiter-api:$junit5Version", - "org.junit.jupiter:junit-jupiter-engine:$junit5Version", - "org.junit.jupiter:junit-jupiter-params:$junit5Version", - "org.mockito:mockito-core:$mockitoVersion", - "org.assertj:assertj-core:$assertJVersion", - "io.rest-assured:rest-assured:$restAssuredVersion", - // Thanks Springboot BOM! :/ - // https://stackoverflow.com/questions/44993615/java-lang-noclassdeffounderror-io-restassured-mapper-factory-gsonobjectmapperfa - "io.rest-assured:json-path:$restAssuredVersion", - "io.rest-assured:xml-path:$restAssuredVersion", - // The jaxb-api is needed for building on the java 11 JDK as these classes were moved out of the Java SE libs. - "javax.xml.bind:jaxb-api:$jaxbApiVersion", - ) -} - -// We're just running tests, not trying to stand up a real Springboot 2 server from gradle. -// Disable the bootJar task so gradle doesn't fall over. -bootJar.enabled = false diff --git a/testonly/testonly-springboot3_3-webmvc/README.md b/testonly/testonly-springboot3_3-webmvc/README.md new file mode 100644 index 0000000..f6301dc --- /dev/null +++ b/testonly/testonly-springboot3_3-webmvc/README.md @@ -0,0 +1,21 @@ +# Backstopper - testonly-springboot3_3-webmvc + +Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 17 and greater. + +(NOTE: The [Backstopper 1.x branch](https://github.com/Nike-Inc/backstopper/tree/v1.x) contains a version of +Backstopper for Java 7+, and for the `javax` ecosystem. The current Backstopper supports Java 17+ and the `jakarta` +ecosystem. The Backstopper 1.x releases also contain support for Spring 4 and 5, and Springboot 1 and 2.) + +This submodule contains tests to verify that the +[backstopper-spring-boot3-webmvc](../../backstopper-spring-boot3-webmvc) module's functionality works as expected in +Spring Boot 3.3 Web MVC (Servlet) environments, for both classpath-scanning and direct-import Backstopper configuration +use cases. + +## More Info + +See the [base project README.md](../../README.md), [User Guide](../../USER_GUIDE.md), and Backstopper repository +source code and javadocs for all further information. + +## License + +Backstopper is released under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/testonly/testonly-springboot3_3-webmvc/build.gradle b/testonly/testonly-springboot3_3-webmvc/build.gradle new file mode 100644 index 0000000..028934e --- /dev/null +++ b/testonly/testonly-springboot3_3-webmvc/build.gradle @@ -0,0 +1,43 @@ +evaluationDependsOn(':') + +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:${springboot3_3Version}") + } +} + +apply plugin: 'org.springframework.boot' +apply plugin: "io.spring.dependency-management" + +test { + useJUnitPlatform() +} + +dependencies { + implementation( + project(":backstopper-spring-boot3-webmvc"), + project(":backstopper-custom-validators"), + "ch.qos.logback:logback-classic", + "org.springframework.boot:spring-boot-dependencies:$springboot3_3Version", + "org.springframework.boot:spring-boot-starter-web", + "org.hibernate.validator:hibernate-validator", + "org.glassfish.expressly:expressly:$glassfishExpresslyVersion", + ) + testImplementation( + project(":backstopper-reusable-tests-junit5"), + project(":testonly:testonly-spring-webmvc-reusable-test-support"), + "org.junit.jupiter:junit-jupiter-api:$junit5Version", + "org.junit.jupiter:junit-jupiter-engine:$junit5Version", + "org.junit.jupiter:junit-jupiter-params:$junit5Version", + "org.mockito:mockito-core:$mockitoVersion", + "org.assertj:assertj-core:$assertJVersion", + "io.rest-assured:rest-assured", + ) +} + +// We're just running tests, not trying to stand up a real Springboot 3 server from gradle. +// Disable the bootJar task so gradle doesn't fall over. +bootJar.enabled = false diff --git a/testonly/testonly-springboot2-webmvc/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot2WebMvcComponentTest.java b/testonly/testonly-springboot3_3-webmvc/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot3_3WebMvcComponentTest.java similarity index 86% rename from testonly/testonly-springboot2-webmvc/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot2WebMvcComponentTest.java rename to testonly/testonly-springboot3_3-webmvc/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot3_3WebMvcComponentTest.java index e85f157..7fc34b3 100644 --- a/testonly/testonly-springboot2-webmvc/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot2WebMvcComponentTest.java +++ b/testonly/testonly-springboot3_3-webmvc/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot3_3WebMvcComponentTest.java @@ -26,8 +26,8 @@ import io.restassured.http.ContentType; import io.restassured.response.ExtractableResponse; -import serverconfig.classpathscan.Springboot2WebMvcClasspathScanConfig; -import serverconfig.directimport.Springboot2WebMvcDirectImportConfig; +import serverconfig.classpathscan.Springboot3_3WebMvcClasspathScanConfig; +import serverconfig.directimport.Springboot3_3WebMvcDirectImportConfig; import testonly.componenttest.spring.reusable.error.SampleProjectApiError; import testonly.componenttest.spring.reusable.model.RgbColor; import testonly.componenttest.spring.reusable.model.SampleModel; @@ -39,6 +39,7 @@ import static testonly.componenttest.spring.reusable.controller.SampleController.CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH; import static testonly.componenttest.spring.reusable.controller.SampleController.SAMPLE_PATH; import static testonly.componenttest.spring.reusable.controller.SampleController.TRIGGER_UNHANDLED_ERROR_SUBPATH; +import static testonly.componenttest.spring.reusable.controller.SampleController.WITH_REQUIRED_HEADER_SUBPATH; import static testonly.componenttest.spring.reusable.controller.SampleController.WITH_REQUIRED_QUERY_PARAM_SUBPATH; import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.FOO_STRING_CANNOT_BE_BLANK; import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.INVALID_RANGE_VALUE; @@ -48,12 +49,14 @@ import static testonly.componenttest.spring.reusable.testutil.TestUtils.verifyErrorReceived; /** - * Component test to verify that the functionality of {@code backstopper-spring-boot2-webmvc} works as expected in a - * Spring Boot 2 Web MVC environment, for both classpath-scanning and direct-import Backstopper configuration use cases. + * Component test to verify that the functionality of {@code backstopper-spring-boot3-webmvc} works as expected in a + * Spring Boot 3.3.x Web MVC environment, for both classpath-scanning and direct-import Backstopper configuration use + * cases. * * @author Nic Munroe */ -public class BackstopperSpringboot2WebMvcComponentTest { +@SuppressWarnings({"ClassEscapesDefinedScope", "NewClassNamingConvention"}) +public class BackstopperSpringboot3_3WebMvcComponentTest { private static final int CLASSPATH_SCAN_SERVER_PORT = findFreePort(); private static final int DIRECT_IMPORT_SERVER_PORT = findFreePort(); @@ -66,10 +69,10 @@ public class BackstopperSpringboot2WebMvcComponentTest { public static void beforeClass() { assertThat(CLASSPATH_SCAN_SERVER_PORT).isNotEqualTo(DIRECT_IMPORT_SERVER_PORT); classpathScanServerAppContext = SpringApplication.run( - Springboot2WebMvcClasspathScanConfig.class, "--server.port=" + CLASSPATH_SCAN_SERVER_PORT + Springboot3_3WebMvcClasspathScanConfig.class, "--server.port=" + CLASSPATH_SCAN_SERVER_PORT ); directImportServerAppContext = SpringApplication.run( - Springboot2WebMvcDirectImportConfig.class, "--server.port=" + DIRECT_IMPORT_SERVER_PORT + Springboot3_3WebMvcDirectImportConfig.class, "--server.port=" + DIRECT_IMPORT_SERVER_PORT ); } @@ -408,7 +411,7 @@ public void verify_sample_post_fails_with_UNSUPPORTED_MEDIA_TYPE_if_passed_inval @EnumSource(ServerScenario.class) @ParameterizedTest - public void verify_MALFORMED_REQUEST_is_thrown_when_required_data_is_missing( + public void verify_MALFORMED_REQUEST_is_thrown_when_required_query_param_is_missing( ServerScenario scenario ) { ExtractableResponse response = @@ -427,15 +430,16 @@ public void verify_MALFORMED_REQUEST_is_thrown_when_required_data_is_missing( response, new ApiErrorWithMetadata( SampleCoreApiError.MALFORMED_REQUEST, + Pair.of("missing_param_name", "requiredQueryParamValue"), Pair.of("missing_param_type", "int"), - Pair.of("missing_param_name", "requiredQueryParamValue") + Pair.of("required_location", "query_param") ) ); } @EnumSource(ServerScenario.class) @ParameterizedTest - public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert_type( + public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert_type_for_query_param( ServerScenario scenario ) { ExtractableResponse response = @@ -453,8 +457,65 @@ public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert verifyErrorReceived(response, new ApiErrorWithMetadata( SampleCoreApiError.TYPE_CONVERSION_ERROR, - MapBuilder.builder("bad_property_name", (Object)"requiredQueryParamValue") - .put("bad_property_value", "not-an-integer") + MapBuilder.builder("bad_property_name", (Object) "requiredQueryParamValue") + .put("bad_property_value","not-an-integer") + .put("required_location","query_param") + .put("required_type", "int") + .build() + )); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_MALFORMED_REQUEST_is_thrown_when_required_header_is_missing( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + WITH_REQUIRED_HEADER_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived( + response, + new ApiErrorWithMetadata( + SampleCoreApiError.MALFORMED_REQUEST, + Pair.of("missing_param_name", "requiredHeaderValue"), + Pair.of("missing_param_type", "int"), + Pair.of("required_location", "header") + ) + ); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert_type_for_header( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + WITH_REQUIRED_HEADER_SUBPATH) + .header("requiredHeaderValue", "not-an-integer") + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, new ApiErrorWithMetadata( + SampleCoreApiError.TYPE_CONVERSION_ERROR, + MapBuilder.builder("bad_property_name", (Object) "requiredHeaderValue") + .put("bad_property_value","not-an-integer") + .put("required_location","header") .put("required_type", "int") .build() )); diff --git a/testonly/testonly-springboot2-webmvc/src/test/java/serverconfig/classpathscan/Springboot2WebMvcClasspathScanConfig.java b/testonly/testonly-springboot3_3-webmvc/src/test/java/serverconfig/classpathscan/Springboot3_3WebMvcClasspathScanConfig.java similarity index 79% rename from testonly/testonly-springboot2-webmvc/src/test/java/serverconfig/classpathscan/Springboot2WebMvcClasspathScanConfig.java rename to testonly/testonly-springboot3_3-webmvc/src/test/java/serverconfig/classpathscan/Springboot3_3WebMvcClasspathScanConfig.java index 2c9de34..b3c8f82 100644 --- a/testonly/testonly-springboot2-webmvc/src/test/java/serverconfig/classpathscan/Springboot2WebMvcClasspathScanConfig.java +++ b/testonly/testonly-springboot3_3-webmvc/src/test/java/serverconfig/classpathscan/Springboot3_3WebMvcClasspathScanConfig.java @@ -8,9 +8,8 @@ import org.springframework.context.annotation.ComponentScan; import org.springframework.core.Ordered; -import javax.validation.Validation; -import javax.validation.Validator; - +import jakarta.validation.Validation; +import jakarta.validation.Validator; import testonly.componenttest.spring.reusable.error.SampleProjectApiErrorsImpl; import testonly.componenttest.spring.reusable.testutil.ExplodingServletFilter; @@ -27,7 +26,8 @@ // Component scan the controller. "testonly.componenttest.spring.reusable.controller" }) -public class Springboot2WebMvcClasspathScanConfig { +@SuppressWarnings("unused") +public class Springboot3_3WebMvcClasspathScanConfig { @Bean public ProjectApiErrors getProjectApiErrors() { @@ -36,12 +36,13 @@ public ProjectApiErrors getProjectApiErrors() { @Bean public Validator getJsr303Validator() { + //noinspection resource return Validation.buildDefaultValidatorFactory().getValidator(); } @Bean - public FilterRegistrationBean explodingServletFilter() { - FilterRegistrationBean frb = new FilterRegistrationBean<>(new ExplodingServletFilter()); + public FilterRegistrationBean explodingServletFilter() { + FilterRegistrationBean frb = new FilterRegistrationBean<>(new ExplodingServletFilter()); frb.setOrder(Ordered.HIGHEST_PRECEDENCE); return frb; } diff --git a/testonly/testonly-springboot2-webmvc/src/test/java/serverconfig/directimport/Springboot2WebMvcDirectImportConfig.java b/testonly/testonly-springboot3_3-webmvc/src/test/java/serverconfig/directimport/Springboot3_3WebMvcDirectImportConfig.java similarity index 73% rename from testonly/testonly-springboot2-webmvc/src/test/java/serverconfig/directimport/Springboot2WebMvcDirectImportConfig.java rename to testonly/testonly-springboot3_3-webmvc/src/test/java/serverconfig/directimport/Springboot3_3WebMvcDirectImportConfig.java index 0edf1db..2c41567 100644 --- a/testonly/testonly-springboot2-webmvc/src/test/java/serverconfig/directimport/Springboot2WebMvcDirectImportConfig.java +++ b/testonly/testonly-springboot3_3-webmvc/src/test/java/serverconfig/directimport/Springboot3_3WebMvcDirectImportConfig.java @@ -1,7 +1,7 @@ package serverconfig.directimport; import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.handler.springboot.config.BackstopperSpringboot2WebMvcConfig; +import com.nike.backstopper.handler.springboot.config.BackstopperSpringboot3WebMvcConfig; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.web.servlet.FilterRegistrationBean; @@ -9,27 +9,27 @@ import org.springframework.context.annotation.Import; import org.springframework.core.Ordered; -import javax.validation.Validation; -import javax.validation.Validator; - +import jakarta.validation.Validation; +import jakarta.validation.Validator; import testonly.componenttest.spring.reusable.controller.SampleController; import testonly.componenttest.spring.reusable.error.SampleProjectApiErrorsImpl; import testonly.componenttest.spring.reusable.testutil.ExplodingServletFilter; /** * Springboot config that uses {@link Import} to integrate Backstopper via direct import of - * {@link BackstopperSpringboot2WebMvcConfig}. + * {@link BackstopperSpringboot3WebMvcConfig}. * * @author Nic Munroe */ @SpringBootApplication @Import({ // Import core Backstopper+Springboot1 support. - BackstopperSpringboot2WebMvcConfig.class, + BackstopperSpringboot3WebMvcConfig.class, // Import the controller. SampleController.class }) -public class Springboot2WebMvcDirectImportConfig { +@SuppressWarnings("unused") +public class Springboot3_3WebMvcDirectImportConfig { @Bean public ProjectApiErrors getProjectApiErrors() { @@ -38,12 +38,13 @@ public ProjectApiErrors getProjectApiErrors() { @Bean public Validator getJsr303Validator() { + //noinspection resource return Validation.buildDefaultValidatorFactory().getValidator(); } @Bean - public FilterRegistrationBean explodingServletFilter() { - FilterRegistrationBean frb = new FilterRegistrationBean<>(new ExplodingServletFilter()); + public FilterRegistrationBean explodingServletFilter() { + FilterRegistrationBean frb = new FilterRegistrationBean<>(new ExplodingServletFilter()); frb.setOrder(Ordered.HIGHEST_PRECEDENCE); return frb; } diff --git a/testonly/testonly-springboot2-webmvc/src/test/resources/logback.xml b/testonly/testonly-springboot3_3-webmvc/src/test/resources/logback.xml similarity index 100% rename from testonly/testonly-springboot2-webmvc/src/test/resources/logback.xml rename to testonly/testonly-springboot3_3-webmvc/src/test/resources/logback.xml From 304b1bde9ce7aedd2766299c59259b84bb949515 Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Wed, 11 Sep 2024 18:03:03 -0700 Subject: [PATCH 26/42] Create spring-webflux-reusable-test-support module for webflux-based testing, and add a testonly-springboot3_3-webflux module --- settings.gradle | 2 + .../build.gradle | 22 ++++ .../controller/SampleWebFluxController.java | 22 ++-- .../reusable/error/SampleProjectApiError.java | 112 ++++++++++++++++++ .../error/SampleProjectApiErrorsImpl.java | 37 ++++++ .../ExplodingHandlerFilterFunction.java | 9 +- .../reusable}/filter/ExplodingWebFilter.java | 5 +- .../spring/reusable/model/RgbColor.java | 27 +++++ .../spring/reusable/model/SampleModel.java | 53 +++++++++ .../spring/reusable/testutil/TestUtils.java | 76 ++++++++++++ .../ApplicationJsr303AnnotationTroller.java | 38 ++++++ .../VerifyJsr303ContractTest.java | 26 ++++ ...rtsToClassTypeAnnotationsAreValidTest.java | 18 +++ .../error/SampleProjectApiErrorsImplTest.java | 20 ++++ .../testonly-springboot2-webflux/README.md | 17 --- .../testonly-springboot3_3-webflux/README.md | 21 ++++ .../build.gradle | 25 ++-- ...perSpringboot3_3WebFluxComponentTest.java} | 102 ++++++++++++---- ...ingboot3_3WebFluxClasspathScanConfig.java} | 21 ++-- ...ringboot3_3WebFluxDirectImportConfig.java} | 17 +-- .../src/test/resources/logback.xml | 0 21 files changed, 583 insertions(+), 87 deletions(-) create mode 100644 testonly/testonly-spring-webflux-reusable-test-support/build.gradle rename testonly/{testonly-springboot2-webflux/src/test/java/testonly/componenttest/spring/webflux => testonly-spring-webflux-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable}/controller/SampleWebFluxController.java (88%) create mode 100644 testonly/testonly-spring-webflux-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/error/SampleProjectApiError.java create mode 100644 testonly/testonly-spring-webflux-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/error/SampleProjectApiErrorsImpl.java rename testonly/{testonly-springboot2-webflux/src/test/java/testonly/componenttest/spring/webflux => testonly-spring-webflux-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable}/filter/ExplodingHandlerFilterFunction.java (86%) rename testonly/{testonly-springboot2-webflux/src/test/java/testonly/componenttest/spring/webflux => testonly-spring-webflux-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable}/filter/ExplodingWebFilter.java (86%) create mode 100644 testonly/testonly-spring-webflux-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/model/RgbColor.java create mode 100644 testonly/testonly-spring-webflux-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/model/SampleModel.java create mode 100644 testonly/testonly-spring-webflux-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/testutil/TestUtils.java create mode 100644 testonly/testonly-spring-webflux-reusable-test-support/src/test/java/jsr303convention/ApplicationJsr303AnnotationTroller.java create mode 100644 testonly/testonly-spring-webflux-reusable-test-support/src/test/java/jsr303convention/VerifyJsr303ContractTest.java create mode 100644 testonly/testonly-spring-webflux-reusable-test-support/src/test/java/jsr303convention/VerifyStringConvertsToClassTypeAnnotationsAreValidTest.java create mode 100644 testonly/testonly-spring-webflux-reusable-test-support/src/test/java/testonly/componenttest/spring/reusable/error/SampleProjectApiErrorsImplTest.java delete mode 100644 testonly/testonly-springboot2-webflux/README.md create mode 100644 testonly/testonly-springboot3_3-webflux/README.md rename testonly/{testonly-springboot2-webflux => testonly-springboot3_3-webflux}/build.gradle (52%) rename testonly/{testonly-springboot2-webflux/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot2WebFluxComponentTest.java => testonly-springboot3_3-webflux/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot3_3WebFluxComponentTest.java} (87%) rename testonly/{testonly-springboot2-webflux/src/test/java/serverconfig/classpathscan/Springboot2WebFluxClasspathScanConfig.java => testonly-springboot3_3-webflux/src/test/java/serverconfig/classpathscan/Springboot3_3WebFluxClasspathScanConfig.java} (71%) rename testonly/{testonly-springboot2-webflux/src/test/java/serverconfig/directimport/Springboot2WebFluxDirectImportConfig.java => testonly-springboot3_3-webflux/src/test/java/serverconfig/directimport/Springboot3_3WebFluxDirectImportConfig.java} (78%) rename testonly/{testonly-springboot2-webflux => testonly-springboot3_3-webflux}/src/test/resources/logback.xml (100%) diff --git a/settings.gradle b/settings.gradle index 84d4495..70b0f41 100644 --- a/settings.gradle +++ b/settings.gradle @@ -13,9 +13,11 @@ include "nike-internal-util", "backstopper-spring-boot3-webmvc", // Test-only modules (not published) "testonly:testonly-spring-webmvc-reusable-test-support", + "testonly:testonly-spring-webflux-reusable-test-support", "testonly:testonly-spring-6_0-webmvc", "testonly:testonly-spring-6_1-webmvc", "testonly:testonly-springboot3_3-webmvc", + "testonly:testonly-springboot3_3-webflux", // Sample modules (not published) "samples:sample-spring-web-mvc", "samples:sample-spring-boot3-webmvc", diff --git a/testonly/testonly-spring-webflux-reusable-test-support/build.gradle b/testonly/testonly-spring-webflux-reusable-test-support/build.gradle new file mode 100644 index 0000000..b6e7903 --- /dev/null +++ b/testonly/testonly-spring-webflux-reusable-test-support/build.gradle @@ -0,0 +1,22 @@ +evaluationDependsOn(':') + +test { + useJUnitPlatform() +} + +dependencies { + compileOnly( + project(":backstopper-spring-web-flux"), + project(":backstopper-custom-validators"), + "org.springframework:spring-context:$spring6_0Version", + "org.springframework:spring-webflux:$spring6_0Version", + "org.hibernate.validator:hibernate-validator:$hibernateValidatorVersion", + "org.jetbrains:annotations:$jetbrainsAnnotationsVersion", + "io.rest-assured:rest-assured:$restAssuredVersion", + "org.assertj:assertj-core:$assertJVersion", + ) + testImplementation( + project(":backstopper-reusable-tests-junit5"), + "org.springframework:spring-webflux:$spring6_0Version", + ) +} diff --git a/testonly/testonly-springboot2-webflux/src/test/java/testonly/componenttest/spring/webflux/controller/SampleWebFluxController.java b/testonly/testonly-spring-webflux-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/controller/SampleWebFluxController.java similarity index 88% rename from testonly/testonly-springboot2-webflux/src/test/java/testonly/componenttest/spring/webflux/controller/SampleWebFluxController.java rename to testonly/testonly-spring-webflux-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/controller/SampleWebFluxController.java index 3a10a1c..6871d84 100644 --- a/testonly/testonly-springboot2-webflux/src/test/java/testonly/componenttest/spring/webflux/controller/SampleWebFluxController.java +++ b/testonly/testonly-spring-webflux-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/controller/SampleWebFluxController.java @@ -1,4 +1,4 @@ -package testonly.componenttest.spring.webflux.controller; +package testonly.componenttest.spring.reusable.controller; import com.nike.backstopper.exception.ApiException; import com.nike.internal.util.Pair; @@ -8,6 +8,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; @@ -18,8 +19,7 @@ import java.util.Arrays; import java.util.UUID; -import javax.validation.Valid; - +import jakarta.validation.Valid; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import testonly.componenttest.spring.reusable.error.SampleProjectApiError; @@ -27,23 +27,23 @@ import testonly.componenttest.spring.reusable.model.SampleModel; import static java.util.Collections.singletonList; -import static testonly.componenttest.spring.webflux.controller.SampleWebFluxController.SAMPLE_PATH; /** * Contains some sample Spring WebFlux endpoints. * - *

NOTE: This is mostly a copy of the reusable {@link - * testonly.componenttest.spring.reusable.controller.SampleController}, except modified to use WebFlux return types + *

NOTE: This is mostly a copy of the reusable {@code SampleController} from + * testonly-spring-webmvc-reusable-test-support, except modified to use WebFlux return types * ({@link Mono} and {@link Flux}) and augmented with a few other WebFlux use cases and features. */ @Controller -@RequestMapping(SAMPLE_PATH) +@RequestMapping(SampleWebFluxController.SAMPLE_PATH) @SuppressWarnings({"unused", "WeakerAccess"}) public class SampleWebFluxController { public static final String SAMPLE_PATH = "/sample"; public static final String CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH = "/coreErrorWrapper"; public static final String WITH_REQUIRED_QUERY_PARAM_SUBPATH = "/withRequiredQueryParam"; + public static final String WITH_REQUIRED_HEADER_SUBPATH = "/withRequiredHeader"; public static final String TRIGGER_UNHANDLED_ERROR_SUBPATH = "/triggerUnhandledError"; public static final String SAMPLE_FROM_ROUTER_FUNCTION_PATH = "/sample/fromRouterFunction"; public static final String SAMPLE_FLUX_SUBPATH = "/flux"; @@ -107,13 +107,19 @@ public Mono withRequiredQueryParam(@RequestParam(name = "requiredQueryPa return Mono.just("You passed in " + someRequiredQueryParam + " for the required query param value"); } + @GetMapping(path = WITH_REQUIRED_HEADER_SUBPATH, produces = "text/plain") + @ResponseBody + public Mono withRequiredHeader(@RequestHeader(name = "requiredHeaderValue") int someRequiredHeader) { + return Mono.just("You passed in " + someRequiredHeader + " for the required header value"); + } + @GetMapping(path = TRIGGER_UNHANDLED_ERROR_SUBPATH) public void triggerUnhandledError() { throw new RuntimeException("This should be handled by SpringUnhandledExceptionHandler."); } public Mono getSampleModelRouterFunction(ServerRequest request) { - return ServerResponse.ok().syncBody( + return ServerResponse.ok().bodyValue( new SampleModel( UUID.randomUUID().toString(), String.valueOf(nextRangeInt(0, 42)), diff --git a/testonly/testonly-spring-webflux-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/error/SampleProjectApiError.java b/testonly/testonly-spring-webflux-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/error/SampleProjectApiError.java new file mode 100644 index 0000000..32e4937 --- /dev/null +++ b/testonly/testonly-spring-webflux-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/error/SampleProjectApiError.java @@ -0,0 +1,112 @@ +package testonly.componenttest.spring.reusable.error; + +import com.nike.backstopper.apierror.ApiError; +import com.nike.backstopper.apierror.ApiErrorBase; +import com.nike.backstopper.apierror.ApiErrorWithMetadata; +import com.nike.backstopper.apierror.sample.SampleCoreApiError; +import com.nike.internal.util.MapBuilder; + +import org.springframework.http.HttpStatus; + +import java.util.Arrays; +import java.util.Map; +import java.util.UUID; + +import testonly.componenttest.spring.reusable.model.RgbColor; + +/** + * Project-specific error definitions for the {@code testonly-spring*} component tests. + * + * @author Nic Munroe + */ +public enum SampleProjectApiError implements ApiError { + FIELD_CANNOT_BE_NULL_OR_BLANK(99100, "Field cannot be null or empty", HttpStatus.BAD_REQUEST.value()), + // FOO_STRING_CANNOT_BE_BLANK shows how you can build off a base/generic error and add metadata. + FOO_STRING_CANNOT_BE_BLANK(FIELD_CANNOT_BE_NULL_OR_BLANK, MapBuilder.builder("field", (Object)"foo").build()), + INVALID_RANGE_VALUE(99110, "The range_0_to_42 field must be between 0 and 42 (inclusive)", + HttpStatus.BAD_REQUEST.value()), + // RGB_COLOR_CANNOT_BE_NULL could build off FIELD_CANNOT_BE_NULL_OR_BLANK like FOO_STRING_CANNOT_BE_BLANK does, + // however this shows how you can make individual field errors with unique code and custom message. + RGB_COLOR_CANNOT_BE_NULL(99120, "The rgb_color field must be defined", HttpStatus.BAD_REQUEST.value()), + NOT_RGB_COLOR_ENUM(99130, "The rgb_color field value must be one of: " + Arrays.toString(RgbColor.values()), + HttpStatus.BAD_REQUEST.value()), + MANUALLY_THROWN_ERROR(99140, "You asked for an error to be thrown", HttpStatus.INTERNAL_SERVER_ERROR.value()), + // This is a wrapper around a core error. It will have the same error code, message, and HTTP status code, + // but will show up in the logs with contributing_errors="SOME_MEANINGFUL_ERROR_NAME", allowing you to + // distinguish the context of the error vs. the core GENERIC_SERVICE_ERROR at a glance. + SOME_MEANINGFUL_ERROR_NAME(SampleCoreApiError.GENERIC_SERVICE_ERROR), + + // Spring Web MVC (Servlet) only errors + ERROR_THROWN_IN_SERVLET_FILTER_OUTSIDE_SPRING( + 99150, "An error occurred in a Servlet Filter outside Spring", HttpStatus.INTERNAL_SERVER_ERROR.value() + ), + + // Spring WebFlux (Netty) only errors + ERROR_THROWN_IN_WEB_FILTER( + 99160, "An error was thrown in a WebFilter", HttpStatus.INTERNAL_SERVER_ERROR.value() + ), + ERROR_RETURNED_IN_WEB_FILTER_MONO( + 99161, "An error was returned in a WebFilter Mono", HttpStatus.INTERNAL_SERVER_ERROR.value() + ), + ERROR_THROWN_IN_HANDLER_FILTER_FUNCTION( + 99165, "An error was thrown in a HandlerFilterFunction", HttpStatus.INTERNAL_SERVER_ERROR.value() + ), + ERROR_RETURNED_IN_HANDLER_FILTER_FUNCTION_MONO( + 99166, "An error was returned in a HandlerFilterFunction Mono", HttpStatus.INTERNAL_SERVER_ERROR.value() + ), + WEBFLUX_MONO_ERROR( + 99170, "You hit the WebFlux Mono error endpoint", HttpStatus.INTERNAL_SERVER_ERROR.value() + ), + WEBFLUX_FLUX_ERROR( + 99180, "You hit the WebFlux Flux error endpoint", HttpStatus.INTERNAL_SERVER_ERROR.value() + ); + + private final ApiError delegate; + + SampleProjectApiError(ApiError delegate) { + this.delegate = delegate; + } + + SampleProjectApiError(ApiError delegate, Map metadata) { + this(new ApiErrorWithMetadata(delegate, metadata)); + } + + SampleProjectApiError(int errorCode, String message, int httpStatusCode) { + this(new ApiErrorBase( + "delegated-to-enum-wrapper-" + UUID.randomUUID(), errorCode, message, httpStatusCode + )); + } + + @SuppressWarnings("unused") + SampleProjectApiError(int errorCode, String message, int httpStatusCode, Map metadata) { + this(new ApiErrorBase( + "delegated-to-enum-wrapper-" + UUID.randomUUID(), errorCode, message, httpStatusCode, metadata + )); + } + + @Override + public String getName() { + return this.name(); + } + + @Override + public String getErrorCode() { + return delegate.getErrorCode(); + } + + @Override + public String getMessage() { + return delegate.getMessage(); + } + + @Override + public int getHttpStatusCode() { + return delegate.getHttpStatusCode(); + } + + @Override + public Map getMetadata() { + return delegate.getMetadata(); + } + +} \ No newline at end of file diff --git a/testonly/testonly-spring-webflux-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/error/SampleProjectApiErrorsImpl.java b/testonly/testonly-spring-webflux-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/error/SampleProjectApiErrorsImpl.java new file mode 100644 index 0000000..df2d1a3 --- /dev/null +++ b/testonly/testonly-spring-webflux-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/error/SampleProjectApiErrorsImpl.java @@ -0,0 +1,37 @@ +package testonly.componenttest.spring.reusable.error; + +import com.nike.backstopper.apierror.ApiError; +import com.nike.backstopper.apierror.projectspecificinfo.ProjectSpecificErrorCodeRange; +import com.nike.backstopper.apierror.projectspecificinfo.ProjectSpecificErrorCodeRangeIntegerImpl; +import com.nike.backstopper.apierror.sample.SampleProjectApiErrorsBase; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import jakarta.inject.Singleton; + +/** + * Returns the project specific errors for the {@code testonly-spring*} component tests. + */ +@Singleton +public class SampleProjectApiErrorsImpl extends SampleProjectApiErrorsBase { + + private static final List projectSpecificApiErrors = + new ArrayList<>(Arrays.asList(SampleProjectApiError.values())); + + private static final ProjectSpecificErrorCodeRange errorCodeRange = new ProjectSpecificErrorCodeRangeIntegerImpl( + 99100, 99200, "SAMPLE_PROJECT_API_ERRORS" + ); + + @Override + protected List getProjectSpecificApiErrors() { + return projectSpecificApiErrors; + } + + @Override + protected ProjectSpecificErrorCodeRange getProjectSpecificErrorCodeRange() { + return errorCodeRange; + } + +} diff --git a/testonly/testonly-springboot2-webflux/src/test/java/testonly/componenttest/spring/webflux/filter/ExplodingHandlerFilterFunction.java b/testonly/testonly-spring-webflux-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/filter/ExplodingHandlerFilterFunction.java similarity index 86% rename from testonly/testonly-springboot2-webflux/src/test/java/testonly/componenttest/spring/webflux/filter/ExplodingHandlerFilterFunction.java rename to testonly/testonly-spring-webflux-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/filter/ExplodingHandlerFilterFunction.java index 666f66a..a01dcc7 100644 --- a/testonly/testonly-springboot2-webflux/src/test/java/testonly/componenttest/spring/webflux/filter/ExplodingHandlerFilterFunction.java +++ b/testonly/testonly-spring-webflux-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/filter/ExplodingHandlerFilterFunction.java @@ -1,7 +1,8 @@ -package testonly.componenttest.spring.webflux.filter; +package testonly.componenttest.spring.reusable.filter; import com.nike.backstopper.exception.ApiException; +import org.jetbrains.annotations.NotNull; import org.springframework.http.HttpHeaders; import org.springframework.web.reactive.function.server.HandlerFilterFunction; import org.springframework.web.reactive.function.server.HandlerFunction; @@ -14,9 +15,9 @@ public class ExplodingHandlerFilterFunction implements HandlerFilterFunction { @Override - public Mono filter( - ServerRequest serverRequest, - HandlerFunction handlerFunction + public @NotNull Mono filter( + @NotNull ServerRequest serverRequest, + @NotNull HandlerFunction handlerFunction ) { HttpHeaders httpHeaders = serverRequest.headers().asHttpHeaders(); diff --git a/testonly/testonly-springboot2-webflux/src/test/java/testonly/componenttest/spring/webflux/filter/ExplodingWebFilter.java b/testonly/testonly-spring-webflux-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/filter/ExplodingWebFilter.java similarity index 86% rename from testonly/testonly-springboot2-webflux/src/test/java/testonly/componenttest/spring/webflux/filter/ExplodingWebFilter.java rename to testonly/testonly-spring-webflux-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/filter/ExplodingWebFilter.java index 876220c..ce4c51c 100644 --- a/testonly/testonly-springboot2-webflux/src/test/java/testonly/componenttest/spring/webflux/filter/ExplodingWebFilter.java +++ b/testonly/testonly-spring-webflux-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/filter/ExplodingWebFilter.java @@ -1,7 +1,8 @@ -package testonly.componenttest.spring.webflux.filter; +package testonly.componenttest.spring.reusable.filter; import com.nike.backstopper.exception.ApiException; +import org.jetbrains.annotations.NotNull; import org.springframework.http.HttpHeaders; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; @@ -13,7 +14,7 @@ public class ExplodingWebFilter implements WebFilter { @Override - public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + public @NotNull Mono filter(ServerWebExchange exchange, @NotNull WebFilterChain chain) { HttpHeaders httpHeaders = exchange.getRequest().getHeaders(); if ("true".equals(httpHeaders.getFirst("throw-web-filter-exception"))) { diff --git a/testonly/testonly-spring-webflux-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/model/RgbColor.java b/testonly/testonly-spring-webflux-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/model/RgbColor.java new file mode 100644 index 0000000..6d39f68 --- /dev/null +++ b/testonly/testonly-spring-webflux-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/model/RgbColor.java @@ -0,0 +1,27 @@ +package testonly.componenttest.spring.reusable.model; + +import com.fasterxml.jackson.annotation.JsonCreator; + +/** + * An enum used by {@link SampleModel} for showing how + * {@link com.nike.backstopper.validation.constraints.StringConvertsToClassType} can work with enums. Note + * the {@link #toRgbColor(String)} annotated with {@link JsonCreator}, which allows callers to pass in lower or + * mixed case versions of the enum values and still have them automatically deserialized to the correct enum. + * This special {@link JsonCreator} method is only necessary if you want to support case-insensitive enum validation + * when deserializing. + */ +public enum RgbColor { + RED, GREEN, BLUE; + + @JsonCreator + @SuppressWarnings("unused") + public static RgbColor toRgbColor(String colorString) { + for (RgbColor color : values()) { + if (color.name().equalsIgnoreCase(colorString)) + return color; + } + throw new IllegalArgumentException( + "Cannot convert the string: \"" + colorString + "\" to a valid RgbColor enum value." + ); + } +} diff --git a/testonly/testonly-spring-webflux-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/model/SampleModel.java b/testonly/testonly-spring-webflux-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/model/SampleModel.java new file mode 100644 index 0000000..3396a43 --- /dev/null +++ b/testonly/testonly-spring-webflux-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/model/SampleModel.java @@ -0,0 +1,53 @@ +package testonly.componenttest.spring.reusable.model; + +import com.nike.backstopper.apierror.ApiError; +import com.nike.backstopper.apierror.sample.SampleCoreApiError; +import com.nike.backstopper.validation.constraints.StringConvertsToClassType; + +import jakarta.validation.constraints.NotBlank; +import org.hibernate.validator.constraints.Range; + +import jakarta.validation.constraints.NotNull; + +import testonly.componenttest.spring.reusable.error.SampleProjectApiError; +import testonly.componenttest.spring.reusable.error.SampleProjectApiErrorsImpl; + +/** + * Simple model class showing the JSR 303 Bean Validation integration in Backstopper. Each message for a JSR 303 + * annotation must match an {@link ApiError#getName()} from one of the errors returned by this project's + * {@link SampleProjectApiErrorsImpl#getProjectApiErrors()}. In this case that means you can use any of the enum + * names from {@link SampleCoreApiError} or {@link SampleProjectApiError}. + * + *

If you have a typo or forget to add a message that matches an error name then the {@code VerifyJsr303ContractTest} + * unit test will catch your error and the project will fail to build - the test will give you info on exactly which + * classes, fields, and annotations don't conform to the necessary convention. + */ +@SuppressWarnings("WeakerAccess") +public class SampleModel { + @NotBlank(message = "FOO_STRING_CANNOT_BE_BLANK") + public final String foo; + + @Range(message = "INVALID_RANGE_VALUE", min = 0, max = 42) + public final String range_0_to_42; + + @NotNull(message = "RGB_COLOR_CANNOT_BE_NULL") + @StringConvertsToClassType( + message = "NOT_RGB_COLOR_ENUM", classType = RgbColor.class, allowCaseInsensitiveEnumMatch = true + ) + public final String rgb_color; + + public final Boolean throw_manual_error; + + @SuppressWarnings("unused") + // Intentionally protected - here for deserialization support. + protected SampleModel() { + this(null, null, null, null); + } + + public SampleModel(String foo, String range_0_to_42, String rgb_color, Boolean throw_manual_error) { + this.foo = foo; + this.range_0_to_42 = range_0_to_42; + this.rgb_color = rgb_color; + this.throw_manual_error = throw_manual_error; + } +} diff --git a/testonly/testonly-spring-webflux-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/testutil/TestUtils.java b/testonly/testonly-spring-webflux-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/testutil/TestUtils.java new file mode 100644 index 0000000..5604f72 --- /dev/null +++ b/testonly/testonly-spring-webflux-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/testutil/TestUtils.java @@ -0,0 +1,76 @@ +package testonly.componenttest.spring.reusable.testutil; + +import com.nike.backstopper.apierror.ApiError; +import com.nike.backstopper.model.DefaultErrorContractDTO; +import com.nike.backstopper.model.DefaultErrorDTO; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.IOException; +import java.util.Collection; +import java.util.UUID; + +import io.restassured.response.ExtractableResponse; +import testonly.componenttest.spring.reusable.controller.SampleWebFluxController; +import testonly.componenttest.spring.reusable.model.SampleModel; + +import static java.util.Collections.singleton; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Contains helpers to be used by {@code testonly-spring*} component tests. + * + * @author Nic Munroe + */ +public class TestUtils { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + public static void verifyErrorReceived(ExtractableResponse response, ApiError expectedError) { + verifyErrorReceived(response, singleton(expectedError), expectedError.getHttpStatusCode()); + } + + public static DefaultErrorDTO findErrorMatching(DefaultErrorContractDTO errorContract, ApiError desiredError) { + for (DefaultErrorDTO error : errorContract.errors) { + if (error.code.equals(desiredError.getErrorCode()) && error.message.equals(desiredError.getMessage())) { + return error; + } + } + + return null; + } + + public static void verifyErrorReceived( + ExtractableResponse response, + Collection expectedErrors, + int expectedHttpStatusCode + ) { + assertThat(response.statusCode()).isEqualTo(expectedHttpStatusCode); + try { + DefaultErrorContractDTO errorContract = objectMapper.readValue( + response.asString(), DefaultErrorContractDTO.class + ); + assertThat(errorContract.error_id).isNotEmpty(); + assertThat(UUID.fromString(errorContract.error_id)).isNotNull(); + assertThat(errorContract.errors).hasSameSizeAs(expectedErrors); + for (ApiError apiError : expectedErrors) { + DefaultErrorDTO matchingError = findErrorMatching(errorContract, apiError); + assertThat(matchingError).isNotNull(); + assertThat(matchingError.code).isEqualTo(apiError.getErrorCode()); + assertThat(matchingError.message).isEqualTo(apiError.getMessage()); + assertThat(matchingError.metadata).isEqualTo(apiError.getMetadata()); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static SampleModel randomizedSampleModel() { + return new SampleModel( + UUID.randomUUID().toString(), + String.valueOf(SampleWebFluxController.nextRangeInt(0, 42)), + SampleWebFluxController.nextRandomColor().name(), + false + ); + } +} diff --git a/testonly/testonly-spring-webflux-reusable-test-support/src/test/java/jsr303convention/ApplicationJsr303AnnotationTroller.java b/testonly/testonly-spring-webflux-reusable-test-support/src/test/java/jsr303convention/ApplicationJsr303AnnotationTroller.java new file mode 100644 index 0000000..ea70c52 --- /dev/null +++ b/testonly/testonly-spring-webflux-reusable-test-support/src/test/java/jsr303convention/ApplicationJsr303AnnotationTroller.java @@ -0,0 +1,38 @@ +package jsr303convention; + +import com.nike.backstopper.apierror.contract.jsr303convention.ReflectionBasedJsr303AnnotationTrollerBase; +import com.nike.internal.util.Pair; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.util.List; +import java.util.function.Predicate; + +/** + * Extension of {@link ReflectionBasedJsr303AnnotationTrollerBase} used by {@link VerifyJsr303ContractTest} and + * {@link VerifyStringConvertsToClassTypeAnnotationsAreValidTest}). + */ +public final class ApplicationJsr303AnnotationTroller extends ReflectionBasedJsr303AnnotationTrollerBase { + + private static final ApplicationJsr303AnnotationTroller INSTANCE = new ApplicationJsr303AnnotationTroller(); + + @SuppressWarnings("WeakerAccess") + public static ApplicationJsr303AnnotationTroller getInstance() { + return INSTANCE; + } + + // Intentionally private - use {@code getInstance()} to retrieve the singleton instance of this class. + private ApplicationJsr303AnnotationTroller() { + super(); + } + + @Override + protected List> ignoreAllAnnotationsAssociatedWithTheseProjectClasses() { + return null; + } + + @Override + protected List>> specificAnnotationDeclarationExclusionsForProject() { + return null; + } +} diff --git a/testonly/testonly-spring-webflux-reusable-test-support/src/test/java/jsr303convention/VerifyJsr303ContractTest.java b/testonly/testonly-spring-webflux-reusable-test-support/src/test/java/jsr303convention/VerifyJsr303ContractTest.java new file mode 100644 index 0000000..8d09e1a --- /dev/null +++ b/testonly/testonly-spring-webflux-reusable-test-support/src/test/java/jsr303convention/VerifyJsr303ContractTest.java @@ -0,0 +1,26 @@ +package jsr303convention; + +import com.nike.backstopper.apierror.contract.jsr303convention.ReflectionBasedJsr303AnnotationTrollerBase; +import com.nike.backstopper.apierror.contract.jsr303convention.VerifyJsr303ValidationMessagesPointToApiErrorsTest; +import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; + +import testonly.componenttest.spring.reusable.error.SampleProjectApiErrorsImpl; + +/** + * Verifies that *ALL* non-excluded JSR 303 validation annotations in this project have a message defined that maps to a + * {@link com.nike.backstopper.apierror.ApiError} enum name from this project's {@link SampleProjectApiErrorsImpl}. + */ +public class VerifyJsr303ContractTest extends VerifyJsr303ValidationMessagesPointToApiErrorsTest { + + private static final ProjectApiErrors PROJECT_API_ERRORS = new SampleProjectApiErrorsImpl(); + + @Override + protected ReflectionBasedJsr303AnnotationTrollerBase getAnnotationTroller() { + return ApplicationJsr303AnnotationTroller.getInstance(); + } + + @Override + protected ProjectApiErrors getProjectApiErrors() { + return PROJECT_API_ERRORS; + } +} diff --git a/testonly/testonly-spring-webflux-reusable-test-support/src/test/java/jsr303convention/VerifyStringConvertsToClassTypeAnnotationsAreValidTest.java b/testonly/testonly-spring-webflux-reusable-test-support/src/test/java/jsr303convention/VerifyStringConvertsToClassTypeAnnotationsAreValidTest.java new file mode 100644 index 0000000..822ee75 --- /dev/null +++ b/testonly/testonly-spring-webflux-reusable-test-support/src/test/java/jsr303convention/VerifyStringConvertsToClassTypeAnnotationsAreValidTest.java @@ -0,0 +1,18 @@ +package jsr303convention; + +import com.nike.backstopper.apierror.contract.jsr303convention.ReflectionBasedJsr303AnnotationTrollerBase; +import com.nike.backstopper.apierror.contract.jsr303convention.VerifyEnumsReferencedByStringConvertsToClassTypeJsr303AnnotationsAreJacksonCaseInsensitiveTest; +import com.nike.backstopper.validation.constraints.StringConvertsToClassType; + +/** + * Makes sure that any Enums referenced by {@link StringConvertsToClassType} JSR 303 annotations are case insensitive if + * they are marked with {@link StringConvertsToClassType#allowCaseInsensitiveEnumMatch()} set to true. + */ +public class VerifyStringConvertsToClassTypeAnnotationsAreValidTest + extends VerifyEnumsReferencedByStringConvertsToClassTypeJsr303AnnotationsAreJacksonCaseInsensitiveTest { + + @Override + protected ReflectionBasedJsr303AnnotationTrollerBase getAnnotationTroller() { + return ApplicationJsr303AnnotationTroller.getInstance(); + } +} diff --git a/testonly/testonly-spring-webflux-reusable-test-support/src/test/java/testonly/componenttest/spring/reusable/error/SampleProjectApiErrorsImplTest.java b/testonly/testonly-spring-webflux-reusable-test-support/src/test/java/testonly/componenttest/spring/reusable/error/SampleProjectApiErrorsImplTest.java new file mode 100644 index 0000000..554e889 --- /dev/null +++ b/testonly/testonly-spring-webflux-reusable-test-support/src/test/java/testonly/componenttest/spring/reusable/error/SampleProjectApiErrorsImplTest.java @@ -0,0 +1,20 @@ +package testonly.componenttest.spring.reusable.error; + +import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; +import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrorsTestBase; + +/** + * Extends {@link ProjectApiErrorsTestBase} in order to inherit tests that will verify the correctness of + * {@link SampleProjectApiErrorsImpl}. + * + * @author Nic Munroe + */ +public class SampleProjectApiErrorsImplTest extends ProjectApiErrorsTestBase { + + private final ProjectApiErrors projectApiErrors = new SampleProjectApiErrorsImpl(); + + @Override + protected ProjectApiErrors getProjectApiErrors() { + return projectApiErrors; + } +} \ No newline at end of file diff --git a/testonly/testonly-springboot2-webflux/README.md b/testonly/testonly-springboot2-webflux/README.md deleted file mode 100644 index 09166c3..0000000 --- a/testonly/testonly-springboot2-webflux/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# Backstopper - testonly-springboot2-webflux - -Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 7 and greater. - -This submodule contains tests to verify that the -[backstopper-spring-web-flux](../../backstopper-spring-web-flux) module's functionality works as expected in -Spring Boot 2 WebFlux (Netty) environments, for both classpath-scanning and direct-import Backstopper configuration -use cases. - -## More Info - -See the [base project README.md](../../README.md), [User Guide](../../USER_GUIDE.md), and Backstopper repository -source code and javadocs for all further information. - -## License - -Backstopper is released under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/testonly/testonly-springboot3_3-webflux/README.md b/testonly/testonly-springboot3_3-webflux/README.md new file mode 100644 index 0000000..e484e98 --- /dev/null +++ b/testonly/testonly-springboot3_3-webflux/README.md @@ -0,0 +1,21 @@ +# Backstopper - testonly-springboot3_3-webflux + +Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 17 and greater. + +(NOTE: The [Backstopper 1.x branch](https://github.com/Nike-Inc/backstopper/tree/v1.x) contains a version of +Backstopper for Java 7+, and for the `javax` ecosystem. The current Backstopper supports Java 17+ and the `jakarta` +ecosystem. The Backstopper 1.x releases also contain support for Spring 4 and 5, and Springboot 1 and 2.) + +This submodule contains tests to verify that the +[backstopper-spring-web-flux](../../backstopper-spring-web-flux) module's functionality works as expected in +Spring Boot 3.3 WebFlux (Netty) environments, for both classpath-scanning and direct-import Backstopper configuration +use cases. + +## More Info + +See the [base project README.md](../../README.md), [User Guide](../../USER_GUIDE.md), and Backstopper repository +source code and javadocs for all further information. + +## License + +Backstopper is released under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/testonly/testonly-springboot2-webflux/build.gradle b/testonly/testonly-springboot3_3-webflux/build.gradle similarity index 52% rename from testonly/testonly-springboot2-webflux/build.gradle rename to testonly/testonly-springboot3_3-webflux/build.gradle index ae11bd2..c89bb02 100644 --- a/testonly/testonly-springboot2-webflux/build.gradle +++ b/testonly/testonly-springboot3_3-webflux/build.gradle @@ -5,13 +5,10 @@ buildscript { mavenCentral() } dependencies { - classpath("org.springframework.boot:spring-boot-gradle-plugin:${springboot2Version}") + classpath("org.springframework.boot:spring-boot-gradle-plugin:${springboot3_3Version}") } } -sourceCompatibility = JavaVersion.VERSION_1_8 -targetCompatibility = JavaVersion.VERSION_1_8 - apply plugin: 'org.springframework.boot' apply plugin: "io.spring.dependency-management" @@ -23,28 +20,22 @@ dependencies { implementation( project(":backstopper-spring-web-flux"), project(":backstopper-custom-validators"), - "ch.qos.logback:logback-classic:$logbackVersion", - "org.springframework.boot:spring-boot-dependencies:$springboot2Version", + "ch.qos.logback:logback-classic", + "org.springframework.boot:spring-boot-dependencies:$springboot3_3Version", "org.springframework.boot:spring-boot-starter-webflux", - "org.hibernate:hibernate-validator:$hibernateValidatorVersionForNewerSpring", - "javax.el:javax.el-api:$elApiVersion", // The el-api and el-impl are needed for the JSR 303 validation - "org.glassfish:javax.el:$elImplVersion", + "org.hibernate.validator:hibernate-validator", + "org.glassfish.expressly:expressly:$glassfishExpresslyVersion", ) testImplementation( project(":backstopper-reusable-tests-junit5"), - project(":testonly:testonly-spring-reusable-test-support"), + project(":testonly:testonly-spring-webflux-reusable-test-support"), + "org.jetbrains:annotations:$jetbrainsAnnotationsVersion", "org.junit.jupiter:junit-jupiter-api:$junit5Version", "org.junit.jupiter:junit-jupiter-engine:$junit5Version", "org.junit.jupiter:junit-jupiter-params:$junit5Version", "org.mockito:mockito-core:$mockitoVersion", "org.assertj:assertj-core:$assertJVersion", - "io.rest-assured:rest-assured:$restAssuredVersion", - // Thanks Springboot BOM! :/ - // https://stackoverflow.com/questions/44993615/java-lang-noclassdeffounderror-io-restassured-mapper-factory-gsonobjectmapperfa - "io.rest-assured:json-path:$restAssuredVersion", - "io.rest-assured:xml-path:$restAssuredVersion", - // The jaxb-api is needed for building on the java 11 JDK as these classes were moved out of the Java SE libs. - "javax.xml.bind:jaxb-api:$jaxbApiVersion", + "io.rest-assured:rest-assured", ) } diff --git a/testonly/testonly-springboot2-webflux/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot2WebFluxComponentTest.java b/testonly/testonly-springboot3_3-webflux/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot3_3WebFluxComponentTest.java similarity index 87% rename from testonly/testonly-springboot2-webflux/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot2WebFluxComponentTest.java rename to testonly/testonly-springboot3_3-webflux/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot3_3WebFluxComponentTest.java index 200198f..055c0ee 100644 --- a/testonly/testonly-springboot2-webflux/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot2WebFluxComponentTest.java +++ b/testonly/testonly-springboot3_3-webflux/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot3_3WebFluxComponentTest.java @@ -27,8 +27,8 @@ import io.restassured.http.ContentType; import io.restassured.response.ExtractableResponse; -import serverconfig.classpathscan.Springboot2WebFluxClasspathScanConfig; -import serverconfig.directimport.Springboot2WebFluxDirectImportConfig; +import serverconfig.classpathscan.Springboot3_3WebFluxClasspathScanConfig; +import serverconfig.directimport.Springboot3_3WebFluxDirectImportConfig; import testonly.componenttest.spring.reusable.error.SampleProjectApiError; import testonly.componenttest.spring.reusable.model.RgbColor; import testonly.componenttest.spring.reusable.model.SampleModel; @@ -37,28 +37,31 @@ import static io.restassured.RestAssured.given; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; -import static testonly.componenttest.spring.reusable.controller.SampleController.CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH; -import static testonly.componenttest.spring.reusable.controller.SampleController.SAMPLE_PATH; -import static testonly.componenttest.spring.reusable.controller.SampleController.TRIGGER_UNHANDLED_ERROR_SUBPATH; -import static testonly.componenttest.spring.reusable.controller.SampleController.WITH_REQUIRED_QUERY_PARAM_SUBPATH; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.FLUX_ERROR_SUBPATH; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.MONO_ERROR_SUBPATH; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.SAMPLE_FLUX_SUBPATH; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.SAMPLE_FROM_ROUTER_FUNCTION_PATH; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.SAMPLE_PATH; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.TRIGGER_UNHANDLED_ERROR_SUBPATH; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.WITH_REQUIRED_HEADER_SUBPATH; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.WITH_REQUIRED_QUERY_PARAM_SUBPATH; import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.FOO_STRING_CANNOT_BE_BLANK; import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.INVALID_RANGE_VALUE; import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.NOT_RGB_COLOR_ENUM; import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.RGB_COLOR_CANNOT_BE_NULL; import static testonly.componenttest.spring.reusable.testutil.TestUtils.randomizedSampleModel; import static testonly.componenttest.spring.reusable.testutil.TestUtils.verifyErrorReceived; -import static testonly.componenttest.spring.webflux.controller.SampleWebFluxController.FLUX_ERROR_SUBPATH; -import static testonly.componenttest.spring.webflux.controller.SampleWebFluxController.MONO_ERROR_SUBPATH; -import static testonly.componenttest.spring.webflux.controller.SampleWebFluxController.SAMPLE_FLUX_SUBPATH; -import static testonly.componenttest.spring.webflux.controller.SampleWebFluxController.SAMPLE_FROM_ROUTER_FUNCTION_PATH; /** * Component test to verify that the functionality of {@code backstopper-spring-web-flux} works as expected in a - * Spring Boot 2 WebFlux environment, for both classpath-scanning and direct-import Backstopper configuration use cases. + * Spring Boot 3.3 WebFlux environment, for both classpath-scanning and direct-import Backstopper configuration use + * cases. * * @author Nic Munroe */ -public class BackstopperSpringboot2WebFluxComponentTest { +@SuppressWarnings({"NewClassNamingConvention", "ClassEscapesDefinedScope"}) +public class BackstopperSpringboot3_3WebFluxComponentTest { private static final int CLASSPATH_SCAN_SERVER_PORT = findFreePort(); private static final int DIRECT_IMPORT_SERVER_PORT = findFreePort(); @@ -71,10 +74,10 @@ public class BackstopperSpringboot2WebFluxComponentTest { public static void beforeClass() { assertThat(CLASSPATH_SCAN_SERVER_PORT).isNotEqualTo(DIRECT_IMPORT_SERVER_PORT); classpathScanServerAppContext = SpringApplication.run( - Springboot2WebFluxClasspathScanConfig.class, "--server.port=" + CLASSPATH_SCAN_SERVER_PORT + Springboot3_3WebFluxClasspathScanConfig.class, "--server.port=" + CLASSPATH_SCAN_SERVER_PORT ); directImportServerAppContext = SpringApplication.run( - Springboot2WebFluxDirectImportConfig.class, "--server.port=" + DIRECT_IMPORT_SERVER_PORT + Springboot3_3WebFluxDirectImportConfig.class, "--server.port=" + DIRECT_IMPORT_SERVER_PORT ); } @@ -193,7 +196,7 @@ public void verify_flux_sample_get(ServerScenario scenario) throws IOException { assertThat(response.statusCode()).isEqualTo(200); List responseBody = objectMapper.readValue( - response.asString(), new TypeReference>(){} + response.asString(), new TypeReference<>() {} ); assertThat(responseBody).hasSizeGreaterThan(1); @@ -574,7 +577,7 @@ public void verify_sample_post_fails_with_UNSUPPORTED_MEDIA_TYPE_if_passed_inval @EnumSource(ServerScenario.class) @ParameterizedTest - public void verify_MALFORMED_REQUEST_is_thrown_when_required_data_is_missing( + public void verify_MALFORMED_REQUEST_is_thrown_when_required_query_param_is_missing( ServerScenario scenario ) { ExtractableResponse response = @@ -593,15 +596,16 @@ public void verify_MALFORMED_REQUEST_is_thrown_when_required_data_is_missing( response, new ApiErrorWithMetadata( SampleCoreApiError.MALFORMED_REQUEST, + Pair.of("missing_param_name", "requiredQueryParamValue"), Pair.of("missing_param_type", "int"), - Pair.of("missing_param_name", "requiredQueryParamValue") + Pair.of("required_location", "query_param") ) ); } @EnumSource(ServerScenario.class) @ParameterizedTest - public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert_type( + public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert_type_for_query_param( ServerScenario scenario ) { ExtractableResponse response = @@ -619,9 +623,65 @@ public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert verifyErrorReceived(response, new ApiErrorWithMetadata( SampleCoreApiError.TYPE_CONVERSION_ERROR, - // We can't expect the bad_property_name=requiredQueryParamValue metadata like we do in Spring Web MVC, - // because Spring WebFlux doesn't add it to the TypeMismatchException cause. - MapBuilder.builder("bad_property_value", (Object) "not-an-integer") + MapBuilder.builder("bad_property_name", (Object) "requiredQueryParamValue") + .put("bad_property_value","not-an-integer") + .put("required_location","query_param") + .put("required_type", "int") + .build() + )); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_MALFORMED_REQUEST_is_thrown_when_required_header_is_missing( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + WITH_REQUIRED_HEADER_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived( + response, + new ApiErrorWithMetadata( + SampleCoreApiError.MALFORMED_REQUEST, + Pair.of("missing_param_name", "requiredHeaderValue"), + Pair.of("missing_param_type", "int"), + Pair.of("required_location", "header") + ) + ); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert_type_for_header( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + WITH_REQUIRED_HEADER_SUBPATH) + .header("requiredHeaderValue", "not-an-integer") + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, new ApiErrorWithMetadata( + SampleCoreApiError.TYPE_CONVERSION_ERROR, + MapBuilder.builder("bad_property_name", (Object) "requiredHeaderValue") + .put("bad_property_value","not-an-integer") + .put("required_location","header") .put("required_type", "int") .build() )); diff --git a/testonly/testonly-springboot2-webflux/src/test/java/serverconfig/classpathscan/Springboot2WebFluxClasspathScanConfig.java b/testonly/testonly-springboot3_3-webflux/src/test/java/serverconfig/classpathscan/Springboot3_3WebFluxClasspathScanConfig.java similarity index 71% rename from testonly/testonly-springboot2-webflux/src/test/java/serverconfig/classpathscan/Springboot2WebFluxClasspathScanConfig.java rename to testonly/testonly-springboot3_3-webflux/src/test/java/serverconfig/classpathscan/Springboot3_3WebFluxClasspathScanConfig.java index 08892de..b47d6a1 100644 --- a/testonly/testonly-springboot2-webflux/src/test/java/serverconfig/classpathscan/Springboot2WebFluxClasspathScanConfig.java +++ b/testonly/testonly-springboot3_3-webflux/src/test/java/serverconfig/classpathscan/Springboot3_3WebFluxClasspathScanConfig.java @@ -12,16 +12,15 @@ import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.server.WebFilter; -import javax.validation.Validation; -import javax.validation.Validator; - +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import testonly.componenttest.spring.reusable.controller.SampleWebFluxController; import testonly.componenttest.spring.reusable.error.SampleProjectApiErrorsImpl; -import testonly.componenttest.spring.webflux.controller.SampleWebFluxController; -import testonly.componenttest.spring.webflux.filter.ExplodingHandlerFilterFunction; -import testonly.componenttest.spring.webflux.filter.ExplodingWebFilter; +import testonly.componenttest.spring.reusable.filter.ExplodingHandlerFilterFunction; +import testonly.componenttest.spring.reusable.filter.ExplodingWebFilter; import static org.springframework.web.reactive.function.server.RequestPredicates.GET; -import static testonly.componenttest.spring.webflux.controller.SampleWebFluxController.SAMPLE_FROM_ROUTER_FUNCTION_PATH; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.SAMPLE_FROM_ROUTER_FUNCTION_PATH; /** * Springboot config that uses {@link ComponentScan} to integrate Backstopper via classpath scanning of the @@ -33,10 +32,11 @@ @ComponentScan(basePackages = { // Component scan the core Backstopper+Spring WebFlux support. "com.nike.backstopper", - // Component scan the controller (note this is the WebFlux controller, not the reusable servlet controller). - "testonly.componenttest.spring.webflux.controller" + // Component scan the controller (note this is the reusable WebFlux controller, not the reusable servlet controller). + "testonly.componenttest.spring.reusable.controller" }) -public class Springboot2WebFluxClasspathScanConfig { +@SuppressWarnings("unused") +public class Springboot3_3WebFluxClasspathScanConfig { @Bean public ProjectApiErrors getProjectApiErrors() { @@ -45,6 +45,7 @@ public ProjectApiErrors getProjectApiErrors() { @Bean public Validator getJsr303Validator() { + //noinspection resource return Validation.buildDefaultValidatorFactory().getValidator(); } diff --git a/testonly/testonly-springboot2-webflux/src/test/java/serverconfig/directimport/Springboot2WebFluxDirectImportConfig.java b/testonly/testonly-springboot3_3-webflux/src/test/java/serverconfig/directimport/Springboot3_3WebFluxDirectImportConfig.java similarity index 78% rename from testonly/testonly-springboot2-webflux/src/test/java/serverconfig/directimport/Springboot2WebFluxDirectImportConfig.java rename to testonly/testonly-springboot3_3-webflux/src/test/java/serverconfig/directimport/Springboot3_3WebFluxDirectImportConfig.java index 0203b66..4cdf5b6 100644 --- a/testonly/testonly-springboot2-webflux/src/test/java/serverconfig/directimport/Springboot2WebFluxDirectImportConfig.java +++ b/testonly/testonly-springboot3_3-webflux/src/test/java/serverconfig/directimport/Springboot3_3WebFluxDirectImportConfig.java @@ -13,16 +13,15 @@ import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.server.WebFilter; -import javax.validation.Validation; -import javax.validation.Validator; - +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import testonly.componenttest.spring.reusable.controller.SampleWebFluxController; import testonly.componenttest.spring.reusable.error.SampleProjectApiErrorsImpl; -import testonly.componenttest.spring.webflux.controller.SampleWebFluxController; -import testonly.componenttest.spring.webflux.filter.ExplodingHandlerFilterFunction; -import testonly.componenttest.spring.webflux.filter.ExplodingWebFilter; +import testonly.componenttest.spring.reusable.filter.ExplodingHandlerFilterFunction; +import testonly.componenttest.spring.reusable.filter.ExplodingWebFilter; import static org.springframework.web.reactive.function.server.RequestPredicates.GET; -import static testonly.componenttest.spring.webflux.controller.SampleWebFluxController.SAMPLE_FROM_ROUTER_FUNCTION_PATH; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.SAMPLE_FROM_ROUTER_FUNCTION_PATH; /** * Springboot config that uses {@link Import} to integrate Backstopper via direct import of @@ -37,7 +36,8 @@ // Import the controller. SampleWebFluxController.class }) -public class Springboot2WebFluxDirectImportConfig { +@SuppressWarnings("unused") +public class Springboot3_3WebFluxDirectImportConfig { @Bean public ProjectApiErrors getProjectApiErrors() { @@ -46,6 +46,7 @@ public ProjectApiErrors getProjectApiErrors() { @Bean public Validator getJsr303Validator() { + //noinspection resource return Validation.buildDefaultValidatorFactory().getValidator(); } diff --git a/testonly/testonly-springboot2-webflux/src/test/resources/logback.xml b/testonly/testonly-springboot3_3-webflux/src/test/resources/logback.xml similarity index 100% rename from testonly/testonly-springboot2-webflux/src/test/resources/logback.xml rename to testonly/testonly-springboot3_3-webflux/src/test/resources/logback.xml From e89e211904cae233943f281478082783e159b16a Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Wed, 11 Sep 2024 18:03:34 -0700 Subject: [PATCH 27/42] Minor dependency and docs cleanup in samples --- samples/sample-spring-boot3-webflux/build.gradle | 1 - samples/sample-spring-boot3-webmvc/README.md | 6 +++++- samples/sample-spring-boot3-webmvc/build.gradle | 1 - samples/sample-spring-web-mvc/build.gradle | 3 --- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/samples/sample-spring-boot3-webflux/build.gradle b/samples/sample-spring-boot3-webflux/build.gradle index 38a68ec..76a3d98 100644 --- a/samples/sample-spring-boot3-webflux/build.gradle +++ b/samples/sample-spring-boot3-webflux/build.gradle @@ -35,7 +35,6 @@ dependencies { "org.junit.jupiter:junit-jupiter-engine:$junit5Version", "org.junit.jupiter:junit-jupiter-params:$junit5Version", "org.mockito:mockito-core:$mockitoVersion", - "ch.qos.logback:logback-classic", "org.assertj:assertj-core:$assertJVersion", "io.rest-assured:rest-assured", ) diff --git a/samples/sample-spring-boot3-webmvc/README.md b/samples/sample-spring-boot3-webmvc/README.md index d1ff074..a6bec2e 100644 --- a/samples/sample-spring-boot3-webmvc/README.md +++ b/samples/sample-spring-boot3-webmvc/README.md @@ -1,6 +1,10 @@ # Backstopper Sample Application - spring-boot3-webmvc -Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 7 and greater. +Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 17 and greater. + +(NOTE: The [Backstopper 1.x branch](https://github.com/Nike-Inc/backstopper/tree/v1.x) contains a version of +Backstopper for Java 7+, and for the `javax` ecosystem. The current Backstopper supports Java 17+ and the `jakarta` +ecosystem. The Backstopper 1.x releases also contain support for Spring 4 and 5, and Springboot 1 and 2.) This submodule contains a sample application based on Spring Boot 3 + Web MVC (Servlet) that fully integrates Backstopper. diff --git a/samples/sample-spring-boot3-webmvc/build.gradle b/samples/sample-spring-boot3-webmvc/build.gradle index d79b286..ee2ea28 100644 --- a/samples/sample-spring-boot3-webmvc/build.gradle +++ b/samples/sample-spring-boot3-webmvc/build.gradle @@ -35,7 +35,6 @@ dependencies { "org.junit.jupiter:junit-jupiter-engine:$junit5Version", "org.junit.jupiter:junit-jupiter-params:$junit5Version", "org.mockito:mockito-core:$mockitoVersion", - "ch.qos.logback:logback-classic", "org.assertj:assertj-core:$assertJVersion", "io.rest-assured:rest-assured", ) diff --git a/samples/sample-spring-web-mvc/build.gradle b/samples/sample-spring-web-mvc/build.gradle index beb1550..e03b693 100644 --- a/samples/sample-spring-web-mvc/build.gradle +++ b/samples/sample-spring-web-mvc/build.gradle @@ -23,9 +23,6 @@ dependencies { "org.junit.jupiter:junit-jupiter-engine:$junit5Version", "org.junit.jupiter:junit-jupiter-params:$junit5Version", "org.mockito:mockito-core:$mockitoVersion", - "com.fasterxml.jackson.core:jackson-core:$jacksonVersion", - "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion", - "ch.qos.logback:logback-classic:$logbackVersion", "org.assertj:assertj-core:$assertJVersion", "io.rest-assured:rest-assured:$restAssuredVersion", ) From a324abb93199b0e0100e027c07eea399fe15248f Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Wed, 11 Sep 2024 18:14:35 -0700 Subject: [PATCH 28/42] Add testonly modules for springboot 3.0.x for webmvc and webflux --- settings.gradle | 2 + .../testonly-springboot3_0-webflux/README.md | 21 + .../build.gradle | 44 + ...pperSpringboot3_0WebFluxComponentTest.java | 762 ++++++++++++++++++ ...ringboot3_0WebFluxClasspathScanConfig.java | 64 ++ ...pringboot3_0WebFluxDirectImportConfig.java | 65 ++ .../src/test/resources/logback.xml | 11 + .../testonly-springboot3_0-webmvc/README.md | 21 + .../build.gradle | 43 + ...opperSpringboot3_0WebMvcComponentTest.java | 574 +++++++++++++ ...pringboot3_0WebMvcClasspathScanConfig.java | 49 ++ ...Springboot3_0WebMvcDirectImportConfig.java | 51 ++ .../src/test/resources/logback.xml | 11 + .../testonly-springboot3_3-webflux/README.md | 2 +- ...pperSpringboot3_3WebFluxComponentTest.java | 2 +- .../testonly-springboot3_3-webmvc/README.md | 2 +- 16 files changed, 1721 insertions(+), 3 deletions(-) create mode 100644 testonly/testonly-springboot3_0-webflux/README.md create mode 100644 testonly/testonly-springboot3_0-webflux/build.gradle create mode 100644 testonly/testonly-springboot3_0-webflux/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot3_0WebFluxComponentTest.java create mode 100644 testonly/testonly-springboot3_0-webflux/src/test/java/serverconfig/classpathscan/Springboot3_0WebFluxClasspathScanConfig.java create mode 100644 testonly/testonly-springboot3_0-webflux/src/test/java/serverconfig/directimport/Springboot3_0WebFluxDirectImportConfig.java create mode 100644 testonly/testonly-springboot3_0-webflux/src/test/resources/logback.xml create mode 100644 testonly/testonly-springboot3_0-webmvc/README.md create mode 100644 testonly/testonly-springboot3_0-webmvc/build.gradle create mode 100644 testonly/testonly-springboot3_0-webmvc/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot3_0WebMvcComponentTest.java create mode 100644 testonly/testonly-springboot3_0-webmvc/src/test/java/serverconfig/classpathscan/Springboot3_0WebMvcClasspathScanConfig.java create mode 100644 testonly/testonly-springboot3_0-webmvc/src/test/java/serverconfig/directimport/Springboot3_0WebMvcDirectImportConfig.java create mode 100644 testonly/testonly-springboot3_0-webmvc/src/test/resources/logback.xml diff --git a/settings.gradle b/settings.gradle index 70b0f41..e1b130d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -16,6 +16,8 @@ include "nike-internal-util", "testonly:testonly-spring-webflux-reusable-test-support", "testonly:testonly-spring-6_0-webmvc", "testonly:testonly-spring-6_1-webmvc", + "testonly:testonly-springboot3_0-webmvc", + "testonly:testonly-springboot3_0-webflux", "testonly:testonly-springboot3_3-webmvc", "testonly:testonly-springboot3_3-webflux", // Sample modules (not published) diff --git a/testonly/testonly-springboot3_0-webflux/README.md b/testonly/testonly-springboot3_0-webflux/README.md new file mode 100644 index 0000000..2937f94 --- /dev/null +++ b/testonly/testonly-springboot3_0-webflux/README.md @@ -0,0 +1,21 @@ +# Backstopper - testonly-springboot3_3-webflux + +Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 17 and greater. + +(NOTE: The [Backstopper 1.x branch](https://github.com/Nike-Inc/backstopper/tree/v1.x) contains a version of +Backstopper for Java 7+, and for the `javax` ecosystem. The current Backstopper supports Java 17+ and the `jakarta` +ecosystem. The Backstopper 1.x releases also contain support for Spring 4 and 5, and Springboot 1 and 2.) + +This submodule contains tests to verify that the +[backstopper-spring-web-flux](../../backstopper-spring-web-flux) module's functionality works as expected in +Spring Boot 3.0.x WebFlux (Netty) environments, for both classpath-scanning and direct-import Backstopper configuration +use cases. + +## More Info + +See the [base project README.md](../../README.md), [User Guide](../../USER_GUIDE.md), and Backstopper repository +source code and javadocs for all further information. + +## License + +Backstopper is released under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/testonly/testonly-springboot3_0-webflux/build.gradle b/testonly/testonly-springboot3_0-webflux/build.gradle new file mode 100644 index 0000000..773577f --- /dev/null +++ b/testonly/testonly-springboot3_0-webflux/build.gradle @@ -0,0 +1,44 @@ +evaluationDependsOn(':') + +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:${springboot3_0Version}") + } +} + +apply plugin: 'org.springframework.boot' +apply plugin: "io.spring.dependency-management" + +test { + useJUnitPlatform() +} + +dependencies { + implementation( + project(":backstopper-spring-web-flux"), + project(":backstopper-custom-validators"), + "ch.qos.logback:logback-classic", + "org.springframework.boot:spring-boot-dependencies:$springboot3_0Version", + "org.springframework.boot:spring-boot-starter-webflux", + "org.hibernate.validator:hibernate-validator", + "org.glassfish.expressly:expressly:$glassfishExpresslyVersion", + ) + testImplementation( + project(":backstopper-reusable-tests-junit5"), + project(":testonly:testonly-spring-webflux-reusable-test-support"), + "org.jetbrains:annotations:$jetbrainsAnnotationsVersion", + "org.junit.jupiter:junit-jupiter-api:$junit5Version", + "org.junit.jupiter:junit-jupiter-engine:$junit5Version", + "org.junit.jupiter:junit-jupiter-params:$junit5Version", + "org.mockito:mockito-core:$mockitoVersion", + "org.assertj:assertj-core:$assertJVersion", + "io.rest-assured:rest-assured", + ) +} + +// We're just running tests, not trying to stand up a real Springboot 2 server from gradle. +// Disable the bootJar task so gradle doesn't fall over. +bootJar.enabled = false diff --git a/testonly/testonly-springboot3_0-webflux/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot3_0WebFluxComponentTest.java b/testonly/testonly-springboot3_0-webflux/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot3_0WebFluxComponentTest.java new file mode 100644 index 0000000..490303b --- /dev/null +++ b/testonly/testonly-springboot3_0-webflux/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot3_0WebFluxComponentTest.java @@ -0,0 +1,762 @@ +package com.nike.backstopper.testonly; + +import com.nike.backstopper.apierror.ApiError; +import com.nike.backstopper.apierror.ApiErrorWithMetadata; +import com.nike.backstopper.apierror.sample.SampleCoreApiError; +import com.nike.internal.util.MapBuilder; +import com.nike.internal.util.Pair; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ConfigurableApplicationContext; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import io.restassured.http.ContentType; +import io.restassured.response.ExtractableResponse; +import serverconfig.classpathscan.Springboot3_0WebFluxClasspathScanConfig; +import serverconfig.directimport.Springboot3_0WebFluxDirectImportConfig; +import testonly.componenttest.spring.reusable.error.SampleProjectApiError; +import testonly.componenttest.spring.reusable.model.RgbColor; +import testonly.componenttest.spring.reusable.model.SampleModel; + +import static com.nike.internal.util.testing.TestUtils.findFreePort; +import static io.restassured.RestAssured.given; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.FLUX_ERROR_SUBPATH; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.MONO_ERROR_SUBPATH; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.SAMPLE_FLUX_SUBPATH; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.SAMPLE_FROM_ROUTER_FUNCTION_PATH; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.SAMPLE_PATH; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.TRIGGER_UNHANDLED_ERROR_SUBPATH; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.WITH_REQUIRED_HEADER_SUBPATH; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.WITH_REQUIRED_QUERY_PARAM_SUBPATH; +import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.FOO_STRING_CANNOT_BE_BLANK; +import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.INVALID_RANGE_VALUE; +import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.NOT_RGB_COLOR_ENUM; +import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.RGB_COLOR_CANNOT_BE_NULL; +import static testonly.componenttest.spring.reusable.testutil.TestUtils.randomizedSampleModel; +import static testonly.componenttest.spring.reusable.testutil.TestUtils.verifyErrorReceived; + +/** + * Component test to verify that the functionality of {@code backstopper-spring-web-flux} works as expected in a + * Spring Boot 3.0.x WebFlux environment, for both classpath-scanning and direct-import Backstopper configuration use + * cases. + * + * @author Nic Munroe + */ +@SuppressWarnings({"NewClassNamingConvention", "ClassEscapesDefinedScope"}) +public class BackstopperSpringboot3_0WebFluxComponentTest { + + private static final int CLASSPATH_SCAN_SERVER_PORT = findFreePort(); + private static final int DIRECT_IMPORT_SERVER_PORT = findFreePort(); + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private static ConfigurableApplicationContext classpathScanServerAppContext; + private static ConfigurableApplicationContext directImportServerAppContext; + + @BeforeAll + public static void beforeClass() { + assertThat(CLASSPATH_SCAN_SERVER_PORT).isNotEqualTo(DIRECT_IMPORT_SERVER_PORT); + classpathScanServerAppContext = SpringApplication.run( + Springboot3_0WebFluxClasspathScanConfig.class, "--server.port=" + CLASSPATH_SCAN_SERVER_PORT + ); + directImportServerAppContext = SpringApplication.run( + Springboot3_0WebFluxDirectImportConfig.class, "--server.port=" + DIRECT_IMPORT_SERVER_PORT + ); + } + + @AfterAll + public static void afterClass() { + SpringApplication.exit(classpathScanServerAppContext); + SpringApplication.exit(directImportServerAppContext); + } + + @SuppressWarnings("unused") + private enum ServerScenario { + CLASSPATH_SCAN_SERVER(CLASSPATH_SCAN_SERVER_PORT), + DIRECT_IMPORT_SERVER(DIRECT_IMPORT_SERVER_PORT); + + public final int serverPort; + + ServerScenario(int serverPort) { + this.serverPort = serverPort; + } + } + + // *************** SUCCESSFUL (NON ERROR) CALLS ****************** + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_basic_sample_get(ServerScenario scenario) throws IOException { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + assertThat(response.statusCode()).isEqualTo(200); + SampleModel responseBody = objectMapper.readValue(response.asString(), SampleModel.class); + verifyNewSampleModel(responseBody); + } + + private void verifyNewSampleModel(SampleModel sampleModel) { + assertThat(sampleModel).isNotNull(); + assertThat(sampleModel.foo).isNotEmpty(); + assertThat(sampleModel.range_0_to_42).isNotEmpty(); + assertThat(Integer.parseInt(sampleModel.range_0_to_42)).isBetween(0, 42); + assertThat(sampleModel.rgb_color).isNotEmpty(); + assertThat(RgbColor.toRgbColor(sampleModel.rgb_color)).isNotNull(); + assertThat(sampleModel.throw_manual_error).isFalse(); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_basic_sample_post(ServerScenario scenario) throws IOException { + SampleModel requestPayload = randomizedSampleModel(); + String requestPayloadAsString = objectMapper.writeValueAsString(requestPayload); + + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .contentType(ContentType.JSON) + .basePath(SAMPLE_PATH) + .body(requestPayloadAsString) + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + assertThat(response.statusCode()).isEqualTo(201); + SampleModel responseBody = objectMapper.readValue(response.asString(), SampleModel.class); + assertThat(responseBody).isNotNull(); + assertThat(responseBody.foo).isEqualTo(requestPayload.foo); + assertThat(responseBody.range_0_to_42).isEqualTo(requestPayload.range_0_to_42); + assertThat(responseBody.rgb_color).isEqualTo(requestPayload.rgb_color); + assertThat(responseBody.throw_manual_error).isEqualTo(requestPayload.throw_manual_error); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_router_function_sample_get(ServerScenario scenario) throws IOException { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_FROM_ROUTER_FUNCTION_PATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + assertThat(response.statusCode()).isEqualTo(200); + SampleModel responseBody = objectMapper.readValue(response.asString(), SampleModel.class); + verifyNewSampleModel(responseBody); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_flux_sample_get(ServerScenario scenario) throws IOException { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + SAMPLE_FLUX_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + assertThat(response.statusCode()).isEqualTo(200); + List responseBody = objectMapper.readValue( + response.asString(), new TypeReference<>() {} + ); + + assertThat(responseBody).hasSizeGreaterThan(1); + responseBody.forEach(this::verifyNewSampleModel); + } + + // *************** JSR 303 AND ENDPOINT ERRORS ****************** + + @SuppressWarnings("unused") + private enum Jsr303SampleModelValidationScenario { + BLANK_FIELD_VIOLATION( + new SampleModel("", "42", "GREEN", false), + singletonList(FOO_STRING_CANNOT_BE_BLANK) + ), + INVALID_RANGE_VIOLATION( + new SampleModel("bar", "-1", "GREEN", false), + singletonList(INVALID_RANGE_VALUE) + ), + NULL_FIELD_VIOLATION( + new SampleModel("bar", "42", null, false), + singletonList(RGB_COLOR_CANNOT_BE_NULL) + ), + STRING_CONVERTS_TO_CLASSTYPE_VIOLATION( + new SampleModel("bar", "42", "car", false), + singletonList(NOT_RGB_COLOR_ENUM) + ), + MULTIPLE_VIOLATIONS( + new SampleModel(" \n\r\t ", "99", "tree", false), + Arrays.asList(FOO_STRING_CANNOT_BE_BLANK, INVALID_RANGE_VALUE, NOT_RGB_COLOR_ENUM) + ); + + public final SampleModel model; + public final List expectedErrors; + + Jsr303SampleModelValidationScenario( + SampleModel model, List expectedErrors + ) { + this.model = model; + this.expectedErrors = expectedErrors; + } + } + + public static List jsr303ValidationErrorScenariosDataProvider() { + List result = new ArrayList<>(); + for (Jsr303SampleModelValidationScenario violationScenario : Jsr303SampleModelValidationScenario.values()) { + for (ServerScenario serverScenario : ServerScenario.values()) { + result.add(new Object[]{violationScenario, serverScenario}); + } + } + return result; + } + + @MethodSource("jsr303ValidationErrorScenariosDataProvider") + @ParameterizedTest + public void verify_jsr303_validation_errors( + Jsr303SampleModelValidationScenario violationScenario, ServerScenario serverScenario + ) throws JsonProcessingException { + String requestPayloadAsString = objectMapper.writeValueAsString(violationScenario.model); + + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(serverScenario.serverPort) + .contentType(ContentType.JSON) + .basePath(SAMPLE_PATH) + .body(requestPayloadAsString) + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + List expectedErrors = new ArrayList<>(); + for (ApiError expectedApiError : violationScenario.expectedErrors) { + String extraMetadataFieldValue = null; + + if (INVALID_RANGE_VALUE.equals(expectedApiError)) { + extraMetadataFieldValue = "range_0_to_42"; + } + else if (RGB_COLOR_CANNOT_BE_NULL.equals(expectedApiError) || NOT_RGB_COLOR_ENUM.equals(expectedApiError)) { + extraMetadataFieldValue = "rgb_color"; + } + + if (extraMetadataFieldValue != null) { + expectedApiError = new ApiErrorWithMetadata( + expectedApiError, + MapBuilder.builder("field", (Object) extraMetadataFieldValue).build() + ); + } + + expectedErrors.add(expectedApiError); + } + verifyErrorReceived(response, expectedErrors, 400); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_MANUALLY_THROWN_ERROR_is_thrown_when_requested(ServerScenario scenario) throws IOException { + SampleModel requestPayload = new SampleModel("bar", "42", "RED", true); + String requestPayloadAsString = objectMapper.writeValueAsString(requestPayload); + + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .contentType(ContentType.JSON) + .basePath(SAMPLE_PATH) + .body(requestPayloadAsString) + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleProjectApiError.MANUALLY_THROWN_ERROR); + // This code path also should add some custom headers to the response + assertThat(response.headers().getValues("rgbColorValue")).isEqualTo(singletonList(requestPayload.rgb_color)); + assertThat(response.headers().getValues("otherExtraMultivalueHeader")).isEqualTo(Arrays.asList("foo", "bar")); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_SOME_MEANINGFUL_ERROR_NAME_is_thrown_when_correct_endpoint_is_hit(ServerScenario scenario) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleProjectApiError.SOME_MEANINGFUL_ERROR_NAME); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_GENERIC_SERVICE_ERROR_is_thrown_when_correct_endpoint_is_hit(ServerScenario scenario) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + TRIGGER_UNHANDLED_ERROR_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.GENERIC_SERVICE_ERROR); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_WEBFLUX_MONO_ERROR_is_thrown_when_correct_endpoint_is_hit(ServerScenario scenario) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + MONO_ERROR_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleProjectApiError.WEBFLUX_MONO_ERROR); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_WEBFLUX_FLUX_ERROR_is_thrown_when_correct_endpoint_is_hit(ServerScenario scenario) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + FLUX_ERROR_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleProjectApiError.WEBFLUX_FLUX_ERROR); + } + + // *************** FRAMEWORK/FILTER ERRORS ****************** + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_NOT_FOUND_returned_if_unknown_path_is_requested(ServerScenario scenario) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(UUID.randomUUID().toString()) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.NOT_FOUND); + } + + private enum WebFilterErrorScenario { + EXCEPTION_THROWN_IN_WEB_FILTER( + "throw-web-filter-exception", SampleProjectApiError.ERROR_THROWN_IN_WEB_FILTER + ), + EXCEPTION_RETURNED_IN_WEB_FILTER( + "return-exception-in-web-filter-mono", SampleProjectApiError.ERROR_RETURNED_IN_WEB_FILTER_MONO + ); + + public final String triggeringHeaderName; + public final ApiError expectedError; + + WebFilterErrorScenario(String triggeringHeaderName, ApiError expectedError) { + this.triggeringHeaderName = triggeringHeaderName; + this.expectedError = expectedError; + } + } + + public static List webFilterErrorScenariosDataProvider() { + List result = new ArrayList<>(); + for (WebFilterErrorScenario webFilterErrorScenario : WebFilterErrorScenario.values()) { + for (ServerScenario serverScenario : ServerScenario.values()) { + result.add(new Object[]{webFilterErrorScenario, serverScenario}); + } + } + return result; + } + + @MethodSource("webFilterErrorScenariosDataProvider") + @ParameterizedTest + public void verify_expected_error_returned_if_web_filter_trigger_occurs( + WebFilterErrorScenario webFilterErrorScenario, ServerScenario serverScenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(serverScenario.serverPort) + .basePath("/doesnotmatter") + .header(webFilterErrorScenario.triggeringHeaderName, "true") + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, webFilterErrorScenario.expectedError); + } + + private enum RouterHandlerFilterErrorScenario { + EXCEPTION_THROWN_IN_ROUTER_HANDLER_FILTER( + "throw-handler-filter-function-exception", + SampleProjectApiError.ERROR_THROWN_IN_HANDLER_FILTER_FUNCTION + ), + EXCEPTION_RETURNED_IN_ROUTER_HANDLER_FILTER( + "return-exception-in-handler-filter-function-mono", + SampleProjectApiError.ERROR_RETURNED_IN_HANDLER_FILTER_FUNCTION_MONO + ); + + public final String triggeringHeaderName; + public final ApiError expectedError; + + RouterHandlerFilterErrorScenario(String triggeringHeaderName, ApiError expectedError) { + this.triggeringHeaderName = triggeringHeaderName; + this.expectedError = expectedError; + } + } + + public static List routerHandlerFilterErrorScenariosDataProvider() { + List result = new ArrayList<>(); + for (RouterHandlerFilterErrorScenario routerHandlerFilterErrorScenario : RouterHandlerFilterErrorScenario.values()) { + for (ServerScenario serverScenario : ServerScenario.values()) { + result.add(new Object[]{routerHandlerFilterErrorScenario, serverScenario}); + } + } + return result; + } + + @MethodSource("routerHandlerFilterErrorScenariosDataProvider") + @ParameterizedTest + public void verify_expected_error_returned_if_handler_filter_function_trigger_occurs( + RouterHandlerFilterErrorScenario routerHandlerFilterErrorScenario, ServerScenario serverScenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(serverScenario.serverPort) + .basePath(SAMPLE_FROM_ROUTER_FUNCTION_PATH) + .header(routerHandlerFilterErrorScenario.triggeringHeaderName, "true") + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, routerHandlerFilterErrorScenario.expectedError); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_METHOD_NOT_ALLOWED_returned_if_known_path_is_requested_with_invalid_http_method( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .log().all() + .when() + .delete() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.METHOD_NOT_ALLOWED); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_sample_get_fails_with_NO_ACCEPTABLE_REPRESENTATION_if_passed_invalid_accept_header( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .accept(ContentType.BINARY) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.NO_ACCEPTABLE_REPRESENTATION); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_sample_post_fails_with_UNSUPPORTED_MEDIA_TYPE_if_passed_invalid_content_type( + ServerScenario scenario + ) throws IOException { + SampleModel requestPayload = randomizedSampleModel(); + String requestPayloadAsString = objectMapper.writeValueAsString(requestPayload); + + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .body(requestPayloadAsString) + .contentType(ContentType.TEXT) + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.UNSUPPORTED_MEDIA_TYPE); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_MALFORMED_REQUEST_is_thrown_when_required_query_param_is_missing( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + WITH_REQUIRED_QUERY_PARAM_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived( + response, + new ApiErrorWithMetadata( + SampleCoreApiError.MALFORMED_REQUEST, + Pair.of("missing_param_name", "requiredQueryParamValue"), + Pair.of("missing_param_type", "int"), + Pair.of("required_location", "query_param") + ) + ); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert_type_for_query_param( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + WITH_REQUIRED_QUERY_PARAM_SUBPATH) + .queryParam("requiredQueryParamValue", "not-an-integer") + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, new ApiErrorWithMetadata( + SampleCoreApiError.TYPE_CONVERSION_ERROR, + // Springboot 3.0.x and 3.1.x WebFlux do not include the property name, so we can't check for them here. + MapBuilder.builder("bad_property_value",(Object)"not-an-integer") + .put("required_location","query_param") + .put("required_type", "int") + .build() + )); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_MALFORMED_REQUEST_is_thrown_when_required_header_is_missing( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + WITH_REQUIRED_HEADER_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived( + response, + new ApiErrorWithMetadata( + SampleCoreApiError.MALFORMED_REQUEST, + Pair.of("missing_param_name", "requiredHeaderValue"), + Pair.of("missing_param_type", "int"), + Pair.of("required_location", "header") + ) + ); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert_type_for_header( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + WITH_REQUIRED_HEADER_SUBPATH) + .header("requiredHeaderValue", "not-an-integer") + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, new ApiErrorWithMetadata( + SampleCoreApiError.TYPE_CONVERSION_ERROR, + // Springboot 3.0.x and 3.1.x WebFlux do not include the property name, so we can't check for them here. + MapBuilder.builder("bad_property_value",(Object)"not-an-integer") + .put("required_location","header") + .put("required_type", "int") + .build() + )); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_sample_post_fails_with_MISSING_EXPECTED_CONTENT_if_passed_empty_body( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .contentType(ContentType.JSON) + .body("") + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.MISSING_EXPECTED_CONTENT); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_sample_post_fails_with_MALFORMED_REQUEST_if_passed_junk_json( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .contentType(ContentType.JSON) + .body("{notjson blah") + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.MALFORMED_REQUEST); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_sample_post_fails_with_MALFORMED_REQUEST_if_passed_bad_json_body( + ServerScenario scenario + ) throws IOException { + SampleModel originalValidPayloadObj = randomizedSampleModel(); + String originalValidPayloadAsString = objectMapper.writeValueAsString(originalValidPayloadObj); + @SuppressWarnings("unchecked") + Map badRequestPayloadAsMap = objectMapper.readValue(originalValidPayloadAsString, Map.class); + badRequestPayloadAsMap.put("throw_manual_error", "not-a-boolean"); + String badJsonPayloadAsString = objectMapper.writeValueAsString(badRequestPayloadAsMap); + + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .contentType(ContentType.JSON) + .body(badJsonPayloadAsString) + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.MALFORMED_REQUEST); + } +} diff --git a/testonly/testonly-springboot3_0-webflux/src/test/java/serverconfig/classpathscan/Springboot3_0WebFluxClasspathScanConfig.java b/testonly/testonly-springboot3_0-webflux/src/test/java/serverconfig/classpathscan/Springboot3_0WebFluxClasspathScanConfig.java new file mode 100644 index 0000000..de27270 --- /dev/null +++ b/testonly/testonly-springboot3_0-webflux/src/test/java/serverconfig/classpathscan/Springboot3_0WebFluxClasspathScanConfig.java @@ -0,0 +1,64 @@ +package serverconfig.classpathscan; + +import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.server.WebFilter; + +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import testonly.componenttest.spring.reusable.controller.SampleWebFluxController; +import testonly.componenttest.spring.reusable.error.SampleProjectApiErrorsImpl; +import testonly.componenttest.spring.reusable.filter.ExplodingHandlerFilterFunction; +import testonly.componenttest.spring.reusable.filter.ExplodingWebFilter; + +import static org.springframework.web.reactive.function.server.RequestPredicates.GET; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.SAMPLE_FROM_ROUTER_FUNCTION_PATH; + +/** + * Springboot config that uses {@link ComponentScan} to integrate Backstopper via classpath scanning of the + * {@code com.nike.backstopper} package. + * + * @author Nic Munroe + */ +@SpringBootApplication +@ComponentScan(basePackages = { + // Component scan the core Backstopper+Spring WebFlux support. + "com.nike.backstopper", + // Component scan the controller (note this is the reusable WebFlux controller, not the reusable servlet controller). + "testonly.componenttest.spring.reusable.controller" +}) +@SuppressWarnings("unused") +public class Springboot3_0WebFluxClasspathScanConfig { + + @Bean + public ProjectApiErrors getProjectApiErrors() { + return new SampleProjectApiErrorsImpl(); + } + + @Bean + public Validator getJsr303Validator() { + //noinspection resource + return Validation.buildDefaultValidatorFactory().getValidator(); + } + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + public WebFilter explodingWebFilter() { + return new ExplodingWebFilter(); + } + + @Bean + public RouterFunction sampleRouterFunction(SampleWebFluxController sampleController) { + return RouterFunctions + .route(GET(SAMPLE_FROM_ROUTER_FUNCTION_PATH), sampleController::getSampleModelRouterFunction) + .filter(new ExplodingHandlerFilterFunction()); + } +} diff --git a/testonly/testonly-springboot3_0-webflux/src/test/java/serverconfig/directimport/Springboot3_0WebFluxDirectImportConfig.java b/testonly/testonly-springboot3_0-webflux/src/test/java/serverconfig/directimport/Springboot3_0WebFluxDirectImportConfig.java new file mode 100644 index 0000000..9f0e886 --- /dev/null +++ b/testonly/testonly-springboot3_0-webflux/src/test/java/serverconfig/directimport/Springboot3_0WebFluxDirectImportConfig.java @@ -0,0 +1,65 @@ +package serverconfig.directimport; + +import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; +import com.nike.backstopper.handler.spring.webflux.config.BackstopperSpringWebFluxConfig; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.server.WebFilter; + +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import testonly.componenttest.spring.reusable.controller.SampleWebFluxController; +import testonly.componenttest.spring.reusable.error.SampleProjectApiErrorsImpl; +import testonly.componenttest.spring.reusable.filter.ExplodingHandlerFilterFunction; +import testonly.componenttest.spring.reusable.filter.ExplodingWebFilter; + +import static org.springframework.web.reactive.function.server.RequestPredicates.GET; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.SAMPLE_FROM_ROUTER_FUNCTION_PATH; + +/** + * Springboot config that uses {@link Import} to integrate Backstopper via direct import of + * {@link BackstopperSpringWebFluxConfig}. + * + * @author Nic Munroe + */ +@SpringBootApplication +@Import({ + // Import core Backstopper+Spring WebFlux support. + BackstopperSpringWebFluxConfig.class, + // Import the controller. + SampleWebFluxController.class +}) +@SuppressWarnings("unused") +public class Springboot3_0WebFluxDirectImportConfig { + + @Bean + public ProjectApiErrors getProjectApiErrors() { + return new SampleProjectApiErrorsImpl(); + } + + @Bean + public Validator getJsr303Validator() { + //noinspection resource + return Validation.buildDefaultValidatorFactory().getValidator(); + } + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + public WebFilter explodingWebFilter() { + return new ExplodingWebFilter(); + } + + @Bean + public RouterFunction sampleRouterFunction(SampleWebFluxController sampleController) { + return RouterFunctions + .route(GET(SAMPLE_FROM_ROUTER_FUNCTION_PATH), sampleController::getSampleModelRouterFunction) + .filter(new ExplodingHandlerFilterFunction()); + } +} diff --git a/testonly/testonly-springboot3_0-webflux/src/test/resources/logback.xml b/testonly/testonly-springboot3_0-webflux/src/test/resources/logback.xml new file mode 100644 index 0000000..80adb28 --- /dev/null +++ b/testonly/testonly-springboot3_0-webflux/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd_HH:mm:ss.SSS} [%thread] |-%-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/testonly/testonly-springboot3_0-webmvc/README.md b/testonly/testonly-springboot3_0-webmvc/README.md new file mode 100644 index 0000000..8edf715 --- /dev/null +++ b/testonly/testonly-springboot3_0-webmvc/README.md @@ -0,0 +1,21 @@ +# Backstopper - testonly-springboot3_3-webmvc + +Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 17 and greater. + +(NOTE: The [Backstopper 1.x branch](https://github.com/Nike-Inc/backstopper/tree/v1.x) contains a version of +Backstopper for Java 7+, and for the `javax` ecosystem. The current Backstopper supports Java 17+ and the `jakarta` +ecosystem. The Backstopper 1.x releases also contain support for Spring 4 and 5, and Springboot 1 and 2.) + +This submodule contains tests to verify that the +[backstopper-spring-boot3-webmvc](../../backstopper-spring-boot3-webmvc) module's functionality works as expected in +Spring Boot 3.0.x Web MVC (Servlet) environments, for both classpath-scanning and direct-import Backstopper configuration +use cases. + +## More Info + +See the [base project README.md](../../README.md), [User Guide](../../USER_GUIDE.md), and Backstopper repository +source code and javadocs for all further information. + +## License + +Backstopper is released under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/testonly/testonly-springboot3_0-webmvc/build.gradle b/testonly/testonly-springboot3_0-webmvc/build.gradle new file mode 100644 index 0000000..8a9a82e --- /dev/null +++ b/testonly/testonly-springboot3_0-webmvc/build.gradle @@ -0,0 +1,43 @@ +evaluationDependsOn(':') + +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:${springboot3_0Version}") + } +} + +apply plugin: 'org.springframework.boot' +apply plugin: "io.spring.dependency-management" + +test { + useJUnitPlatform() +} + +dependencies { + implementation( + project(":backstopper-spring-boot3-webmvc"), + project(":backstopper-custom-validators"), + "ch.qos.logback:logback-classic", + "org.springframework.boot:spring-boot-dependencies:$springboot3_0Version", + "org.springframework.boot:spring-boot-starter-web", + "org.hibernate.validator:hibernate-validator", + "org.glassfish.expressly:expressly:$glassfishExpresslyVersion", + ) + testImplementation( + project(":backstopper-reusable-tests-junit5"), + project(":testonly:testonly-spring-webmvc-reusable-test-support"), + "org.junit.jupiter:junit-jupiter-api:$junit5Version", + "org.junit.jupiter:junit-jupiter-engine:$junit5Version", + "org.junit.jupiter:junit-jupiter-params:$junit5Version", + "org.mockito:mockito-core:$mockitoVersion", + "org.assertj:assertj-core:$assertJVersion", + "io.rest-assured:rest-assured", + ) +} + +// We're just running tests, not trying to stand up a real Springboot 3 server from gradle. +// Disable the bootJar task so gradle doesn't fall over. +bootJar.enabled = false diff --git a/testonly/testonly-springboot3_0-webmvc/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot3_0WebMvcComponentTest.java b/testonly/testonly-springboot3_0-webmvc/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot3_0WebMvcComponentTest.java new file mode 100644 index 0000000..18163b4 --- /dev/null +++ b/testonly/testonly-springboot3_0-webmvc/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot3_0WebMvcComponentTest.java @@ -0,0 +1,574 @@ +package com.nike.backstopper.testonly; + +import com.nike.backstopper.apierror.ApiError; +import com.nike.backstopper.apierror.ApiErrorWithMetadata; +import com.nike.backstopper.apierror.sample.SampleCoreApiError; +import com.nike.internal.util.MapBuilder; +import com.nike.internal.util.Pair; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ConfigurableApplicationContext; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import io.restassured.http.ContentType; +import io.restassured.response.ExtractableResponse; +import serverconfig.classpathscan.Springboot3_0WebMvcClasspathScanConfig; +import serverconfig.directimport.Springboot3_0WebMvcDirectImportConfig; +import testonly.componenttest.spring.reusable.error.SampleProjectApiError; +import testonly.componenttest.spring.reusable.model.RgbColor; +import testonly.componenttest.spring.reusable.model.SampleModel; + +import static com.nike.internal.util.testing.TestUtils.findFreePort; +import static io.restassured.RestAssured.given; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static testonly.componenttest.spring.reusable.controller.SampleController.CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH; +import static testonly.componenttest.spring.reusable.controller.SampleController.SAMPLE_PATH; +import static testonly.componenttest.spring.reusable.controller.SampleController.TRIGGER_UNHANDLED_ERROR_SUBPATH; +import static testonly.componenttest.spring.reusable.controller.SampleController.WITH_REQUIRED_HEADER_SUBPATH; +import static testonly.componenttest.spring.reusable.controller.SampleController.WITH_REQUIRED_QUERY_PARAM_SUBPATH; +import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.FOO_STRING_CANNOT_BE_BLANK; +import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.INVALID_RANGE_VALUE; +import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.NOT_RGB_COLOR_ENUM; +import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.RGB_COLOR_CANNOT_BE_NULL; +import static testonly.componenttest.spring.reusable.testutil.TestUtils.randomizedSampleModel; +import static testonly.componenttest.spring.reusable.testutil.TestUtils.verifyErrorReceived; + +/** + * Component test to verify that the functionality of {@code backstopper-spring-boot3-webmvc} works as expected in a + * Spring Boot 3.0.x Web MVC environment, for both classpath-scanning and direct-import Backstopper configuration use + * cases. + * + * @author Nic Munroe + */ +@SuppressWarnings({"ClassEscapesDefinedScope", "NewClassNamingConvention"}) +public class BackstopperSpringboot3_0WebMvcComponentTest { + + private static final int CLASSPATH_SCAN_SERVER_PORT = findFreePort(); + private static final int DIRECT_IMPORT_SERVER_PORT = findFreePort(); + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private static ConfigurableApplicationContext classpathScanServerAppContext; + private static ConfigurableApplicationContext directImportServerAppContext; + + @BeforeAll + public static void beforeClass() { + assertThat(CLASSPATH_SCAN_SERVER_PORT).isNotEqualTo(DIRECT_IMPORT_SERVER_PORT); + classpathScanServerAppContext = SpringApplication.run( + Springboot3_0WebMvcClasspathScanConfig.class, "--server.port=" + CLASSPATH_SCAN_SERVER_PORT + ); + directImportServerAppContext = SpringApplication.run( + Springboot3_0WebMvcDirectImportConfig.class, "--server.port=" + DIRECT_IMPORT_SERVER_PORT + ); + } + + @AfterAll + public static void afterClass() { + SpringApplication.exit(classpathScanServerAppContext); + SpringApplication.exit(directImportServerAppContext); + } + + @SuppressWarnings("unused") + private enum ServerScenario { + CLASSPATH_SCAN_SERVER(CLASSPATH_SCAN_SERVER_PORT), + DIRECT_IMPORT_SERVER(DIRECT_IMPORT_SERVER_PORT); + + public final int serverPort; + + ServerScenario(int serverPort) { + this.serverPort = serverPort; + } + } + + // *************** SUCCESSFUL (NON ERROR) CALLS ****************** + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_basic_sample_get(ServerScenario scenario) throws IOException { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + assertThat(response.statusCode()).isEqualTo(200); + SampleModel responseBody = objectMapper.readValue(response.asString(), SampleModel.class); + assertThat(responseBody).isNotNull(); + assertThat(responseBody.foo).isNotEmpty(); + assertThat(responseBody.range_0_to_42).isNotEmpty(); + assertThat(Integer.parseInt(responseBody.range_0_to_42)).isBetween(0, 42); + assertThat(responseBody.rgb_color).isNotEmpty(); + assertThat(RgbColor.toRgbColor(responseBody.rgb_color)).isNotNull(); + assertThat(responseBody.throw_manual_error).isFalse(); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_basic_sample_post(ServerScenario scenario) throws IOException { + SampleModel requestPayload = randomizedSampleModel(); + String requestPayloadAsString = objectMapper.writeValueAsString(requestPayload); + + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .contentType(ContentType.JSON) + .basePath(SAMPLE_PATH) + .body(requestPayloadAsString) + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + assertThat(response.statusCode()).isEqualTo(201); + SampleModel responseBody = objectMapper.readValue(response.asString(), SampleModel.class); + assertThat(responseBody).isNotNull(); + assertThat(responseBody.foo).isEqualTo(requestPayload.foo); + assertThat(responseBody.range_0_to_42).isEqualTo(requestPayload.range_0_to_42); + assertThat(responseBody.rgb_color).isEqualTo(requestPayload.rgb_color); + assertThat(responseBody.throw_manual_error).isEqualTo(requestPayload.throw_manual_error); + } + + // *************** JSR 303 AND ENDPOINT ERRORS ****************** + + @SuppressWarnings("unused") + private enum Jsr303SampleModelValidationScenario { + BLANK_FIELD_VIOLATION( + new SampleModel("", "42", "GREEN", false), + singletonList(FOO_STRING_CANNOT_BE_BLANK) + ), + INVALID_RANGE_VIOLATION( + new SampleModel("bar", "-1", "GREEN", false), + singletonList(INVALID_RANGE_VALUE) + ), + NULL_FIELD_VIOLATION( + new SampleModel("bar", "42", null, false), + singletonList(RGB_COLOR_CANNOT_BE_NULL) + ), + STRING_CONVERTS_TO_CLASSTYPE_VIOLATION( + new SampleModel("bar", "42", "car", false), + singletonList(NOT_RGB_COLOR_ENUM) + ), + MULTIPLE_VIOLATIONS( + new SampleModel(" \n\r\t ", "99", "tree", false), + Arrays.asList(FOO_STRING_CANNOT_BE_BLANK, INVALID_RANGE_VALUE, NOT_RGB_COLOR_ENUM) + ); + + public final SampleModel model; + public final List expectedErrors; + + Jsr303SampleModelValidationScenario( + SampleModel model, List expectedErrors + ) { + this.model = model; + this.expectedErrors = expectedErrors; + } + } + + public static List jsr303ValidationErrorScenariosDataProvider() { + List result = new ArrayList<>(); + for (Jsr303SampleModelValidationScenario violationScenario : Jsr303SampleModelValidationScenario.values()) { + for (ServerScenario serverScenario : ServerScenario.values()) { + result.add(new Object[]{violationScenario, serverScenario}); + } + } + return result; + } + + @MethodSource("jsr303ValidationErrorScenariosDataProvider") + @ParameterizedTest + public void verify_jsr303_validation_errors( + Jsr303SampleModelValidationScenario violationScenario, ServerScenario serverScenario + ) throws JsonProcessingException { + String requestPayloadAsString = objectMapper.writeValueAsString(violationScenario.model); + + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(serverScenario.serverPort) + .contentType(ContentType.JSON) + .basePath(SAMPLE_PATH) + .body(requestPayloadAsString) + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + List expectedErrors = new ArrayList<>(); + for (ApiError expectedApiError : violationScenario.expectedErrors) { + String extraMetadataFieldValue = null; + + if (INVALID_RANGE_VALUE.equals(expectedApiError)) { + extraMetadataFieldValue = "range_0_to_42"; + } + else if (RGB_COLOR_CANNOT_BE_NULL.equals(expectedApiError) || NOT_RGB_COLOR_ENUM.equals(expectedApiError)) { + extraMetadataFieldValue = "rgb_color"; + } + + if (extraMetadataFieldValue != null) { + expectedApiError = new ApiErrorWithMetadata( + expectedApiError, + MapBuilder.builder("field", (Object) extraMetadataFieldValue).build() + ); + } + + expectedErrors.add(expectedApiError); + } + verifyErrorReceived(response, expectedErrors, 400); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_MANUALLY_THROWN_ERROR_is_thrown_when_requested(ServerScenario scenario) throws IOException { + SampleModel requestPayload = new SampleModel("bar", "42", "RED", true); + String requestPayloadAsString = objectMapper.writeValueAsString(requestPayload); + + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .contentType(ContentType.JSON) + .basePath(SAMPLE_PATH) + .body(requestPayloadAsString) + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleProjectApiError.MANUALLY_THROWN_ERROR); + // This code path also should add some custom headers to the response + assertThat(response.headers().getValues("rgbColorValue")).isEqualTo(singletonList(requestPayload.rgb_color)); + assertThat(response.headers().getValues("otherExtraMultivalueHeader")).isEqualTo(Arrays.asList("foo", "bar")); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_SOME_MEANINGFUL_ERROR_NAME_is_thrown_when_correct_endpoint_is_hit(ServerScenario scenario) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleProjectApiError.SOME_MEANINGFUL_ERROR_NAME); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_GENERIC_SERVICE_ERROR_is_thrown_when_correct_endpoint_is_hit(ServerScenario scenario) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + TRIGGER_UNHANDLED_ERROR_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.GENERIC_SERVICE_ERROR); + } + + // *************** FRAMEWORK ERRORS ****************** + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_NOT_FOUND_returned_if_unknown_path_is_requested(ServerScenario scenario) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(UUID.randomUUID().toString()) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.NOT_FOUND); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_ERROR_THROWN_IN_SERVLET_FILTER_OUTSIDE_SPRING_returned_if_servlet_filter_trigger_occurs( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .header("throw-servlet-filter-exception", "true") + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleProjectApiError.ERROR_THROWN_IN_SERVLET_FILTER_OUTSIDE_SPRING); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_METHOD_NOT_ALLOWED_returned_if_known_path_is_requested_with_invalid_http_method( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .log().all() + .when() + .delete() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.METHOD_NOT_ALLOWED); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_sample_get_fails_with_NO_ACCEPTABLE_REPRESENTATION_if_passed_invalid_accept_header( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .accept(ContentType.BINARY) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.NO_ACCEPTABLE_REPRESENTATION); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_sample_post_fails_with_UNSUPPORTED_MEDIA_TYPE_if_passed_invalid_content_type( + ServerScenario scenario + ) throws IOException { + SampleModel requestPayload = randomizedSampleModel(); + String requestPayloadAsString = objectMapper.writeValueAsString(requestPayload); + + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .body(requestPayloadAsString) + .contentType(ContentType.TEXT) + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.UNSUPPORTED_MEDIA_TYPE); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_MALFORMED_REQUEST_is_thrown_when_required_query_param_is_missing( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + WITH_REQUIRED_QUERY_PARAM_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived( + response, + new ApiErrorWithMetadata( + SampleCoreApiError.MALFORMED_REQUEST, + Pair.of("missing_param_name", "requiredQueryParamValue"), + Pair.of("missing_param_type", "int"), + Pair.of("required_location", "query_param") + ) + ); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert_type_for_query_param( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + WITH_REQUIRED_QUERY_PARAM_SUBPATH) + .queryParam("requiredQueryParamValue", "not-an-integer") + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, new ApiErrorWithMetadata( + SampleCoreApiError.TYPE_CONVERSION_ERROR, + MapBuilder.builder("bad_property_name", (Object) "requiredQueryParamValue") + .put("bad_property_value","not-an-integer") + .put("required_location","query_param") + .put("required_type", "int") + .build() + )); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_MALFORMED_REQUEST_is_thrown_when_required_header_is_missing( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + WITH_REQUIRED_HEADER_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived( + response, + new ApiErrorWithMetadata( + SampleCoreApiError.MALFORMED_REQUEST, + Pair.of("missing_param_name", "requiredHeaderValue"), + Pair.of("missing_param_type", "int"), + Pair.of("required_location", "header") + ) + ); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert_type_for_header( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + WITH_REQUIRED_HEADER_SUBPATH) + .header("requiredHeaderValue", "not-an-integer") + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, new ApiErrorWithMetadata( + SampleCoreApiError.TYPE_CONVERSION_ERROR, + MapBuilder.builder("bad_property_name", (Object) "requiredHeaderValue") + .put("bad_property_value","not-an-integer") + .put("required_location","header") + .put("required_type", "int") + .build() + )); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_sample_post_fails_with_MISSING_EXPECTED_CONTENT_if_passed_empty_body( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .contentType(ContentType.JSON) + .body("") + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.MISSING_EXPECTED_CONTENT); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_sample_post_fails_with_MALFORMED_REQUEST_if_passed_bad_json_body( + ServerScenario scenario + ) throws IOException { + SampleModel originalValidPayloadObj = randomizedSampleModel(); + String originalValidPayloadAsString = objectMapper.writeValueAsString(originalValidPayloadObj); + @SuppressWarnings("unchecked") + Map badRequestPayloadAsMap = objectMapper.readValue(originalValidPayloadAsString, Map.class); + badRequestPayloadAsMap.put("throw_manual_error", "not-a-boolean"); + String badJsonPayloadAsString = objectMapper.writeValueAsString(badRequestPayloadAsMap); + + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .contentType(ContentType.JSON) + .body(badJsonPayloadAsString) + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.MALFORMED_REQUEST); + } +} diff --git a/testonly/testonly-springboot3_0-webmvc/src/test/java/serverconfig/classpathscan/Springboot3_0WebMvcClasspathScanConfig.java b/testonly/testonly-springboot3_0-webmvc/src/test/java/serverconfig/classpathscan/Springboot3_0WebMvcClasspathScanConfig.java new file mode 100644 index 0000000..3028fbd --- /dev/null +++ b/testonly/testonly-springboot3_0-webmvc/src/test/java/serverconfig/classpathscan/Springboot3_0WebMvcClasspathScanConfig.java @@ -0,0 +1,49 @@ +package serverconfig.classpathscan; + +import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.core.Ordered; + +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import testonly.componenttest.spring.reusable.error.SampleProjectApiErrorsImpl; +import testonly.componenttest.spring.reusable.testutil.ExplodingServletFilter; + +/** + * Springboot config that uses {@link ComponentScan} to integrate Backstopper via classpath scanning of the + * {@code com.nike.backstopper} package. + * + * @author Nic Munroe + */ +@SpringBootApplication +@ComponentScan(basePackages = { + // Component scan the core Backstopper+Springboot1 support. + "com.nike.backstopper", + // Component scan the controller. + "testonly.componenttest.spring.reusable.controller" +}) +@SuppressWarnings("unused") +public class Springboot3_0WebMvcClasspathScanConfig { + + @Bean + public ProjectApiErrors getProjectApiErrors() { + return new SampleProjectApiErrorsImpl(); + } + + @Bean + public Validator getJsr303Validator() { + //noinspection resource + return Validation.buildDefaultValidatorFactory().getValidator(); + } + + @Bean + public FilterRegistrationBean explodingServletFilter() { + FilterRegistrationBean frb = new FilterRegistrationBean<>(new ExplodingServletFilter()); + frb.setOrder(Ordered.HIGHEST_PRECEDENCE); + return frb; + } +} diff --git a/testonly/testonly-springboot3_0-webmvc/src/test/java/serverconfig/directimport/Springboot3_0WebMvcDirectImportConfig.java b/testonly/testonly-springboot3_0-webmvc/src/test/java/serverconfig/directimport/Springboot3_0WebMvcDirectImportConfig.java new file mode 100644 index 0000000..ffc939a --- /dev/null +++ b/testonly/testonly-springboot3_0-webmvc/src/test/java/serverconfig/directimport/Springboot3_0WebMvcDirectImportConfig.java @@ -0,0 +1,51 @@ +package serverconfig.directimport; + +import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; +import com.nike.backstopper.handler.springboot.config.BackstopperSpringboot3WebMvcConfig; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.core.Ordered; + +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import testonly.componenttest.spring.reusable.controller.SampleController; +import testonly.componenttest.spring.reusable.error.SampleProjectApiErrorsImpl; +import testonly.componenttest.spring.reusable.testutil.ExplodingServletFilter; + +/** + * Springboot config that uses {@link Import} to integrate Backstopper via direct import of + * {@link BackstopperSpringboot3WebMvcConfig}. + * + * @author Nic Munroe + */ +@SpringBootApplication +@Import({ + // Import core Backstopper+Springboot1 support. + BackstopperSpringboot3WebMvcConfig.class, + // Import the controller. + SampleController.class +}) +@SuppressWarnings("unused") +public class Springboot3_0WebMvcDirectImportConfig { + + @Bean + public ProjectApiErrors getProjectApiErrors() { + return new SampleProjectApiErrorsImpl(); + } + + @Bean + public Validator getJsr303Validator() { + //noinspection resource + return Validation.buildDefaultValidatorFactory().getValidator(); + } + + @Bean + public FilterRegistrationBean explodingServletFilter() { + FilterRegistrationBean frb = new FilterRegistrationBean<>(new ExplodingServletFilter()); + frb.setOrder(Ordered.HIGHEST_PRECEDENCE); + return frb; + } +} diff --git a/testonly/testonly-springboot3_0-webmvc/src/test/resources/logback.xml b/testonly/testonly-springboot3_0-webmvc/src/test/resources/logback.xml new file mode 100644 index 0000000..80adb28 --- /dev/null +++ b/testonly/testonly-springboot3_0-webmvc/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd_HH:mm:ss.SSS} [%thread] |-%-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/testonly/testonly-springboot3_3-webflux/README.md b/testonly/testonly-springboot3_3-webflux/README.md index e484e98..28a378e 100644 --- a/testonly/testonly-springboot3_3-webflux/README.md +++ b/testonly/testonly-springboot3_3-webflux/README.md @@ -8,7 +8,7 @@ ecosystem. The Backstopper 1.x releases also contain support for Spring 4 and 5, This submodule contains tests to verify that the [backstopper-spring-web-flux](../../backstopper-spring-web-flux) module's functionality works as expected in -Spring Boot 3.3 WebFlux (Netty) environments, for both classpath-scanning and direct-import Backstopper configuration +Spring Boot 3.3.x WebFlux (Netty) environments, for both classpath-scanning and direct-import Backstopper configuration use cases. ## More Info diff --git a/testonly/testonly-springboot3_3-webflux/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot3_3WebFluxComponentTest.java b/testonly/testonly-springboot3_3-webflux/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot3_3WebFluxComponentTest.java index 055c0ee..9e05da7 100644 --- a/testonly/testonly-springboot3_3-webflux/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot3_3WebFluxComponentTest.java +++ b/testonly/testonly-springboot3_3-webflux/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot3_3WebFluxComponentTest.java @@ -55,7 +55,7 @@ /** * Component test to verify that the functionality of {@code backstopper-spring-web-flux} works as expected in a - * Spring Boot 3.3 WebFlux environment, for both classpath-scanning and direct-import Backstopper configuration use + * Spring Boot 3.3.x WebFlux environment, for both classpath-scanning and direct-import Backstopper configuration use * cases. * * @author Nic Munroe diff --git a/testonly/testonly-springboot3_3-webmvc/README.md b/testonly/testonly-springboot3_3-webmvc/README.md index f6301dc..a6f8424 100644 --- a/testonly/testonly-springboot3_3-webmvc/README.md +++ b/testonly/testonly-springboot3_3-webmvc/README.md @@ -8,7 +8,7 @@ ecosystem. The Backstopper 1.x releases also contain support for Spring 4 and 5, This submodule contains tests to verify that the [backstopper-spring-boot3-webmvc](../../backstopper-spring-boot3-webmvc) module's functionality works as expected in -Spring Boot 3.3 Web MVC (Servlet) environments, for both classpath-scanning and direct-import Backstopper configuration +Spring Boot 3.3.x Web MVC (Servlet) environments, for both classpath-scanning and direct-import Backstopper configuration use cases. ## More Info From 266a84cf95325b424b0fc3de2cc4393eb4454a58 Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Wed, 11 Sep 2024 18:19:26 -0700 Subject: [PATCH 29/42] Add testonly modules for springboot 3.1.x for webmvc and webflux --- settings.gradle | 2 + .../testonly-springboot3_0-webflux/README.md | 2 +- .../testonly-springboot3_0-webmvc/README.md | 2 +- .../testonly-springboot3_1-webflux/README.md | 21 + .../build.gradle | 44 + ...pperSpringboot3_1WebFluxComponentTest.java | 762 ++++++++++++++++++ ...ringboot3_1WebFluxClasspathScanConfig.java | 64 ++ ...pringboot3_1WebFluxDirectImportConfig.java | 65 ++ .../src/test/resources/logback.xml | 11 + .../testonly-springboot3_1-webmvc/README.md | 21 + .../build.gradle | 43 + ...opperSpringboot3_1WebMvcComponentTest.java | 574 +++++++++++++ ...pringboot3_1WebMvcClasspathScanConfig.java | 49 ++ ...Springboot3_1WebMvcDirectImportConfig.java | 51 ++ .../src/test/resources/logback.xml | 11 + 15 files changed, 1720 insertions(+), 2 deletions(-) create mode 100644 testonly/testonly-springboot3_1-webflux/README.md create mode 100644 testonly/testonly-springboot3_1-webflux/build.gradle create mode 100644 testonly/testonly-springboot3_1-webflux/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot3_1WebFluxComponentTest.java create mode 100644 testonly/testonly-springboot3_1-webflux/src/test/java/serverconfig/classpathscan/Springboot3_1WebFluxClasspathScanConfig.java create mode 100644 testonly/testonly-springboot3_1-webflux/src/test/java/serverconfig/directimport/Springboot3_1WebFluxDirectImportConfig.java create mode 100644 testonly/testonly-springboot3_1-webflux/src/test/resources/logback.xml create mode 100644 testonly/testonly-springboot3_1-webmvc/README.md create mode 100644 testonly/testonly-springboot3_1-webmvc/build.gradle create mode 100644 testonly/testonly-springboot3_1-webmvc/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot3_1WebMvcComponentTest.java create mode 100644 testonly/testonly-springboot3_1-webmvc/src/test/java/serverconfig/classpathscan/Springboot3_1WebMvcClasspathScanConfig.java create mode 100644 testonly/testonly-springboot3_1-webmvc/src/test/java/serverconfig/directimport/Springboot3_1WebMvcDirectImportConfig.java create mode 100644 testonly/testonly-springboot3_1-webmvc/src/test/resources/logback.xml diff --git a/settings.gradle b/settings.gradle index e1b130d..84dc0b0 100644 --- a/settings.gradle +++ b/settings.gradle @@ -18,6 +18,8 @@ include "nike-internal-util", "testonly:testonly-spring-6_1-webmvc", "testonly:testonly-springboot3_0-webmvc", "testonly:testonly-springboot3_0-webflux", + "testonly:testonly-springboot3_1-webmvc", + "testonly:testonly-springboot3_1-webflux", "testonly:testonly-springboot3_3-webmvc", "testonly:testonly-springboot3_3-webflux", // Sample modules (not published) diff --git a/testonly/testonly-springboot3_0-webflux/README.md b/testonly/testonly-springboot3_0-webflux/README.md index 2937f94..c95950c 100644 --- a/testonly/testonly-springboot3_0-webflux/README.md +++ b/testonly/testonly-springboot3_0-webflux/README.md @@ -1,4 +1,4 @@ -# Backstopper - testonly-springboot3_3-webflux +# Backstopper - testonly-springboot3_0-webflux Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 17 and greater. diff --git a/testonly/testonly-springboot3_0-webmvc/README.md b/testonly/testonly-springboot3_0-webmvc/README.md index 8edf715..0950029 100644 --- a/testonly/testonly-springboot3_0-webmvc/README.md +++ b/testonly/testonly-springboot3_0-webmvc/README.md @@ -1,4 +1,4 @@ -# Backstopper - testonly-springboot3_3-webmvc +# Backstopper - testonly-springboot3_0-webmvc Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 17 and greater. diff --git a/testonly/testonly-springboot3_1-webflux/README.md b/testonly/testonly-springboot3_1-webflux/README.md new file mode 100644 index 0000000..b5cc560 --- /dev/null +++ b/testonly/testonly-springboot3_1-webflux/README.md @@ -0,0 +1,21 @@ +# Backstopper - testonly-springboot3_1-webflux + +Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 17 and greater. + +(NOTE: The [Backstopper 1.x branch](https://github.com/Nike-Inc/backstopper/tree/v1.x) contains a version of +Backstopper for Java 7+, and for the `javax` ecosystem. The current Backstopper supports Java 17+ and the `jakarta` +ecosystem. The Backstopper 1.x releases also contain support for Spring 4 and 5, and Springboot 1 and 2.) + +This submodule contains tests to verify that the +[backstopper-spring-web-flux](../../backstopper-spring-web-flux) module's functionality works as expected in +Spring Boot 3.1.x WebFlux (Netty) environments, for both classpath-scanning and direct-import Backstopper configuration +use cases. + +## More Info + +See the [base project README.md](../../README.md), [User Guide](../../USER_GUIDE.md), and Backstopper repository +source code and javadocs for all further information. + +## License + +Backstopper is released under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/testonly/testonly-springboot3_1-webflux/build.gradle b/testonly/testonly-springboot3_1-webflux/build.gradle new file mode 100644 index 0000000..26b4fd3 --- /dev/null +++ b/testonly/testonly-springboot3_1-webflux/build.gradle @@ -0,0 +1,44 @@ +evaluationDependsOn(':') + +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:${springboot3_1Version}") + } +} + +apply plugin: 'org.springframework.boot' +apply plugin: "io.spring.dependency-management" + +test { + useJUnitPlatform() +} + +dependencies { + implementation( + project(":backstopper-spring-web-flux"), + project(":backstopper-custom-validators"), + "ch.qos.logback:logback-classic", + "org.springframework.boot:spring-boot-dependencies:$springboot3_1Version", + "org.springframework.boot:spring-boot-starter-webflux", + "org.hibernate.validator:hibernate-validator", + "org.glassfish.expressly:expressly:$glassfishExpresslyVersion", + ) + testImplementation( + project(":backstopper-reusable-tests-junit5"), + project(":testonly:testonly-spring-webflux-reusable-test-support"), + "org.jetbrains:annotations:$jetbrainsAnnotationsVersion", + "org.junit.jupiter:junit-jupiter-api:$junit5Version", + "org.junit.jupiter:junit-jupiter-engine:$junit5Version", + "org.junit.jupiter:junit-jupiter-params:$junit5Version", + "org.mockito:mockito-core:$mockitoVersion", + "org.assertj:assertj-core:$assertJVersion", + "io.rest-assured:rest-assured", + ) +} + +// We're just running tests, not trying to stand up a real Springboot 2 server from gradle. +// Disable the bootJar task so gradle doesn't fall over. +bootJar.enabled = false diff --git a/testonly/testonly-springboot3_1-webflux/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot3_1WebFluxComponentTest.java b/testonly/testonly-springboot3_1-webflux/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot3_1WebFluxComponentTest.java new file mode 100644 index 0000000..8c44c4b --- /dev/null +++ b/testonly/testonly-springboot3_1-webflux/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot3_1WebFluxComponentTest.java @@ -0,0 +1,762 @@ +package com.nike.backstopper.testonly; + +import com.nike.backstopper.apierror.ApiError; +import com.nike.backstopper.apierror.ApiErrorWithMetadata; +import com.nike.backstopper.apierror.sample.SampleCoreApiError; +import com.nike.internal.util.MapBuilder; +import com.nike.internal.util.Pair; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ConfigurableApplicationContext; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import io.restassured.http.ContentType; +import io.restassured.response.ExtractableResponse; +import serverconfig.classpathscan.Springboot3_1WebFluxClasspathScanConfig; +import serverconfig.directimport.Springboot3_1WebFluxDirectImportConfig; +import testonly.componenttest.spring.reusable.error.SampleProjectApiError; +import testonly.componenttest.spring.reusable.model.RgbColor; +import testonly.componenttest.spring.reusable.model.SampleModel; + +import static com.nike.internal.util.testing.TestUtils.findFreePort; +import static io.restassured.RestAssured.given; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.FLUX_ERROR_SUBPATH; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.MONO_ERROR_SUBPATH; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.SAMPLE_FLUX_SUBPATH; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.SAMPLE_FROM_ROUTER_FUNCTION_PATH; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.SAMPLE_PATH; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.TRIGGER_UNHANDLED_ERROR_SUBPATH; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.WITH_REQUIRED_HEADER_SUBPATH; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.WITH_REQUIRED_QUERY_PARAM_SUBPATH; +import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.FOO_STRING_CANNOT_BE_BLANK; +import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.INVALID_RANGE_VALUE; +import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.NOT_RGB_COLOR_ENUM; +import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.RGB_COLOR_CANNOT_BE_NULL; +import static testonly.componenttest.spring.reusable.testutil.TestUtils.randomizedSampleModel; +import static testonly.componenttest.spring.reusable.testutil.TestUtils.verifyErrorReceived; + +/** + * Component test to verify that the functionality of {@code backstopper-spring-web-flux} works as expected in a + * Spring Boot 3.1.x WebFlux environment, for both classpath-scanning and direct-import Backstopper configuration use + * cases. + * + * @author Nic Munroe + */ +@SuppressWarnings({"NewClassNamingConvention", "ClassEscapesDefinedScope"}) +public class BackstopperSpringboot3_1WebFluxComponentTest { + + private static final int CLASSPATH_SCAN_SERVER_PORT = findFreePort(); + private static final int DIRECT_IMPORT_SERVER_PORT = findFreePort(); + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private static ConfigurableApplicationContext classpathScanServerAppContext; + private static ConfigurableApplicationContext directImportServerAppContext; + + @BeforeAll + public static void beforeClass() { + assertThat(CLASSPATH_SCAN_SERVER_PORT).isNotEqualTo(DIRECT_IMPORT_SERVER_PORT); + classpathScanServerAppContext = SpringApplication.run( + Springboot3_1WebFluxClasspathScanConfig.class, "--server.port=" + CLASSPATH_SCAN_SERVER_PORT + ); + directImportServerAppContext = SpringApplication.run( + Springboot3_1WebFluxDirectImportConfig.class, "--server.port=" + DIRECT_IMPORT_SERVER_PORT + ); + } + + @AfterAll + public static void afterClass() { + SpringApplication.exit(classpathScanServerAppContext); + SpringApplication.exit(directImportServerAppContext); + } + + @SuppressWarnings("unused") + private enum ServerScenario { + CLASSPATH_SCAN_SERVER(CLASSPATH_SCAN_SERVER_PORT), + DIRECT_IMPORT_SERVER(DIRECT_IMPORT_SERVER_PORT); + + public final int serverPort; + + ServerScenario(int serverPort) { + this.serverPort = serverPort; + } + } + + // *************** SUCCESSFUL (NON ERROR) CALLS ****************** + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_basic_sample_get(ServerScenario scenario) throws IOException { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + assertThat(response.statusCode()).isEqualTo(200); + SampleModel responseBody = objectMapper.readValue(response.asString(), SampleModel.class); + verifyNewSampleModel(responseBody); + } + + private void verifyNewSampleModel(SampleModel sampleModel) { + assertThat(sampleModel).isNotNull(); + assertThat(sampleModel.foo).isNotEmpty(); + assertThat(sampleModel.range_0_to_42).isNotEmpty(); + assertThat(Integer.parseInt(sampleModel.range_0_to_42)).isBetween(0, 42); + assertThat(sampleModel.rgb_color).isNotEmpty(); + assertThat(RgbColor.toRgbColor(sampleModel.rgb_color)).isNotNull(); + assertThat(sampleModel.throw_manual_error).isFalse(); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_basic_sample_post(ServerScenario scenario) throws IOException { + SampleModel requestPayload = randomizedSampleModel(); + String requestPayloadAsString = objectMapper.writeValueAsString(requestPayload); + + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .contentType(ContentType.JSON) + .basePath(SAMPLE_PATH) + .body(requestPayloadAsString) + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + assertThat(response.statusCode()).isEqualTo(201); + SampleModel responseBody = objectMapper.readValue(response.asString(), SampleModel.class); + assertThat(responseBody).isNotNull(); + assertThat(responseBody.foo).isEqualTo(requestPayload.foo); + assertThat(responseBody.range_0_to_42).isEqualTo(requestPayload.range_0_to_42); + assertThat(responseBody.rgb_color).isEqualTo(requestPayload.rgb_color); + assertThat(responseBody.throw_manual_error).isEqualTo(requestPayload.throw_manual_error); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_router_function_sample_get(ServerScenario scenario) throws IOException { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_FROM_ROUTER_FUNCTION_PATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + assertThat(response.statusCode()).isEqualTo(200); + SampleModel responseBody = objectMapper.readValue(response.asString(), SampleModel.class); + verifyNewSampleModel(responseBody); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_flux_sample_get(ServerScenario scenario) throws IOException { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + SAMPLE_FLUX_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + assertThat(response.statusCode()).isEqualTo(200); + List responseBody = objectMapper.readValue( + response.asString(), new TypeReference<>() {} + ); + + assertThat(responseBody).hasSizeGreaterThan(1); + responseBody.forEach(this::verifyNewSampleModel); + } + + // *************** JSR 303 AND ENDPOINT ERRORS ****************** + + @SuppressWarnings("unused") + private enum Jsr303SampleModelValidationScenario { + BLANK_FIELD_VIOLATION( + new SampleModel("", "42", "GREEN", false), + singletonList(FOO_STRING_CANNOT_BE_BLANK) + ), + INVALID_RANGE_VIOLATION( + new SampleModel("bar", "-1", "GREEN", false), + singletonList(INVALID_RANGE_VALUE) + ), + NULL_FIELD_VIOLATION( + new SampleModel("bar", "42", null, false), + singletonList(RGB_COLOR_CANNOT_BE_NULL) + ), + STRING_CONVERTS_TO_CLASSTYPE_VIOLATION( + new SampleModel("bar", "42", "car", false), + singletonList(NOT_RGB_COLOR_ENUM) + ), + MULTIPLE_VIOLATIONS( + new SampleModel(" \n\r\t ", "99", "tree", false), + Arrays.asList(FOO_STRING_CANNOT_BE_BLANK, INVALID_RANGE_VALUE, NOT_RGB_COLOR_ENUM) + ); + + public final SampleModel model; + public final List expectedErrors; + + Jsr303SampleModelValidationScenario( + SampleModel model, List expectedErrors + ) { + this.model = model; + this.expectedErrors = expectedErrors; + } + } + + public static List jsr303ValidationErrorScenariosDataProvider() { + List result = new ArrayList<>(); + for (Jsr303SampleModelValidationScenario violationScenario : Jsr303SampleModelValidationScenario.values()) { + for (ServerScenario serverScenario : ServerScenario.values()) { + result.add(new Object[]{violationScenario, serverScenario}); + } + } + return result; + } + + @MethodSource("jsr303ValidationErrorScenariosDataProvider") + @ParameterizedTest + public void verify_jsr303_validation_errors( + Jsr303SampleModelValidationScenario violationScenario, ServerScenario serverScenario + ) throws JsonProcessingException { + String requestPayloadAsString = objectMapper.writeValueAsString(violationScenario.model); + + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(serverScenario.serverPort) + .contentType(ContentType.JSON) + .basePath(SAMPLE_PATH) + .body(requestPayloadAsString) + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + List expectedErrors = new ArrayList<>(); + for (ApiError expectedApiError : violationScenario.expectedErrors) { + String extraMetadataFieldValue = null; + + if (INVALID_RANGE_VALUE.equals(expectedApiError)) { + extraMetadataFieldValue = "range_0_to_42"; + } + else if (RGB_COLOR_CANNOT_BE_NULL.equals(expectedApiError) || NOT_RGB_COLOR_ENUM.equals(expectedApiError)) { + extraMetadataFieldValue = "rgb_color"; + } + + if (extraMetadataFieldValue != null) { + expectedApiError = new ApiErrorWithMetadata( + expectedApiError, + MapBuilder.builder("field", (Object) extraMetadataFieldValue).build() + ); + } + + expectedErrors.add(expectedApiError); + } + verifyErrorReceived(response, expectedErrors, 400); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_MANUALLY_THROWN_ERROR_is_thrown_when_requested(ServerScenario scenario) throws IOException { + SampleModel requestPayload = new SampleModel("bar", "42", "RED", true); + String requestPayloadAsString = objectMapper.writeValueAsString(requestPayload); + + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .contentType(ContentType.JSON) + .basePath(SAMPLE_PATH) + .body(requestPayloadAsString) + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleProjectApiError.MANUALLY_THROWN_ERROR); + // This code path also should add some custom headers to the response + assertThat(response.headers().getValues("rgbColorValue")).isEqualTo(singletonList(requestPayload.rgb_color)); + assertThat(response.headers().getValues("otherExtraMultivalueHeader")).isEqualTo(Arrays.asList("foo", "bar")); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_SOME_MEANINGFUL_ERROR_NAME_is_thrown_when_correct_endpoint_is_hit(ServerScenario scenario) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleProjectApiError.SOME_MEANINGFUL_ERROR_NAME); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_GENERIC_SERVICE_ERROR_is_thrown_when_correct_endpoint_is_hit(ServerScenario scenario) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + TRIGGER_UNHANDLED_ERROR_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.GENERIC_SERVICE_ERROR); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_WEBFLUX_MONO_ERROR_is_thrown_when_correct_endpoint_is_hit(ServerScenario scenario) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + MONO_ERROR_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleProjectApiError.WEBFLUX_MONO_ERROR); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_WEBFLUX_FLUX_ERROR_is_thrown_when_correct_endpoint_is_hit(ServerScenario scenario) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + FLUX_ERROR_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleProjectApiError.WEBFLUX_FLUX_ERROR); + } + + // *************** FRAMEWORK/FILTER ERRORS ****************** + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_NOT_FOUND_returned_if_unknown_path_is_requested(ServerScenario scenario) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(UUID.randomUUID().toString()) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.NOT_FOUND); + } + + private enum WebFilterErrorScenario { + EXCEPTION_THROWN_IN_WEB_FILTER( + "throw-web-filter-exception", SampleProjectApiError.ERROR_THROWN_IN_WEB_FILTER + ), + EXCEPTION_RETURNED_IN_WEB_FILTER( + "return-exception-in-web-filter-mono", SampleProjectApiError.ERROR_RETURNED_IN_WEB_FILTER_MONO + ); + + public final String triggeringHeaderName; + public final ApiError expectedError; + + WebFilterErrorScenario(String triggeringHeaderName, ApiError expectedError) { + this.triggeringHeaderName = triggeringHeaderName; + this.expectedError = expectedError; + } + } + + public static List webFilterErrorScenariosDataProvider() { + List result = new ArrayList<>(); + for (WebFilterErrorScenario webFilterErrorScenario : WebFilterErrorScenario.values()) { + for (ServerScenario serverScenario : ServerScenario.values()) { + result.add(new Object[]{webFilterErrorScenario, serverScenario}); + } + } + return result; + } + + @MethodSource("webFilterErrorScenariosDataProvider") + @ParameterizedTest + public void verify_expected_error_returned_if_web_filter_trigger_occurs( + WebFilterErrorScenario webFilterErrorScenario, ServerScenario serverScenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(serverScenario.serverPort) + .basePath("/doesnotmatter") + .header(webFilterErrorScenario.triggeringHeaderName, "true") + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, webFilterErrorScenario.expectedError); + } + + private enum RouterHandlerFilterErrorScenario { + EXCEPTION_THROWN_IN_ROUTER_HANDLER_FILTER( + "throw-handler-filter-function-exception", + SampleProjectApiError.ERROR_THROWN_IN_HANDLER_FILTER_FUNCTION + ), + EXCEPTION_RETURNED_IN_ROUTER_HANDLER_FILTER( + "return-exception-in-handler-filter-function-mono", + SampleProjectApiError.ERROR_RETURNED_IN_HANDLER_FILTER_FUNCTION_MONO + ); + + public final String triggeringHeaderName; + public final ApiError expectedError; + + RouterHandlerFilterErrorScenario(String triggeringHeaderName, ApiError expectedError) { + this.triggeringHeaderName = triggeringHeaderName; + this.expectedError = expectedError; + } + } + + public static List routerHandlerFilterErrorScenariosDataProvider() { + List result = new ArrayList<>(); + for (RouterHandlerFilterErrorScenario routerHandlerFilterErrorScenario : RouterHandlerFilterErrorScenario.values()) { + for (ServerScenario serverScenario : ServerScenario.values()) { + result.add(new Object[]{routerHandlerFilterErrorScenario, serverScenario}); + } + } + return result; + } + + @MethodSource("routerHandlerFilterErrorScenariosDataProvider") + @ParameterizedTest + public void verify_expected_error_returned_if_handler_filter_function_trigger_occurs( + RouterHandlerFilterErrorScenario routerHandlerFilterErrorScenario, ServerScenario serverScenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(serverScenario.serverPort) + .basePath(SAMPLE_FROM_ROUTER_FUNCTION_PATH) + .header(routerHandlerFilterErrorScenario.triggeringHeaderName, "true") + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, routerHandlerFilterErrorScenario.expectedError); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_METHOD_NOT_ALLOWED_returned_if_known_path_is_requested_with_invalid_http_method( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .log().all() + .when() + .delete() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.METHOD_NOT_ALLOWED); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_sample_get_fails_with_NO_ACCEPTABLE_REPRESENTATION_if_passed_invalid_accept_header( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .accept(ContentType.BINARY) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.NO_ACCEPTABLE_REPRESENTATION); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_sample_post_fails_with_UNSUPPORTED_MEDIA_TYPE_if_passed_invalid_content_type( + ServerScenario scenario + ) throws IOException { + SampleModel requestPayload = randomizedSampleModel(); + String requestPayloadAsString = objectMapper.writeValueAsString(requestPayload); + + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .body(requestPayloadAsString) + .contentType(ContentType.TEXT) + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.UNSUPPORTED_MEDIA_TYPE); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_MALFORMED_REQUEST_is_thrown_when_required_query_param_is_missing( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + WITH_REQUIRED_QUERY_PARAM_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived( + response, + new ApiErrorWithMetadata( + SampleCoreApiError.MALFORMED_REQUEST, + Pair.of("missing_param_name", "requiredQueryParamValue"), + Pair.of("missing_param_type", "int"), + Pair.of("required_location", "query_param") + ) + ); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert_type_for_query_param( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + WITH_REQUIRED_QUERY_PARAM_SUBPATH) + .queryParam("requiredQueryParamValue", "not-an-integer") + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, new ApiErrorWithMetadata( + SampleCoreApiError.TYPE_CONVERSION_ERROR, + // Springboot 3.0.x and 3.1.x WebFlux do not include the property name, so we can't check for them here. + MapBuilder.builder("bad_property_value",(Object)"not-an-integer") + .put("required_location","query_param") + .put("required_type", "int") + .build() + )); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_MALFORMED_REQUEST_is_thrown_when_required_header_is_missing( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + WITH_REQUIRED_HEADER_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived( + response, + new ApiErrorWithMetadata( + SampleCoreApiError.MALFORMED_REQUEST, + Pair.of("missing_param_name", "requiredHeaderValue"), + Pair.of("missing_param_type", "int"), + Pair.of("required_location", "header") + ) + ); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert_type_for_header( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + WITH_REQUIRED_HEADER_SUBPATH) + .header("requiredHeaderValue", "not-an-integer") + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, new ApiErrorWithMetadata( + SampleCoreApiError.TYPE_CONVERSION_ERROR, + // Springboot 3.0.x and 3.1.x WebFlux do not include the property name, so we can't check for them here. + MapBuilder.builder("bad_property_value",(Object)"not-an-integer") + .put("required_location","header") + .put("required_type", "int") + .build() + )); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_sample_post_fails_with_MISSING_EXPECTED_CONTENT_if_passed_empty_body( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .contentType(ContentType.JSON) + .body("") + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.MISSING_EXPECTED_CONTENT); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_sample_post_fails_with_MALFORMED_REQUEST_if_passed_junk_json( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .contentType(ContentType.JSON) + .body("{notjson blah") + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.MALFORMED_REQUEST); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_sample_post_fails_with_MALFORMED_REQUEST_if_passed_bad_json_body( + ServerScenario scenario + ) throws IOException { + SampleModel originalValidPayloadObj = randomizedSampleModel(); + String originalValidPayloadAsString = objectMapper.writeValueAsString(originalValidPayloadObj); + @SuppressWarnings("unchecked") + Map badRequestPayloadAsMap = objectMapper.readValue(originalValidPayloadAsString, Map.class); + badRequestPayloadAsMap.put("throw_manual_error", "not-a-boolean"); + String badJsonPayloadAsString = objectMapper.writeValueAsString(badRequestPayloadAsMap); + + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .contentType(ContentType.JSON) + .body(badJsonPayloadAsString) + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.MALFORMED_REQUEST); + } +} diff --git a/testonly/testonly-springboot3_1-webflux/src/test/java/serverconfig/classpathscan/Springboot3_1WebFluxClasspathScanConfig.java b/testonly/testonly-springboot3_1-webflux/src/test/java/serverconfig/classpathscan/Springboot3_1WebFluxClasspathScanConfig.java new file mode 100644 index 0000000..cd9a757 --- /dev/null +++ b/testonly/testonly-springboot3_1-webflux/src/test/java/serverconfig/classpathscan/Springboot3_1WebFluxClasspathScanConfig.java @@ -0,0 +1,64 @@ +package serverconfig.classpathscan; + +import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.server.WebFilter; + +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import testonly.componenttest.spring.reusable.controller.SampleWebFluxController; +import testonly.componenttest.spring.reusable.error.SampleProjectApiErrorsImpl; +import testonly.componenttest.spring.reusable.filter.ExplodingHandlerFilterFunction; +import testonly.componenttest.spring.reusable.filter.ExplodingWebFilter; + +import static org.springframework.web.reactive.function.server.RequestPredicates.GET; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.SAMPLE_FROM_ROUTER_FUNCTION_PATH; + +/** + * Springboot config that uses {@link ComponentScan} to integrate Backstopper via classpath scanning of the + * {@code com.nike.backstopper} package. + * + * @author Nic Munroe + */ +@SpringBootApplication +@ComponentScan(basePackages = { + // Component scan the core Backstopper+Spring WebFlux support. + "com.nike.backstopper", + // Component scan the controller (note this is the reusable WebFlux controller, not the reusable servlet controller). + "testonly.componenttest.spring.reusable.controller" +}) +@SuppressWarnings("unused") +public class Springboot3_1WebFluxClasspathScanConfig { + + @Bean + public ProjectApiErrors getProjectApiErrors() { + return new SampleProjectApiErrorsImpl(); + } + + @Bean + public Validator getJsr303Validator() { + //noinspection resource + return Validation.buildDefaultValidatorFactory().getValidator(); + } + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + public WebFilter explodingWebFilter() { + return new ExplodingWebFilter(); + } + + @Bean + public RouterFunction sampleRouterFunction(SampleWebFluxController sampleController) { + return RouterFunctions + .route(GET(SAMPLE_FROM_ROUTER_FUNCTION_PATH), sampleController::getSampleModelRouterFunction) + .filter(new ExplodingHandlerFilterFunction()); + } +} diff --git a/testonly/testonly-springboot3_1-webflux/src/test/java/serverconfig/directimport/Springboot3_1WebFluxDirectImportConfig.java b/testonly/testonly-springboot3_1-webflux/src/test/java/serverconfig/directimport/Springboot3_1WebFluxDirectImportConfig.java new file mode 100644 index 0000000..aab939c --- /dev/null +++ b/testonly/testonly-springboot3_1-webflux/src/test/java/serverconfig/directimport/Springboot3_1WebFluxDirectImportConfig.java @@ -0,0 +1,65 @@ +package serverconfig.directimport; + +import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; +import com.nike.backstopper.handler.spring.webflux.config.BackstopperSpringWebFluxConfig; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.server.WebFilter; + +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import testonly.componenttest.spring.reusable.controller.SampleWebFluxController; +import testonly.componenttest.spring.reusable.error.SampleProjectApiErrorsImpl; +import testonly.componenttest.spring.reusable.filter.ExplodingHandlerFilterFunction; +import testonly.componenttest.spring.reusable.filter.ExplodingWebFilter; + +import static org.springframework.web.reactive.function.server.RequestPredicates.GET; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.SAMPLE_FROM_ROUTER_FUNCTION_PATH; + +/** + * Springboot config that uses {@link Import} to integrate Backstopper via direct import of + * {@link BackstopperSpringWebFluxConfig}. + * + * @author Nic Munroe + */ +@SpringBootApplication +@Import({ + // Import core Backstopper+Spring WebFlux support. + BackstopperSpringWebFluxConfig.class, + // Import the controller. + SampleWebFluxController.class +}) +@SuppressWarnings("unused") +public class Springboot3_1WebFluxDirectImportConfig { + + @Bean + public ProjectApiErrors getProjectApiErrors() { + return new SampleProjectApiErrorsImpl(); + } + + @Bean + public Validator getJsr303Validator() { + //noinspection resource + return Validation.buildDefaultValidatorFactory().getValidator(); + } + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + public WebFilter explodingWebFilter() { + return new ExplodingWebFilter(); + } + + @Bean + public RouterFunction sampleRouterFunction(SampleWebFluxController sampleController) { + return RouterFunctions + .route(GET(SAMPLE_FROM_ROUTER_FUNCTION_PATH), sampleController::getSampleModelRouterFunction) + .filter(new ExplodingHandlerFilterFunction()); + } +} diff --git a/testonly/testonly-springboot3_1-webflux/src/test/resources/logback.xml b/testonly/testonly-springboot3_1-webflux/src/test/resources/logback.xml new file mode 100644 index 0000000..80adb28 --- /dev/null +++ b/testonly/testonly-springboot3_1-webflux/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd_HH:mm:ss.SSS} [%thread] |-%-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/testonly/testonly-springboot3_1-webmvc/README.md b/testonly/testonly-springboot3_1-webmvc/README.md new file mode 100644 index 0000000..54b924a --- /dev/null +++ b/testonly/testonly-springboot3_1-webmvc/README.md @@ -0,0 +1,21 @@ +# Backstopper - testonly-springboot3_1-webmvc + +Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 17 and greater. + +(NOTE: The [Backstopper 1.x branch](https://github.com/Nike-Inc/backstopper/tree/v1.x) contains a version of +Backstopper for Java 7+, and for the `javax` ecosystem. The current Backstopper supports Java 17+ and the `jakarta` +ecosystem. The Backstopper 1.x releases also contain support for Spring 4 and 5, and Springboot 1 and 2.) + +This submodule contains tests to verify that the +[backstopper-spring-boot3-webmvc](../../backstopper-spring-boot3-webmvc) module's functionality works as expected in +Spring Boot 3.1.x Web MVC (Servlet) environments, for both classpath-scanning and direct-import Backstopper configuration +use cases. + +## More Info + +See the [base project README.md](../../README.md), [User Guide](../../USER_GUIDE.md), and Backstopper repository +source code and javadocs for all further information. + +## License + +Backstopper is released under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/testonly/testonly-springboot3_1-webmvc/build.gradle b/testonly/testonly-springboot3_1-webmvc/build.gradle new file mode 100644 index 0000000..d0cc632 --- /dev/null +++ b/testonly/testonly-springboot3_1-webmvc/build.gradle @@ -0,0 +1,43 @@ +evaluationDependsOn(':') + +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:${springboot3_1Version}") + } +} + +apply plugin: 'org.springframework.boot' +apply plugin: "io.spring.dependency-management" + +test { + useJUnitPlatform() +} + +dependencies { + implementation( + project(":backstopper-spring-boot3-webmvc"), + project(":backstopper-custom-validators"), + "ch.qos.logback:logback-classic", + "org.springframework.boot:spring-boot-dependencies:$springboot3_1Version", + "org.springframework.boot:spring-boot-starter-web", + "org.hibernate.validator:hibernate-validator", + "org.glassfish.expressly:expressly:$glassfishExpresslyVersion", + ) + testImplementation( + project(":backstopper-reusable-tests-junit5"), + project(":testonly:testonly-spring-webmvc-reusable-test-support"), + "org.junit.jupiter:junit-jupiter-api:$junit5Version", + "org.junit.jupiter:junit-jupiter-engine:$junit5Version", + "org.junit.jupiter:junit-jupiter-params:$junit5Version", + "org.mockito:mockito-core:$mockitoVersion", + "org.assertj:assertj-core:$assertJVersion", + "io.rest-assured:rest-assured", + ) +} + +// We're just running tests, not trying to stand up a real Springboot 3 server from gradle. +// Disable the bootJar task so gradle doesn't fall over. +bootJar.enabled = false diff --git a/testonly/testonly-springboot3_1-webmvc/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot3_1WebMvcComponentTest.java b/testonly/testonly-springboot3_1-webmvc/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot3_1WebMvcComponentTest.java new file mode 100644 index 0000000..0405d18 --- /dev/null +++ b/testonly/testonly-springboot3_1-webmvc/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot3_1WebMvcComponentTest.java @@ -0,0 +1,574 @@ +package com.nike.backstopper.testonly; + +import com.nike.backstopper.apierror.ApiError; +import com.nike.backstopper.apierror.ApiErrorWithMetadata; +import com.nike.backstopper.apierror.sample.SampleCoreApiError; +import com.nike.internal.util.MapBuilder; +import com.nike.internal.util.Pair; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ConfigurableApplicationContext; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import io.restassured.http.ContentType; +import io.restassured.response.ExtractableResponse; +import serverconfig.classpathscan.Springboot3_1WebMvcClasspathScanConfig; +import serverconfig.directimport.Springboot3_1WebMvcDirectImportConfig; +import testonly.componenttest.spring.reusable.error.SampleProjectApiError; +import testonly.componenttest.spring.reusable.model.RgbColor; +import testonly.componenttest.spring.reusable.model.SampleModel; + +import static com.nike.internal.util.testing.TestUtils.findFreePort; +import static io.restassured.RestAssured.given; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static testonly.componenttest.spring.reusable.controller.SampleController.CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH; +import static testonly.componenttest.spring.reusable.controller.SampleController.SAMPLE_PATH; +import static testonly.componenttest.spring.reusable.controller.SampleController.TRIGGER_UNHANDLED_ERROR_SUBPATH; +import static testonly.componenttest.spring.reusable.controller.SampleController.WITH_REQUIRED_HEADER_SUBPATH; +import static testonly.componenttest.spring.reusable.controller.SampleController.WITH_REQUIRED_QUERY_PARAM_SUBPATH; +import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.FOO_STRING_CANNOT_BE_BLANK; +import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.INVALID_RANGE_VALUE; +import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.NOT_RGB_COLOR_ENUM; +import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.RGB_COLOR_CANNOT_BE_NULL; +import static testonly.componenttest.spring.reusable.testutil.TestUtils.randomizedSampleModel; +import static testonly.componenttest.spring.reusable.testutil.TestUtils.verifyErrorReceived; + +/** + * Component test to verify that the functionality of {@code backstopper-spring-boot3-webmvc} works as expected in a + * Spring Boot 3.1.x Web MVC environment, for both classpath-scanning and direct-import Backstopper configuration use + * cases. + * + * @author Nic Munroe + */ +@SuppressWarnings({"ClassEscapesDefinedScope", "NewClassNamingConvention"}) +public class BackstopperSpringboot3_1WebMvcComponentTest { + + private static final int CLASSPATH_SCAN_SERVER_PORT = findFreePort(); + private static final int DIRECT_IMPORT_SERVER_PORT = findFreePort(); + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private static ConfigurableApplicationContext classpathScanServerAppContext; + private static ConfigurableApplicationContext directImportServerAppContext; + + @BeforeAll + public static void beforeClass() { + assertThat(CLASSPATH_SCAN_SERVER_PORT).isNotEqualTo(DIRECT_IMPORT_SERVER_PORT); + classpathScanServerAppContext = SpringApplication.run( + Springboot3_1WebMvcClasspathScanConfig.class, "--server.port=" + CLASSPATH_SCAN_SERVER_PORT + ); + directImportServerAppContext = SpringApplication.run( + Springboot3_1WebMvcDirectImportConfig.class, "--server.port=" + DIRECT_IMPORT_SERVER_PORT + ); + } + + @AfterAll + public static void afterClass() { + SpringApplication.exit(classpathScanServerAppContext); + SpringApplication.exit(directImportServerAppContext); + } + + @SuppressWarnings("unused") + private enum ServerScenario { + CLASSPATH_SCAN_SERVER(CLASSPATH_SCAN_SERVER_PORT), + DIRECT_IMPORT_SERVER(DIRECT_IMPORT_SERVER_PORT); + + public final int serverPort; + + ServerScenario(int serverPort) { + this.serverPort = serverPort; + } + } + + // *************** SUCCESSFUL (NON ERROR) CALLS ****************** + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_basic_sample_get(ServerScenario scenario) throws IOException { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + assertThat(response.statusCode()).isEqualTo(200); + SampleModel responseBody = objectMapper.readValue(response.asString(), SampleModel.class); + assertThat(responseBody).isNotNull(); + assertThat(responseBody.foo).isNotEmpty(); + assertThat(responseBody.range_0_to_42).isNotEmpty(); + assertThat(Integer.parseInt(responseBody.range_0_to_42)).isBetween(0, 42); + assertThat(responseBody.rgb_color).isNotEmpty(); + assertThat(RgbColor.toRgbColor(responseBody.rgb_color)).isNotNull(); + assertThat(responseBody.throw_manual_error).isFalse(); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_basic_sample_post(ServerScenario scenario) throws IOException { + SampleModel requestPayload = randomizedSampleModel(); + String requestPayloadAsString = objectMapper.writeValueAsString(requestPayload); + + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .contentType(ContentType.JSON) + .basePath(SAMPLE_PATH) + .body(requestPayloadAsString) + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + assertThat(response.statusCode()).isEqualTo(201); + SampleModel responseBody = objectMapper.readValue(response.asString(), SampleModel.class); + assertThat(responseBody).isNotNull(); + assertThat(responseBody.foo).isEqualTo(requestPayload.foo); + assertThat(responseBody.range_0_to_42).isEqualTo(requestPayload.range_0_to_42); + assertThat(responseBody.rgb_color).isEqualTo(requestPayload.rgb_color); + assertThat(responseBody.throw_manual_error).isEqualTo(requestPayload.throw_manual_error); + } + + // *************** JSR 303 AND ENDPOINT ERRORS ****************** + + @SuppressWarnings("unused") + private enum Jsr303SampleModelValidationScenario { + BLANK_FIELD_VIOLATION( + new SampleModel("", "42", "GREEN", false), + singletonList(FOO_STRING_CANNOT_BE_BLANK) + ), + INVALID_RANGE_VIOLATION( + new SampleModel("bar", "-1", "GREEN", false), + singletonList(INVALID_RANGE_VALUE) + ), + NULL_FIELD_VIOLATION( + new SampleModel("bar", "42", null, false), + singletonList(RGB_COLOR_CANNOT_BE_NULL) + ), + STRING_CONVERTS_TO_CLASSTYPE_VIOLATION( + new SampleModel("bar", "42", "car", false), + singletonList(NOT_RGB_COLOR_ENUM) + ), + MULTIPLE_VIOLATIONS( + new SampleModel(" \n\r\t ", "99", "tree", false), + Arrays.asList(FOO_STRING_CANNOT_BE_BLANK, INVALID_RANGE_VALUE, NOT_RGB_COLOR_ENUM) + ); + + public final SampleModel model; + public final List expectedErrors; + + Jsr303SampleModelValidationScenario( + SampleModel model, List expectedErrors + ) { + this.model = model; + this.expectedErrors = expectedErrors; + } + } + + public static List jsr303ValidationErrorScenariosDataProvider() { + List result = new ArrayList<>(); + for (Jsr303SampleModelValidationScenario violationScenario : Jsr303SampleModelValidationScenario.values()) { + for (ServerScenario serverScenario : ServerScenario.values()) { + result.add(new Object[]{violationScenario, serverScenario}); + } + } + return result; + } + + @MethodSource("jsr303ValidationErrorScenariosDataProvider") + @ParameterizedTest + public void verify_jsr303_validation_errors( + Jsr303SampleModelValidationScenario violationScenario, ServerScenario serverScenario + ) throws JsonProcessingException { + String requestPayloadAsString = objectMapper.writeValueAsString(violationScenario.model); + + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(serverScenario.serverPort) + .contentType(ContentType.JSON) + .basePath(SAMPLE_PATH) + .body(requestPayloadAsString) + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + List expectedErrors = new ArrayList<>(); + for (ApiError expectedApiError : violationScenario.expectedErrors) { + String extraMetadataFieldValue = null; + + if (INVALID_RANGE_VALUE.equals(expectedApiError)) { + extraMetadataFieldValue = "range_0_to_42"; + } + else if (RGB_COLOR_CANNOT_BE_NULL.equals(expectedApiError) || NOT_RGB_COLOR_ENUM.equals(expectedApiError)) { + extraMetadataFieldValue = "rgb_color"; + } + + if (extraMetadataFieldValue != null) { + expectedApiError = new ApiErrorWithMetadata( + expectedApiError, + MapBuilder.builder("field", (Object) extraMetadataFieldValue).build() + ); + } + + expectedErrors.add(expectedApiError); + } + verifyErrorReceived(response, expectedErrors, 400); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_MANUALLY_THROWN_ERROR_is_thrown_when_requested(ServerScenario scenario) throws IOException { + SampleModel requestPayload = new SampleModel("bar", "42", "RED", true); + String requestPayloadAsString = objectMapper.writeValueAsString(requestPayload); + + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .contentType(ContentType.JSON) + .basePath(SAMPLE_PATH) + .body(requestPayloadAsString) + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleProjectApiError.MANUALLY_THROWN_ERROR); + // This code path also should add some custom headers to the response + assertThat(response.headers().getValues("rgbColorValue")).isEqualTo(singletonList(requestPayload.rgb_color)); + assertThat(response.headers().getValues("otherExtraMultivalueHeader")).isEqualTo(Arrays.asList("foo", "bar")); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_SOME_MEANINGFUL_ERROR_NAME_is_thrown_when_correct_endpoint_is_hit(ServerScenario scenario) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleProjectApiError.SOME_MEANINGFUL_ERROR_NAME); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_GENERIC_SERVICE_ERROR_is_thrown_when_correct_endpoint_is_hit(ServerScenario scenario) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + TRIGGER_UNHANDLED_ERROR_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.GENERIC_SERVICE_ERROR); + } + + // *************** FRAMEWORK ERRORS ****************** + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_NOT_FOUND_returned_if_unknown_path_is_requested(ServerScenario scenario) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(UUID.randomUUID().toString()) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.NOT_FOUND); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_ERROR_THROWN_IN_SERVLET_FILTER_OUTSIDE_SPRING_returned_if_servlet_filter_trigger_occurs( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .header("throw-servlet-filter-exception", "true") + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleProjectApiError.ERROR_THROWN_IN_SERVLET_FILTER_OUTSIDE_SPRING); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_METHOD_NOT_ALLOWED_returned_if_known_path_is_requested_with_invalid_http_method( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .log().all() + .when() + .delete() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.METHOD_NOT_ALLOWED); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_sample_get_fails_with_NO_ACCEPTABLE_REPRESENTATION_if_passed_invalid_accept_header( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .accept(ContentType.BINARY) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.NO_ACCEPTABLE_REPRESENTATION); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_sample_post_fails_with_UNSUPPORTED_MEDIA_TYPE_if_passed_invalid_content_type( + ServerScenario scenario + ) throws IOException { + SampleModel requestPayload = randomizedSampleModel(); + String requestPayloadAsString = objectMapper.writeValueAsString(requestPayload); + + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .body(requestPayloadAsString) + .contentType(ContentType.TEXT) + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.UNSUPPORTED_MEDIA_TYPE); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_MALFORMED_REQUEST_is_thrown_when_required_query_param_is_missing( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + WITH_REQUIRED_QUERY_PARAM_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived( + response, + new ApiErrorWithMetadata( + SampleCoreApiError.MALFORMED_REQUEST, + Pair.of("missing_param_name", "requiredQueryParamValue"), + Pair.of("missing_param_type", "int"), + Pair.of("required_location", "query_param") + ) + ); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert_type_for_query_param( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + WITH_REQUIRED_QUERY_PARAM_SUBPATH) + .queryParam("requiredQueryParamValue", "not-an-integer") + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, new ApiErrorWithMetadata( + SampleCoreApiError.TYPE_CONVERSION_ERROR, + MapBuilder.builder("bad_property_name", (Object) "requiredQueryParamValue") + .put("bad_property_value","not-an-integer") + .put("required_location","query_param") + .put("required_type", "int") + .build() + )); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_MALFORMED_REQUEST_is_thrown_when_required_header_is_missing( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + WITH_REQUIRED_HEADER_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived( + response, + new ApiErrorWithMetadata( + SampleCoreApiError.MALFORMED_REQUEST, + Pair.of("missing_param_name", "requiredHeaderValue"), + Pair.of("missing_param_type", "int"), + Pair.of("required_location", "header") + ) + ); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert_type_for_header( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + WITH_REQUIRED_HEADER_SUBPATH) + .header("requiredHeaderValue", "not-an-integer") + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, new ApiErrorWithMetadata( + SampleCoreApiError.TYPE_CONVERSION_ERROR, + MapBuilder.builder("bad_property_name", (Object) "requiredHeaderValue") + .put("bad_property_value","not-an-integer") + .put("required_location","header") + .put("required_type", "int") + .build() + )); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_sample_post_fails_with_MISSING_EXPECTED_CONTENT_if_passed_empty_body( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .contentType(ContentType.JSON) + .body("") + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.MISSING_EXPECTED_CONTENT); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_sample_post_fails_with_MALFORMED_REQUEST_if_passed_bad_json_body( + ServerScenario scenario + ) throws IOException { + SampleModel originalValidPayloadObj = randomizedSampleModel(); + String originalValidPayloadAsString = objectMapper.writeValueAsString(originalValidPayloadObj); + @SuppressWarnings("unchecked") + Map badRequestPayloadAsMap = objectMapper.readValue(originalValidPayloadAsString, Map.class); + badRequestPayloadAsMap.put("throw_manual_error", "not-a-boolean"); + String badJsonPayloadAsString = objectMapper.writeValueAsString(badRequestPayloadAsMap); + + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .contentType(ContentType.JSON) + .body(badJsonPayloadAsString) + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.MALFORMED_REQUEST); + } +} diff --git a/testonly/testonly-springboot3_1-webmvc/src/test/java/serverconfig/classpathscan/Springboot3_1WebMvcClasspathScanConfig.java b/testonly/testonly-springboot3_1-webmvc/src/test/java/serverconfig/classpathscan/Springboot3_1WebMvcClasspathScanConfig.java new file mode 100644 index 0000000..cc36dd9 --- /dev/null +++ b/testonly/testonly-springboot3_1-webmvc/src/test/java/serverconfig/classpathscan/Springboot3_1WebMvcClasspathScanConfig.java @@ -0,0 +1,49 @@ +package serverconfig.classpathscan; + +import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.core.Ordered; + +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import testonly.componenttest.spring.reusable.error.SampleProjectApiErrorsImpl; +import testonly.componenttest.spring.reusable.testutil.ExplodingServletFilter; + +/** + * Springboot config that uses {@link ComponentScan} to integrate Backstopper via classpath scanning of the + * {@code com.nike.backstopper} package. + * + * @author Nic Munroe + */ +@SpringBootApplication +@ComponentScan(basePackages = { + // Component scan the core Backstopper+Springboot1 support. + "com.nike.backstopper", + // Component scan the controller. + "testonly.componenttest.spring.reusable.controller" +}) +@SuppressWarnings("unused") +public class Springboot3_1WebMvcClasspathScanConfig { + + @Bean + public ProjectApiErrors getProjectApiErrors() { + return new SampleProjectApiErrorsImpl(); + } + + @Bean + public Validator getJsr303Validator() { + //noinspection resource + return Validation.buildDefaultValidatorFactory().getValidator(); + } + + @Bean + public FilterRegistrationBean explodingServletFilter() { + FilterRegistrationBean frb = new FilterRegistrationBean<>(new ExplodingServletFilter()); + frb.setOrder(Ordered.HIGHEST_PRECEDENCE); + return frb; + } +} diff --git a/testonly/testonly-springboot3_1-webmvc/src/test/java/serverconfig/directimport/Springboot3_1WebMvcDirectImportConfig.java b/testonly/testonly-springboot3_1-webmvc/src/test/java/serverconfig/directimport/Springboot3_1WebMvcDirectImportConfig.java new file mode 100644 index 0000000..b49bbac --- /dev/null +++ b/testonly/testonly-springboot3_1-webmvc/src/test/java/serverconfig/directimport/Springboot3_1WebMvcDirectImportConfig.java @@ -0,0 +1,51 @@ +package serverconfig.directimport; + +import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; +import com.nike.backstopper.handler.springboot.config.BackstopperSpringboot3WebMvcConfig; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.core.Ordered; + +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import testonly.componenttest.spring.reusable.controller.SampleController; +import testonly.componenttest.spring.reusable.error.SampleProjectApiErrorsImpl; +import testonly.componenttest.spring.reusable.testutil.ExplodingServletFilter; + +/** + * Springboot config that uses {@link Import} to integrate Backstopper via direct import of + * {@link BackstopperSpringboot3WebMvcConfig}. + * + * @author Nic Munroe + */ +@SpringBootApplication +@Import({ + // Import core Backstopper+Springboot1 support. + BackstopperSpringboot3WebMvcConfig.class, + // Import the controller. + SampleController.class +}) +@SuppressWarnings("unused") +public class Springboot3_1WebMvcDirectImportConfig { + + @Bean + public ProjectApiErrors getProjectApiErrors() { + return new SampleProjectApiErrorsImpl(); + } + + @Bean + public Validator getJsr303Validator() { + //noinspection resource + return Validation.buildDefaultValidatorFactory().getValidator(); + } + + @Bean + public FilterRegistrationBean explodingServletFilter() { + FilterRegistrationBean frb = new FilterRegistrationBean<>(new ExplodingServletFilter()); + frb.setOrder(Ordered.HIGHEST_PRECEDENCE); + return frb; + } +} diff --git a/testonly/testonly-springboot3_1-webmvc/src/test/resources/logback.xml b/testonly/testonly-springboot3_1-webmvc/src/test/resources/logback.xml new file mode 100644 index 0000000..80adb28 --- /dev/null +++ b/testonly/testonly-springboot3_1-webmvc/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd_HH:mm:ss.SSS} [%thread] |-%-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file From 7d18bd952f43f761098ecd70e86652eac4c8688f Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Wed, 11 Sep 2024 18:24:02 -0700 Subject: [PATCH 30/42] Add testonly modules for springboot 3.2.x for webmvc and webflux --- settings.gradle | 2 + .../testonly-springboot3_2-webflux/README.md | 21 + .../build.gradle | 44 + ...pperSpringboot3_2WebFluxComponentTest.java | 762 ++++++++++++++++++ ...ringboot3_2WebFluxClasspathScanConfig.java | 64 ++ ...pringboot3_2WebFluxDirectImportConfig.java | 65 ++ .../src/test/resources/logback.xml | 11 + .../testonly-springboot3_2-webmvc/README.md | 21 + .../build.gradle | 43 + ...opperSpringboot3_2WebMvcComponentTest.java | 574 +++++++++++++ ...pringboot3_2WebMvcClasspathScanConfig.java | 49 ++ ...Springboot3_2WebMvcDirectImportConfig.java | 51 ++ .../src/test/resources/logback.xml | 11 + 13 files changed, 1718 insertions(+) create mode 100644 testonly/testonly-springboot3_2-webflux/README.md create mode 100644 testonly/testonly-springboot3_2-webflux/build.gradle create mode 100644 testonly/testonly-springboot3_2-webflux/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot3_2WebFluxComponentTest.java create mode 100644 testonly/testonly-springboot3_2-webflux/src/test/java/serverconfig/classpathscan/Springboot3_2WebFluxClasspathScanConfig.java create mode 100644 testonly/testonly-springboot3_2-webflux/src/test/java/serverconfig/directimport/Springboot3_2WebFluxDirectImportConfig.java create mode 100644 testonly/testonly-springboot3_2-webflux/src/test/resources/logback.xml create mode 100644 testonly/testonly-springboot3_2-webmvc/README.md create mode 100644 testonly/testonly-springboot3_2-webmvc/build.gradle create mode 100644 testonly/testonly-springboot3_2-webmvc/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot3_2WebMvcComponentTest.java create mode 100644 testonly/testonly-springboot3_2-webmvc/src/test/java/serverconfig/classpathscan/Springboot3_2WebMvcClasspathScanConfig.java create mode 100644 testonly/testonly-springboot3_2-webmvc/src/test/java/serverconfig/directimport/Springboot3_2WebMvcDirectImportConfig.java create mode 100644 testonly/testonly-springboot3_2-webmvc/src/test/resources/logback.xml diff --git a/settings.gradle b/settings.gradle index 84dc0b0..8d1f489 100644 --- a/settings.gradle +++ b/settings.gradle @@ -20,6 +20,8 @@ include "nike-internal-util", "testonly:testonly-springboot3_0-webflux", "testonly:testonly-springboot3_1-webmvc", "testonly:testonly-springboot3_1-webflux", + "testonly:testonly-springboot3_2-webmvc", + "testonly:testonly-springboot3_2-webflux", "testonly:testonly-springboot3_3-webmvc", "testonly:testonly-springboot3_3-webflux", // Sample modules (not published) diff --git a/testonly/testonly-springboot3_2-webflux/README.md b/testonly/testonly-springboot3_2-webflux/README.md new file mode 100644 index 0000000..02f82d3 --- /dev/null +++ b/testonly/testonly-springboot3_2-webflux/README.md @@ -0,0 +1,21 @@ +# Backstopper - testonly-springboot3_2-webflux + +Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 17 and greater. + +(NOTE: The [Backstopper 1.x branch](https://github.com/Nike-Inc/backstopper/tree/v1.x) contains a version of +Backstopper for Java 7+, and for the `javax` ecosystem. The current Backstopper supports Java 17+ and the `jakarta` +ecosystem. The Backstopper 1.x releases also contain support for Spring 4 and 5, and Springboot 1 and 2.) + +This submodule contains tests to verify that the +[backstopper-spring-web-flux](../../backstopper-spring-web-flux) module's functionality works as expected in +Spring Boot 3.2.x WebFlux (Netty) environments, for both classpath-scanning and direct-import Backstopper configuration +use cases. + +## More Info + +See the [base project README.md](../../README.md), [User Guide](../../USER_GUIDE.md), and Backstopper repository +source code and javadocs for all further information. + +## License + +Backstopper is released under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/testonly/testonly-springboot3_2-webflux/build.gradle b/testonly/testonly-springboot3_2-webflux/build.gradle new file mode 100644 index 0000000..2346d2c --- /dev/null +++ b/testonly/testonly-springboot3_2-webflux/build.gradle @@ -0,0 +1,44 @@ +evaluationDependsOn(':') + +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:${springboot3_2Version}") + } +} + +apply plugin: 'org.springframework.boot' +apply plugin: "io.spring.dependency-management" + +test { + useJUnitPlatform() +} + +dependencies { + implementation( + project(":backstopper-spring-web-flux"), + project(":backstopper-custom-validators"), + "ch.qos.logback:logback-classic", + "org.springframework.boot:spring-boot-dependencies:$springboot3_2Version", + "org.springframework.boot:spring-boot-starter-webflux", + "org.hibernate.validator:hibernate-validator", + "org.glassfish.expressly:expressly:$glassfishExpresslyVersion", + ) + testImplementation( + project(":backstopper-reusable-tests-junit5"), + project(":testonly:testonly-spring-webflux-reusable-test-support"), + "org.jetbrains:annotations:$jetbrainsAnnotationsVersion", + "org.junit.jupiter:junit-jupiter-api:$junit5Version", + "org.junit.jupiter:junit-jupiter-engine:$junit5Version", + "org.junit.jupiter:junit-jupiter-params:$junit5Version", + "org.mockito:mockito-core:$mockitoVersion", + "org.assertj:assertj-core:$assertJVersion", + "io.rest-assured:rest-assured", + ) +} + +// We're just running tests, not trying to stand up a real Springboot 2 server from gradle. +// Disable the bootJar task so gradle doesn't fall over. +bootJar.enabled = false diff --git a/testonly/testonly-springboot3_2-webflux/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot3_2WebFluxComponentTest.java b/testonly/testonly-springboot3_2-webflux/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot3_2WebFluxComponentTest.java new file mode 100644 index 0000000..5f2ba3e --- /dev/null +++ b/testonly/testonly-springboot3_2-webflux/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot3_2WebFluxComponentTest.java @@ -0,0 +1,762 @@ +package com.nike.backstopper.testonly; + +import com.nike.backstopper.apierror.ApiError; +import com.nike.backstopper.apierror.ApiErrorWithMetadata; +import com.nike.backstopper.apierror.sample.SampleCoreApiError; +import com.nike.internal.util.MapBuilder; +import com.nike.internal.util.Pair; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ConfigurableApplicationContext; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import io.restassured.http.ContentType; +import io.restassured.response.ExtractableResponse; +import serverconfig.classpathscan.Springboot3_2WebFluxClasspathScanConfig; +import serverconfig.directimport.Springboot3_2WebFluxDirectImportConfig; +import testonly.componenttest.spring.reusable.error.SampleProjectApiError; +import testonly.componenttest.spring.reusable.model.RgbColor; +import testonly.componenttest.spring.reusable.model.SampleModel; + +import static com.nike.internal.util.testing.TestUtils.findFreePort; +import static io.restassured.RestAssured.given; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.FLUX_ERROR_SUBPATH; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.MONO_ERROR_SUBPATH; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.SAMPLE_FLUX_SUBPATH; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.SAMPLE_FROM_ROUTER_FUNCTION_PATH; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.SAMPLE_PATH; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.TRIGGER_UNHANDLED_ERROR_SUBPATH; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.WITH_REQUIRED_HEADER_SUBPATH; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.WITH_REQUIRED_QUERY_PARAM_SUBPATH; +import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.FOO_STRING_CANNOT_BE_BLANK; +import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.INVALID_RANGE_VALUE; +import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.NOT_RGB_COLOR_ENUM; +import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.RGB_COLOR_CANNOT_BE_NULL; +import static testonly.componenttest.spring.reusable.testutil.TestUtils.randomizedSampleModel; +import static testonly.componenttest.spring.reusable.testutil.TestUtils.verifyErrorReceived; + +/** + * Component test to verify that the functionality of {@code backstopper-spring-web-flux} works as expected in a + * Spring Boot 3.2.x WebFlux environment, for both classpath-scanning and direct-import Backstopper configuration use + * cases. + * + * @author Nic Munroe + */ +@SuppressWarnings({"NewClassNamingConvention", "ClassEscapesDefinedScope"}) +public class BackstopperSpringboot3_2WebFluxComponentTest { + + private static final int CLASSPATH_SCAN_SERVER_PORT = findFreePort(); + private static final int DIRECT_IMPORT_SERVER_PORT = findFreePort(); + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private static ConfigurableApplicationContext classpathScanServerAppContext; + private static ConfigurableApplicationContext directImportServerAppContext; + + @BeforeAll + public static void beforeClass() { + assertThat(CLASSPATH_SCAN_SERVER_PORT).isNotEqualTo(DIRECT_IMPORT_SERVER_PORT); + classpathScanServerAppContext = SpringApplication.run( + Springboot3_2WebFluxClasspathScanConfig.class, "--server.port=" + CLASSPATH_SCAN_SERVER_PORT + ); + directImportServerAppContext = SpringApplication.run( + Springboot3_2WebFluxDirectImportConfig.class, "--server.port=" + DIRECT_IMPORT_SERVER_PORT + ); + } + + @AfterAll + public static void afterClass() { + SpringApplication.exit(classpathScanServerAppContext); + SpringApplication.exit(directImportServerAppContext); + } + + @SuppressWarnings("unused") + private enum ServerScenario { + CLASSPATH_SCAN_SERVER(CLASSPATH_SCAN_SERVER_PORT), + DIRECT_IMPORT_SERVER(DIRECT_IMPORT_SERVER_PORT); + + public final int serverPort; + + ServerScenario(int serverPort) { + this.serverPort = serverPort; + } + } + + // *************** SUCCESSFUL (NON ERROR) CALLS ****************** + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_basic_sample_get(ServerScenario scenario) throws IOException { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + assertThat(response.statusCode()).isEqualTo(200); + SampleModel responseBody = objectMapper.readValue(response.asString(), SampleModel.class); + verifyNewSampleModel(responseBody); + } + + private void verifyNewSampleModel(SampleModel sampleModel) { + assertThat(sampleModel).isNotNull(); + assertThat(sampleModel.foo).isNotEmpty(); + assertThat(sampleModel.range_0_to_42).isNotEmpty(); + assertThat(Integer.parseInt(sampleModel.range_0_to_42)).isBetween(0, 42); + assertThat(sampleModel.rgb_color).isNotEmpty(); + assertThat(RgbColor.toRgbColor(sampleModel.rgb_color)).isNotNull(); + assertThat(sampleModel.throw_manual_error).isFalse(); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_basic_sample_post(ServerScenario scenario) throws IOException { + SampleModel requestPayload = randomizedSampleModel(); + String requestPayloadAsString = objectMapper.writeValueAsString(requestPayload); + + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .contentType(ContentType.JSON) + .basePath(SAMPLE_PATH) + .body(requestPayloadAsString) + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + assertThat(response.statusCode()).isEqualTo(201); + SampleModel responseBody = objectMapper.readValue(response.asString(), SampleModel.class); + assertThat(responseBody).isNotNull(); + assertThat(responseBody.foo).isEqualTo(requestPayload.foo); + assertThat(responseBody.range_0_to_42).isEqualTo(requestPayload.range_0_to_42); + assertThat(responseBody.rgb_color).isEqualTo(requestPayload.rgb_color); + assertThat(responseBody.throw_manual_error).isEqualTo(requestPayload.throw_manual_error); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_router_function_sample_get(ServerScenario scenario) throws IOException { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_FROM_ROUTER_FUNCTION_PATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + assertThat(response.statusCode()).isEqualTo(200); + SampleModel responseBody = objectMapper.readValue(response.asString(), SampleModel.class); + verifyNewSampleModel(responseBody); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_flux_sample_get(ServerScenario scenario) throws IOException { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + SAMPLE_FLUX_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + assertThat(response.statusCode()).isEqualTo(200); + List responseBody = objectMapper.readValue( + response.asString(), new TypeReference<>() {} + ); + + assertThat(responseBody).hasSizeGreaterThan(1); + responseBody.forEach(this::verifyNewSampleModel); + } + + // *************** JSR 303 AND ENDPOINT ERRORS ****************** + + @SuppressWarnings("unused") + private enum Jsr303SampleModelValidationScenario { + BLANK_FIELD_VIOLATION( + new SampleModel("", "42", "GREEN", false), + singletonList(FOO_STRING_CANNOT_BE_BLANK) + ), + INVALID_RANGE_VIOLATION( + new SampleModel("bar", "-1", "GREEN", false), + singletonList(INVALID_RANGE_VALUE) + ), + NULL_FIELD_VIOLATION( + new SampleModel("bar", "42", null, false), + singletonList(RGB_COLOR_CANNOT_BE_NULL) + ), + STRING_CONVERTS_TO_CLASSTYPE_VIOLATION( + new SampleModel("bar", "42", "car", false), + singletonList(NOT_RGB_COLOR_ENUM) + ), + MULTIPLE_VIOLATIONS( + new SampleModel(" \n\r\t ", "99", "tree", false), + Arrays.asList(FOO_STRING_CANNOT_BE_BLANK, INVALID_RANGE_VALUE, NOT_RGB_COLOR_ENUM) + ); + + public final SampleModel model; + public final List expectedErrors; + + Jsr303SampleModelValidationScenario( + SampleModel model, List expectedErrors + ) { + this.model = model; + this.expectedErrors = expectedErrors; + } + } + + public static List jsr303ValidationErrorScenariosDataProvider() { + List result = new ArrayList<>(); + for (Jsr303SampleModelValidationScenario violationScenario : Jsr303SampleModelValidationScenario.values()) { + for (ServerScenario serverScenario : ServerScenario.values()) { + result.add(new Object[]{violationScenario, serverScenario}); + } + } + return result; + } + + @MethodSource("jsr303ValidationErrorScenariosDataProvider") + @ParameterizedTest + public void verify_jsr303_validation_errors( + Jsr303SampleModelValidationScenario violationScenario, ServerScenario serverScenario + ) throws JsonProcessingException { + String requestPayloadAsString = objectMapper.writeValueAsString(violationScenario.model); + + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(serverScenario.serverPort) + .contentType(ContentType.JSON) + .basePath(SAMPLE_PATH) + .body(requestPayloadAsString) + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + List expectedErrors = new ArrayList<>(); + for (ApiError expectedApiError : violationScenario.expectedErrors) { + String extraMetadataFieldValue = null; + + if (INVALID_RANGE_VALUE.equals(expectedApiError)) { + extraMetadataFieldValue = "range_0_to_42"; + } + else if (RGB_COLOR_CANNOT_BE_NULL.equals(expectedApiError) || NOT_RGB_COLOR_ENUM.equals(expectedApiError)) { + extraMetadataFieldValue = "rgb_color"; + } + + if (extraMetadataFieldValue != null) { + expectedApiError = new ApiErrorWithMetadata( + expectedApiError, + MapBuilder.builder("field", (Object) extraMetadataFieldValue).build() + ); + } + + expectedErrors.add(expectedApiError); + } + verifyErrorReceived(response, expectedErrors, 400); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_MANUALLY_THROWN_ERROR_is_thrown_when_requested(ServerScenario scenario) throws IOException { + SampleModel requestPayload = new SampleModel("bar", "42", "RED", true); + String requestPayloadAsString = objectMapper.writeValueAsString(requestPayload); + + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .contentType(ContentType.JSON) + .basePath(SAMPLE_PATH) + .body(requestPayloadAsString) + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleProjectApiError.MANUALLY_THROWN_ERROR); + // This code path also should add some custom headers to the response + assertThat(response.headers().getValues("rgbColorValue")).isEqualTo(singletonList(requestPayload.rgb_color)); + assertThat(response.headers().getValues("otherExtraMultivalueHeader")).isEqualTo(Arrays.asList("foo", "bar")); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_SOME_MEANINGFUL_ERROR_NAME_is_thrown_when_correct_endpoint_is_hit(ServerScenario scenario) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleProjectApiError.SOME_MEANINGFUL_ERROR_NAME); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_GENERIC_SERVICE_ERROR_is_thrown_when_correct_endpoint_is_hit(ServerScenario scenario) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + TRIGGER_UNHANDLED_ERROR_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.GENERIC_SERVICE_ERROR); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_WEBFLUX_MONO_ERROR_is_thrown_when_correct_endpoint_is_hit(ServerScenario scenario) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + MONO_ERROR_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleProjectApiError.WEBFLUX_MONO_ERROR); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_WEBFLUX_FLUX_ERROR_is_thrown_when_correct_endpoint_is_hit(ServerScenario scenario) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + FLUX_ERROR_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleProjectApiError.WEBFLUX_FLUX_ERROR); + } + + // *************** FRAMEWORK/FILTER ERRORS ****************** + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_NOT_FOUND_returned_if_unknown_path_is_requested(ServerScenario scenario) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(UUID.randomUUID().toString()) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.NOT_FOUND); + } + + private enum WebFilterErrorScenario { + EXCEPTION_THROWN_IN_WEB_FILTER( + "throw-web-filter-exception", SampleProjectApiError.ERROR_THROWN_IN_WEB_FILTER + ), + EXCEPTION_RETURNED_IN_WEB_FILTER( + "return-exception-in-web-filter-mono", SampleProjectApiError.ERROR_RETURNED_IN_WEB_FILTER_MONO + ); + + public final String triggeringHeaderName; + public final ApiError expectedError; + + WebFilterErrorScenario(String triggeringHeaderName, ApiError expectedError) { + this.triggeringHeaderName = triggeringHeaderName; + this.expectedError = expectedError; + } + } + + public static List webFilterErrorScenariosDataProvider() { + List result = new ArrayList<>(); + for (WebFilterErrorScenario webFilterErrorScenario : WebFilterErrorScenario.values()) { + for (ServerScenario serverScenario : ServerScenario.values()) { + result.add(new Object[]{webFilterErrorScenario, serverScenario}); + } + } + return result; + } + + @MethodSource("webFilterErrorScenariosDataProvider") + @ParameterizedTest + public void verify_expected_error_returned_if_web_filter_trigger_occurs( + WebFilterErrorScenario webFilterErrorScenario, ServerScenario serverScenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(serverScenario.serverPort) + .basePath("/doesnotmatter") + .header(webFilterErrorScenario.triggeringHeaderName, "true") + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, webFilterErrorScenario.expectedError); + } + + private enum RouterHandlerFilterErrorScenario { + EXCEPTION_THROWN_IN_ROUTER_HANDLER_FILTER( + "throw-handler-filter-function-exception", + SampleProjectApiError.ERROR_THROWN_IN_HANDLER_FILTER_FUNCTION + ), + EXCEPTION_RETURNED_IN_ROUTER_HANDLER_FILTER( + "return-exception-in-handler-filter-function-mono", + SampleProjectApiError.ERROR_RETURNED_IN_HANDLER_FILTER_FUNCTION_MONO + ); + + public final String triggeringHeaderName; + public final ApiError expectedError; + + RouterHandlerFilterErrorScenario(String triggeringHeaderName, ApiError expectedError) { + this.triggeringHeaderName = triggeringHeaderName; + this.expectedError = expectedError; + } + } + + public static List routerHandlerFilterErrorScenariosDataProvider() { + List result = new ArrayList<>(); + for (RouterHandlerFilterErrorScenario routerHandlerFilterErrorScenario : RouterHandlerFilterErrorScenario.values()) { + for (ServerScenario serverScenario : ServerScenario.values()) { + result.add(new Object[]{routerHandlerFilterErrorScenario, serverScenario}); + } + } + return result; + } + + @MethodSource("routerHandlerFilterErrorScenariosDataProvider") + @ParameterizedTest + public void verify_expected_error_returned_if_handler_filter_function_trigger_occurs( + RouterHandlerFilterErrorScenario routerHandlerFilterErrorScenario, ServerScenario serverScenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(serverScenario.serverPort) + .basePath(SAMPLE_FROM_ROUTER_FUNCTION_PATH) + .header(routerHandlerFilterErrorScenario.triggeringHeaderName, "true") + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, routerHandlerFilterErrorScenario.expectedError); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_METHOD_NOT_ALLOWED_returned_if_known_path_is_requested_with_invalid_http_method( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .log().all() + .when() + .delete() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.METHOD_NOT_ALLOWED); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_sample_get_fails_with_NO_ACCEPTABLE_REPRESENTATION_if_passed_invalid_accept_header( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .accept(ContentType.BINARY) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.NO_ACCEPTABLE_REPRESENTATION); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_sample_post_fails_with_UNSUPPORTED_MEDIA_TYPE_if_passed_invalid_content_type( + ServerScenario scenario + ) throws IOException { + SampleModel requestPayload = randomizedSampleModel(); + String requestPayloadAsString = objectMapper.writeValueAsString(requestPayload); + + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .body(requestPayloadAsString) + .contentType(ContentType.TEXT) + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.UNSUPPORTED_MEDIA_TYPE); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_MALFORMED_REQUEST_is_thrown_when_required_query_param_is_missing( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + WITH_REQUIRED_QUERY_PARAM_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived( + response, + new ApiErrorWithMetadata( + SampleCoreApiError.MALFORMED_REQUEST, + Pair.of("missing_param_name", "requiredQueryParamValue"), + Pair.of("missing_param_type", "int"), + Pair.of("required_location", "query_param") + ) + ); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert_type_for_query_param( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + WITH_REQUIRED_QUERY_PARAM_SUBPATH) + .queryParam("requiredQueryParamValue", "not-an-integer") + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, new ApiErrorWithMetadata( + SampleCoreApiError.TYPE_CONVERSION_ERROR, + MapBuilder.builder("bad_property_name", (Object) "requiredQueryParamValue") + .put("bad_property_value","not-an-integer") + .put("required_location","query_param") + .put("required_type", "int") + .build() + )); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_MALFORMED_REQUEST_is_thrown_when_required_header_is_missing( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + WITH_REQUIRED_HEADER_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived( + response, + new ApiErrorWithMetadata( + SampleCoreApiError.MALFORMED_REQUEST, + Pair.of("missing_param_name", "requiredHeaderValue"), + Pair.of("missing_param_type", "int"), + Pair.of("required_location", "header") + ) + ); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert_type_for_header( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + WITH_REQUIRED_HEADER_SUBPATH) + .header("requiredHeaderValue", "not-an-integer") + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, new ApiErrorWithMetadata( + SampleCoreApiError.TYPE_CONVERSION_ERROR, + MapBuilder.builder("bad_property_name", (Object) "requiredHeaderValue") + .put("bad_property_value","not-an-integer") + .put("required_location","header") + .put("required_type", "int") + .build() + )); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_sample_post_fails_with_MISSING_EXPECTED_CONTENT_if_passed_empty_body( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .contentType(ContentType.JSON) + .body("") + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.MISSING_EXPECTED_CONTENT); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_sample_post_fails_with_MALFORMED_REQUEST_if_passed_junk_json( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .contentType(ContentType.JSON) + .body("{notjson blah") + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.MALFORMED_REQUEST); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_sample_post_fails_with_MALFORMED_REQUEST_if_passed_bad_json_body( + ServerScenario scenario + ) throws IOException { + SampleModel originalValidPayloadObj = randomizedSampleModel(); + String originalValidPayloadAsString = objectMapper.writeValueAsString(originalValidPayloadObj); + @SuppressWarnings("unchecked") + Map badRequestPayloadAsMap = objectMapper.readValue(originalValidPayloadAsString, Map.class); + badRequestPayloadAsMap.put("throw_manual_error", "not-a-boolean"); + String badJsonPayloadAsString = objectMapper.writeValueAsString(badRequestPayloadAsMap); + + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .contentType(ContentType.JSON) + .body(badJsonPayloadAsString) + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.MALFORMED_REQUEST); + } +} diff --git a/testonly/testonly-springboot3_2-webflux/src/test/java/serverconfig/classpathscan/Springboot3_2WebFluxClasspathScanConfig.java b/testonly/testonly-springboot3_2-webflux/src/test/java/serverconfig/classpathscan/Springboot3_2WebFluxClasspathScanConfig.java new file mode 100644 index 0000000..2f9aac8 --- /dev/null +++ b/testonly/testonly-springboot3_2-webflux/src/test/java/serverconfig/classpathscan/Springboot3_2WebFluxClasspathScanConfig.java @@ -0,0 +1,64 @@ +package serverconfig.classpathscan; + +import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.server.WebFilter; + +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import testonly.componenttest.spring.reusable.controller.SampleWebFluxController; +import testonly.componenttest.spring.reusable.error.SampleProjectApiErrorsImpl; +import testonly.componenttest.spring.reusable.filter.ExplodingHandlerFilterFunction; +import testonly.componenttest.spring.reusable.filter.ExplodingWebFilter; + +import static org.springframework.web.reactive.function.server.RequestPredicates.GET; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.SAMPLE_FROM_ROUTER_FUNCTION_PATH; + +/** + * Springboot config that uses {@link ComponentScan} to integrate Backstopper via classpath scanning of the + * {@code com.nike.backstopper} package. + * + * @author Nic Munroe + */ +@SpringBootApplication +@ComponentScan(basePackages = { + // Component scan the core Backstopper+Spring WebFlux support. + "com.nike.backstopper", + // Component scan the controller (note this is the reusable WebFlux controller, not the reusable servlet controller). + "testonly.componenttest.spring.reusable.controller" +}) +@SuppressWarnings("unused") +public class Springboot3_2WebFluxClasspathScanConfig { + + @Bean + public ProjectApiErrors getProjectApiErrors() { + return new SampleProjectApiErrorsImpl(); + } + + @Bean + public Validator getJsr303Validator() { + //noinspection resource + return Validation.buildDefaultValidatorFactory().getValidator(); + } + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + public WebFilter explodingWebFilter() { + return new ExplodingWebFilter(); + } + + @Bean + public RouterFunction sampleRouterFunction(SampleWebFluxController sampleController) { + return RouterFunctions + .route(GET(SAMPLE_FROM_ROUTER_FUNCTION_PATH), sampleController::getSampleModelRouterFunction) + .filter(new ExplodingHandlerFilterFunction()); + } +} diff --git a/testonly/testonly-springboot3_2-webflux/src/test/java/serverconfig/directimport/Springboot3_2WebFluxDirectImportConfig.java b/testonly/testonly-springboot3_2-webflux/src/test/java/serverconfig/directimport/Springboot3_2WebFluxDirectImportConfig.java new file mode 100644 index 0000000..6e25fc9 --- /dev/null +++ b/testonly/testonly-springboot3_2-webflux/src/test/java/serverconfig/directimport/Springboot3_2WebFluxDirectImportConfig.java @@ -0,0 +1,65 @@ +package serverconfig.directimport; + +import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; +import com.nike.backstopper.handler.spring.webflux.config.BackstopperSpringWebFluxConfig; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.RouterFunctions; +import org.springframework.web.reactive.function.server.ServerResponse; +import org.springframework.web.server.WebFilter; + +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import testonly.componenttest.spring.reusable.controller.SampleWebFluxController; +import testonly.componenttest.spring.reusable.error.SampleProjectApiErrorsImpl; +import testonly.componenttest.spring.reusable.filter.ExplodingHandlerFilterFunction; +import testonly.componenttest.spring.reusable.filter.ExplodingWebFilter; + +import static org.springframework.web.reactive.function.server.RequestPredicates.GET; +import static testonly.componenttest.spring.reusable.controller.SampleWebFluxController.SAMPLE_FROM_ROUTER_FUNCTION_PATH; + +/** + * Springboot config that uses {@link Import} to integrate Backstopper via direct import of + * {@link BackstopperSpringWebFluxConfig}. + * + * @author Nic Munroe + */ +@SpringBootApplication +@Import({ + // Import core Backstopper+Spring WebFlux support. + BackstopperSpringWebFluxConfig.class, + // Import the controller. + SampleWebFluxController.class +}) +@SuppressWarnings("unused") +public class Springboot3_2WebFluxDirectImportConfig { + + @Bean + public ProjectApiErrors getProjectApiErrors() { + return new SampleProjectApiErrorsImpl(); + } + + @Bean + public Validator getJsr303Validator() { + //noinspection resource + return Validation.buildDefaultValidatorFactory().getValidator(); + } + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + public WebFilter explodingWebFilter() { + return new ExplodingWebFilter(); + } + + @Bean + public RouterFunction sampleRouterFunction(SampleWebFluxController sampleController) { + return RouterFunctions + .route(GET(SAMPLE_FROM_ROUTER_FUNCTION_PATH), sampleController::getSampleModelRouterFunction) + .filter(new ExplodingHandlerFilterFunction()); + } +} diff --git a/testonly/testonly-springboot3_2-webflux/src/test/resources/logback.xml b/testonly/testonly-springboot3_2-webflux/src/test/resources/logback.xml new file mode 100644 index 0000000..80adb28 --- /dev/null +++ b/testonly/testonly-springboot3_2-webflux/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd_HH:mm:ss.SSS} [%thread] |-%-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/testonly/testonly-springboot3_2-webmvc/README.md b/testonly/testonly-springboot3_2-webmvc/README.md new file mode 100644 index 0000000..db29465 --- /dev/null +++ b/testonly/testonly-springboot3_2-webmvc/README.md @@ -0,0 +1,21 @@ +# Backstopper - testonly-springboot3_2-webmvc + +Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 17 and greater. + +(NOTE: The [Backstopper 1.x branch](https://github.com/Nike-Inc/backstopper/tree/v1.x) contains a version of +Backstopper for Java 7+, and for the `javax` ecosystem. The current Backstopper supports Java 17+ and the `jakarta` +ecosystem. The Backstopper 1.x releases also contain support for Spring 4 and 5, and Springboot 1 and 2.) + +This submodule contains tests to verify that the +[backstopper-spring-boot3-webmvc](../../backstopper-spring-boot3-webmvc) module's functionality works as expected in +Spring Boot 3.2.x Web MVC (Servlet) environments, for both classpath-scanning and direct-import Backstopper configuration +use cases. + +## More Info + +See the [base project README.md](../../README.md), [User Guide](../../USER_GUIDE.md), and Backstopper repository +source code and javadocs for all further information. + +## License + +Backstopper is released under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/testonly/testonly-springboot3_2-webmvc/build.gradle b/testonly/testonly-springboot3_2-webmvc/build.gradle new file mode 100644 index 0000000..306e0b1 --- /dev/null +++ b/testonly/testonly-springboot3_2-webmvc/build.gradle @@ -0,0 +1,43 @@ +evaluationDependsOn(':') + +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:${springboot3_2Version}") + } +} + +apply plugin: 'org.springframework.boot' +apply plugin: "io.spring.dependency-management" + +test { + useJUnitPlatform() +} + +dependencies { + implementation( + project(":backstopper-spring-boot3-webmvc"), + project(":backstopper-custom-validators"), + "ch.qos.logback:logback-classic", + "org.springframework.boot:spring-boot-dependencies:$springboot3_2Version", + "org.springframework.boot:spring-boot-starter-web", + "org.hibernate.validator:hibernate-validator", + "org.glassfish.expressly:expressly:$glassfishExpresslyVersion", + ) + testImplementation( + project(":backstopper-reusable-tests-junit5"), + project(":testonly:testonly-spring-webmvc-reusable-test-support"), + "org.junit.jupiter:junit-jupiter-api:$junit5Version", + "org.junit.jupiter:junit-jupiter-engine:$junit5Version", + "org.junit.jupiter:junit-jupiter-params:$junit5Version", + "org.mockito:mockito-core:$mockitoVersion", + "org.assertj:assertj-core:$assertJVersion", + "io.rest-assured:rest-assured", + ) +} + +// We're just running tests, not trying to stand up a real Springboot 3 server from gradle. +// Disable the bootJar task so gradle doesn't fall over. +bootJar.enabled = false diff --git a/testonly/testonly-springboot3_2-webmvc/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot3_2WebMvcComponentTest.java b/testonly/testonly-springboot3_2-webmvc/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot3_2WebMvcComponentTest.java new file mode 100644 index 0000000..3bd8071 --- /dev/null +++ b/testonly/testonly-springboot3_2-webmvc/src/test/java/com/nike/backstopper/testonly/BackstopperSpringboot3_2WebMvcComponentTest.java @@ -0,0 +1,574 @@ +package com.nike.backstopper.testonly; + +import com.nike.backstopper.apierror.ApiError; +import com.nike.backstopper.apierror.ApiErrorWithMetadata; +import com.nike.backstopper.apierror.sample.SampleCoreApiError; +import com.nike.internal.util.MapBuilder; +import com.nike.internal.util.Pair; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ConfigurableApplicationContext; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import io.restassured.http.ContentType; +import io.restassured.response.ExtractableResponse; +import serverconfig.classpathscan.Springboot3_2WebMvcClasspathScanConfig; +import serverconfig.directimport.Springboot3_2WebMvcDirectImportConfig; +import testonly.componenttest.spring.reusable.error.SampleProjectApiError; +import testonly.componenttest.spring.reusable.model.RgbColor; +import testonly.componenttest.spring.reusable.model.SampleModel; + +import static com.nike.internal.util.testing.TestUtils.findFreePort; +import static io.restassured.RestAssured.given; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static testonly.componenttest.spring.reusable.controller.SampleController.CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH; +import static testonly.componenttest.spring.reusable.controller.SampleController.SAMPLE_PATH; +import static testonly.componenttest.spring.reusable.controller.SampleController.TRIGGER_UNHANDLED_ERROR_SUBPATH; +import static testonly.componenttest.spring.reusable.controller.SampleController.WITH_REQUIRED_HEADER_SUBPATH; +import static testonly.componenttest.spring.reusable.controller.SampleController.WITH_REQUIRED_QUERY_PARAM_SUBPATH; +import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.FOO_STRING_CANNOT_BE_BLANK; +import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.INVALID_RANGE_VALUE; +import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.NOT_RGB_COLOR_ENUM; +import static testonly.componenttest.spring.reusable.error.SampleProjectApiError.RGB_COLOR_CANNOT_BE_NULL; +import static testonly.componenttest.spring.reusable.testutil.TestUtils.randomizedSampleModel; +import static testonly.componenttest.spring.reusable.testutil.TestUtils.verifyErrorReceived; + +/** + * Component test to verify that the functionality of {@code backstopper-spring-boot3-webmvc} works as expected in a + * Spring Boot 3.2.x Web MVC environment, for both classpath-scanning and direct-import Backstopper configuration use + * cases. + * + * @author Nic Munroe + */ +@SuppressWarnings({"ClassEscapesDefinedScope", "NewClassNamingConvention"}) +public class BackstopperSpringboot3_2WebMvcComponentTest { + + private static final int CLASSPATH_SCAN_SERVER_PORT = findFreePort(); + private static final int DIRECT_IMPORT_SERVER_PORT = findFreePort(); + private static final ObjectMapper objectMapper = new ObjectMapper(); + + private static ConfigurableApplicationContext classpathScanServerAppContext; + private static ConfigurableApplicationContext directImportServerAppContext; + + @BeforeAll + public static void beforeClass() { + assertThat(CLASSPATH_SCAN_SERVER_PORT).isNotEqualTo(DIRECT_IMPORT_SERVER_PORT); + classpathScanServerAppContext = SpringApplication.run( + Springboot3_2WebMvcClasspathScanConfig.class, "--server.port=" + CLASSPATH_SCAN_SERVER_PORT + ); + directImportServerAppContext = SpringApplication.run( + Springboot3_2WebMvcDirectImportConfig.class, "--server.port=" + DIRECT_IMPORT_SERVER_PORT + ); + } + + @AfterAll + public static void afterClass() { + SpringApplication.exit(classpathScanServerAppContext); + SpringApplication.exit(directImportServerAppContext); + } + + @SuppressWarnings("unused") + private enum ServerScenario { + CLASSPATH_SCAN_SERVER(CLASSPATH_SCAN_SERVER_PORT), + DIRECT_IMPORT_SERVER(DIRECT_IMPORT_SERVER_PORT); + + public final int serverPort; + + ServerScenario(int serverPort) { + this.serverPort = serverPort; + } + } + + // *************** SUCCESSFUL (NON ERROR) CALLS ****************** + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_basic_sample_get(ServerScenario scenario) throws IOException { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + assertThat(response.statusCode()).isEqualTo(200); + SampleModel responseBody = objectMapper.readValue(response.asString(), SampleModel.class); + assertThat(responseBody).isNotNull(); + assertThat(responseBody.foo).isNotEmpty(); + assertThat(responseBody.range_0_to_42).isNotEmpty(); + assertThat(Integer.parseInt(responseBody.range_0_to_42)).isBetween(0, 42); + assertThat(responseBody.rgb_color).isNotEmpty(); + assertThat(RgbColor.toRgbColor(responseBody.rgb_color)).isNotNull(); + assertThat(responseBody.throw_manual_error).isFalse(); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_basic_sample_post(ServerScenario scenario) throws IOException { + SampleModel requestPayload = randomizedSampleModel(); + String requestPayloadAsString = objectMapper.writeValueAsString(requestPayload); + + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .contentType(ContentType.JSON) + .basePath(SAMPLE_PATH) + .body(requestPayloadAsString) + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + assertThat(response.statusCode()).isEqualTo(201); + SampleModel responseBody = objectMapper.readValue(response.asString(), SampleModel.class); + assertThat(responseBody).isNotNull(); + assertThat(responseBody.foo).isEqualTo(requestPayload.foo); + assertThat(responseBody.range_0_to_42).isEqualTo(requestPayload.range_0_to_42); + assertThat(responseBody.rgb_color).isEqualTo(requestPayload.rgb_color); + assertThat(responseBody.throw_manual_error).isEqualTo(requestPayload.throw_manual_error); + } + + // *************** JSR 303 AND ENDPOINT ERRORS ****************** + + @SuppressWarnings("unused") + private enum Jsr303SampleModelValidationScenario { + BLANK_FIELD_VIOLATION( + new SampleModel("", "42", "GREEN", false), + singletonList(FOO_STRING_CANNOT_BE_BLANK) + ), + INVALID_RANGE_VIOLATION( + new SampleModel("bar", "-1", "GREEN", false), + singletonList(INVALID_RANGE_VALUE) + ), + NULL_FIELD_VIOLATION( + new SampleModel("bar", "42", null, false), + singletonList(RGB_COLOR_CANNOT_BE_NULL) + ), + STRING_CONVERTS_TO_CLASSTYPE_VIOLATION( + new SampleModel("bar", "42", "car", false), + singletonList(NOT_RGB_COLOR_ENUM) + ), + MULTIPLE_VIOLATIONS( + new SampleModel(" \n\r\t ", "99", "tree", false), + Arrays.asList(FOO_STRING_CANNOT_BE_BLANK, INVALID_RANGE_VALUE, NOT_RGB_COLOR_ENUM) + ); + + public final SampleModel model; + public final List expectedErrors; + + Jsr303SampleModelValidationScenario( + SampleModel model, List expectedErrors + ) { + this.model = model; + this.expectedErrors = expectedErrors; + } + } + + public static List jsr303ValidationErrorScenariosDataProvider() { + List result = new ArrayList<>(); + for (Jsr303SampleModelValidationScenario violationScenario : Jsr303SampleModelValidationScenario.values()) { + for (ServerScenario serverScenario : ServerScenario.values()) { + result.add(new Object[]{violationScenario, serverScenario}); + } + } + return result; + } + + @MethodSource("jsr303ValidationErrorScenariosDataProvider") + @ParameterizedTest + public void verify_jsr303_validation_errors( + Jsr303SampleModelValidationScenario violationScenario, ServerScenario serverScenario + ) throws JsonProcessingException { + String requestPayloadAsString = objectMapper.writeValueAsString(violationScenario.model); + + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(serverScenario.serverPort) + .contentType(ContentType.JSON) + .basePath(SAMPLE_PATH) + .body(requestPayloadAsString) + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + List expectedErrors = new ArrayList<>(); + for (ApiError expectedApiError : violationScenario.expectedErrors) { + String extraMetadataFieldValue = null; + + if (INVALID_RANGE_VALUE.equals(expectedApiError)) { + extraMetadataFieldValue = "range_0_to_42"; + } + else if (RGB_COLOR_CANNOT_BE_NULL.equals(expectedApiError) || NOT_RGB_COLOR_ENUM.equals(expectedApiError)) { + extraMetadataFieldValue = "rgb_color"; + } + + if (extraMetadataFieldValue != null) { + expectedApiError = new ApiErrorWithMetadata( + expectedApiError, + MapBuilder.builder("field", (Object) extraMetadataFieldValue).build() + ); + } + + expectedErrors.add(expectedApiError); + } + verifyErrorReceived(response, expectedErrors, 400); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_MANUALLY_THROWN_ERROR_is_thrown_when_requested(ServerScenario scenario) throws IOException { + SampleModel requestPayload = new SampleModel("bar", "42", "RED", true); + String requestPayloadAsString = objectMapper.writeValueAsString(requestPayload); + + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .contentType(ContentType.JSON) + .basePath(SAMPLE_PATH) + .body(requestPayloadAsString) + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleProjectApiError.MANUALLY_THROWN_ERROR); + // This code path also should add some custom headers to the response + assertThat(response.headers().getValues("rgbColorValue")).isEqualTo(singletonList(requestPayload.rgb_color)); + assertThat(response.headers().getValues("otherExtraMultivalueHeader")).isEqualTo(Arrays.asList("foo", "bar")); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_SOME_MEANINGFUL_ERROR_NAME_is_thrown_when_correct_endpoint_is_hit(ServerScenario scenario) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + CORE_ERROR_WRAPPER_ENDPOINT_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleProjectApiError.SOME_MEANINGFUL_ERROR_NAME); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_GENERIC_SERVICE_ERROR_is_thrown_when_correct_endpoint_is_hit(ServerScenario scenario) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + TRIGGER_UNHANDLED_ERROR_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.GENERIC_SERVICE_ERROR); + } + + // *************** FRAMEWORK ERRORS ****************** + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_NOT_FOUND_returned_if_unknown_path_is_requested(ServerScenario scenario) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(UUID.randomUUID().toString()) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.NOT_FOUND); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_ERROR_THROWN_IN_SERVLET_FILTER_OUTSIDE_SPRING_returned_if_servlet_filter_trigger_occurs( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .header("throw-servlet-filter-exception", "true") + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleProjectApiError.ERROR_THROWN_IN_SERVLET_FILTER_OUTSIDE_SPRING); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_METHOD_NOT_ALLOWED_returned_if_known_path_is_requested_with_invalid_http_method( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .log().all() + .when() + .delete() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.METHOD_NOT_ALLOWED); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_sample_get_fails_with_NO_ACCEPTABLE_REPRESENTATION_if_passed_invalid_accept_header( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .accept(ContentType.BINARY) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.NO_ACCEPTABLE_REPRESENTATION); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_sample_post_fails_with_UNSUPPORTED_MEDIA_TYPE_if_passed_invalid_content_type( + ServerScenario scenario + ) throws IOException { + SampleModel requestPayload = randomizedSampleModel(); + String requestPayloadAsString = objectMapper.writeValueAsString(requestPayload); + + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .body(requestPayloadAsString) + .contentType(ContentType.TEXT) + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.UNSUPPORTED_MEDIA_TYPE); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_MALFORMED_REQUEST_is_thrown_when_required_query_param_is_missing( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + WITH_REQUIRED_QUERY_PARAM_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived( + response, + new ApiErrorWithMetadata( + SampleCoreApiError.MALFORMED_REQUEST, + Pair.of("missing_param_name", "requiredQueryParamValue"), + Pair.of("missing_param_type", "int"), + Pair.of("required_location", "query_param") + ) + ); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert_type_for_query_param( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + WITH_REQUIRED_QUERY_PARAM_SUBPATH) + .queryParam("requiredQueryParamValue", "not-an-integer") + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, new ApiErrorWithMetadata( + SampleCoreApiError.TYPE_CONVERSION_ERROR, + MapBuilder.builder("bad_property_name", (Object) "requiredQueryParamValue") + .put("bad_property_value","not-an-integer") + .put("required_location","query_param") + .put("required_type", "int") + .build() + )); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_MALFORMED_REQUEST_is_thrown_when_required_header_is_missing( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + WITH_REQUIRED_HEADER_SUBPATH) + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived( + response, + new ApiErrorWithMetadata( + SampleCoreApiError.MALFORMED_REQUEST, + Pair.of("missing_param_name", "requiredHeaderValue"), + Pair.of("missing_param_type", "int"), + Pair.of("required_location", "header") + ) + ); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert_type_for_header( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH + WITH_REQUIRED_HEADER_SUBPATH) + .header("requiredHeaderValue", "not-an-integer") + .log().all() + .when() + .get() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, new ApiErrorWithMetadata( + SampleCoreApiError.TYPE_CONVERSION_ERROR, + MapBuilder.builder("bad_property_name", (Object) "requiredHeaderValue") + .put("bad_property_value","not-an-integer") + .put("required_location","header") + .put("required_type", "int") + .build() + )); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_sample_post_fails_with_MISSING_EXPECTED_CONTENT_if_passed_empty_body( + ServerScenario scenario + ) { + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .contentType(ContentType.JSON) + .body("") + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.MISSING_EXPECTED_CONTENT); + } + + @EnumSource(ServerScenario.class) + @ParameterizedTest + public void verify_sample_post_fails_with_MALFORMED_REQUEST_if_passed_bad_json_body( + ServerScenario scenario + ) throws IOException { + SampleModel originalValidPayloadObj = randomizedSampleModel(); + String originalValidPayloadAsString = objectMapper.writeValueAsString(originalValidPayloadObj); + @SuppressWarnings("unchecked") + Map badRequestPayloadAsMap = objectMapper.readValue(originalValidPayloadAsString, Map.class); + badRequestPayloadAsMap.put("throw_manual_error", "not-a-boolean"); + String badJsonPayloadAsString = objectMapper.writeValueAsString(badRequestPayloadAsMap); + + ExtractableResponse response = + given() + .baseUri("http://localhost") + .port(scenario.serverPort) + .basePath(SAMPLE_PATH) + .contentType(ContentType.JSON) + .body(badJsonPayloadAsString) + .log().all() + .when() + .post() + .then() + .log().all() + .extract(); + + verifyErrorReceived(response, SampleCoreApiError.MALFORMED_REQUEST); + } +} diff --git a/testonly/testonly-springboot3_2-webmvc/src/test/java/serverconfig/classpathscan/Springboot3_2WebMvcClasspathScanConfig.java b/testonly/testonly-springboot3_2-webmvc/src/test/java/serverconfig/classpathscan/Springboot3_2WebMvcClasspathScanConfig.java new file mode 100644 index 0000000..0d53a42 --- /dev/null +++ b/testonly/testonly-springboot3_2-webmvc/src/test/java/serverconfig/classpathscan/Springboot3_2WebMvcClasspathScanConfig.java @@ -0,0 +1,49 @@ +package serverconfig.classpathscan; + +import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.core.Ordered; + +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import testonly.componenttest.spring.reusable.error.SampleProjectApiErrorsImpl; +import testonly.componenttest.spring.reusable.testutil.ExplodingServletFilter; + +/** + * Springboot config that uses {@link ComponentScan} to integrate Backstopper via classpath scanning of the + * {@code com.nike.backstopper} package. + * + * @author Nic Munroe + */ +@SpringBootApplication +@ComponentScan(basePackages = { + // Component scan the core Backstopper+Springboot1 support. + "com.nike.backstopper", + // Component scan the controller. + "testonly.componenttest.spring.reusable.controller" +}) +@SuppressWarnings("unused") +public class Springboot3_2WebMvcClasspathScanConfig { + + @Bean + public ProjectApiErrors getProjectApiErrors() { + return new SampleProjectApiErrorsImpl(); + } + + @Bean + public Validator getJsr303Validator() { + //noinspection resource + return Validation.buildDefaultValidatorFactory().getValidator(); + } + + @Bean + public FilterRegistrationBean explodingServletFilter() { + FilterRegistrationBean frb = new FilterRegistrationBean<>(new ExplodingServletFilter()); + frb.setOrder(Ordered.HIGHEST_PRECEDENCE); + return frb; + } +} diff --git a/testonly/testonly-springboot3_2-webmvc/src/test/java/serverconfig/directimport/Springboot3_2WebMvcDirectImportConfig.java b/testonly/testonly-springboot3_2-webmvc/src/test/java/serverconfig/directimport/Springboot3_2WebMvcDirectImportConfig.java new file mode 100644 index 0000000..c9137df --- /dev/null +++ b/testonly/testonly-springboot3_2-webmvc/src/test/java/serverconfig/directimport/Springboot3_2WebMvcDirectImportConfig.java @@ -0,0 +1,51 @@ +package serverconfig.directimport; + +import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; +import com.nike.backstopper.handler.springboot.config.BackstopperSpringboot3WebMvcConfig; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.core.Ordered; + +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import testonly.componenttest.spring.reusable.controller.SampleController; +import testonly.componenttest.spring.reusable.error.SampleProjectApiErrorsImpl; +import testonly.componenttest.spring.reusable.testutil.ExplodingServletFilter; + +/** + * Springboot config that uses {@link Import} to integrate Backstopper via direct import of + * {@link BackstopperSpringboot3WebMvcConfig}. + * + * @author Nic Munroe + */ +@SpringBootApplication +@Import({ + // Import core Backstopper+Springboot1 support. + BackstopperSpringboot3WebMvcConfig.class, + // Import the controller. + SampleController.class +}) +@SuppressWarnings("unused") +public class Springboot3_2WebMvcDirectImportConfig { + + @Bean + public ProjectApiErrors getProjectApiErrors() { + return new SampleProjectApiErrorsImpl(); + } + + @Bean + public Validator getJsr303Validator() { + //noinspection resource + return Validation.buildDefaultValidatorFactory().getValidator(); + } + + @Bean + public FilterRegistrationBean explodingServletFilter() { + FilterRegistrationBean frb = new FilterRegistrationBean<>(new ExplodingServletFilter()); + frb.setOrder(Ordered.HIGHEST_PRECEDENCE); + return frb; + } +} diff --git a/testonly/testonly-springboot3_2-webmvc/src/test/resources/logback.xml b/testonly/testonly-springboot3_2-webmvc/src/test/resources/logback.xml new file mode 100644 index 0000000..80adb28 --- /dev/null +++ b/testonly/testonly-springboot3_2-webmvc/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{yyyy-MM-dd_HH:mm:ss.SSS} [%thread] |-%-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file From d7ac08dae5835395f5c3026f5ec47caea0d50790 Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Wed, 11 Sep 2024 19:04:31 -0700 Subject: [PATCH 31/42] Clean up springboot 1 and 2 references, and bring disconnected-client detection up to current spring logic --- .../UnhandledServletContainerErrorHelper.java | 7 +-- ...andledServletContainerErrorHelperTest.java | 48 ++++++--------- .../webflux/DisconnectedClientHelper.java | 59 +++++++++++++++++++ .../SpringWebfluxApiExceptionHandler.java | 40 +++---------- ...pringWebfluxUnhandledExceptionHandler.java | 11 ++-- .../SpringWebfluxApiExceptionHandlerTest.java | 3 +- ...BackstopperSpringWebFluxComponentTest.java | 2 +- backstopper-spring-web-mvc/README.md | 2 - backstopper-spring-web/README.md | 7 +-- .../build.gradle | 2 +- ...pringboot3_0WebMvcClasspathScanConfig.java | 2 +- ...Springboot3_0WebMvcDirectImportConfig.java | 2 +- .../build.gradle | 2 +- ...pringboot3_1WebMvcClasspathScanConfig.java | 2 +- ...Springboot3_1WebMvcDirectImportConfig.java | 2 +- .../build.gradle | 2 +- ...pringboot3_2WebMvcClasspathScanConfig.java | 2 +- ...Springboot3_2WebMvcDirectImportConfig.java | 2 +- .../build.gradle | 2 +- ...pringboot3_3WebMvcClasspathScanConfig.java | 2 +- ...Springboot3_3WebMvcDirectImportConfig.java | 2 +- 21 files changed, 107 insertions(+), 96 deletions(-) create mode 100644 backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/DisconnectedClientHelper.java diff --git a/backstopper-servlet-api/src/main/java/com/nike/backstopper/servletapi/UnhandledServletContainerErrorHelper.java b/backstopper-servlet-api/src/main/java/com/nike/backstopper/servletapi/UnhandledServletContainerErrorHelper.java index e671626..3889bf3 100644 --- a/backstopper-servlet-api/src/main/java/com/nike/backstopper/servletapi/UnhandledServletContainerErrorHelper.java +++ b/backstopper-servlet-api/src/main/java/com/nike/backstopper/servletapi/UnhandledServletContainerErrorHelper.java @@ -45,18 +45,13 @@ @SuppressWarnings("WeakerAccess") public class UnhandledServletContainerErrorHelper { - // TODO javax-to-jakarta: Test these things in with the new spring/springboot libs/frameworks. protected static final List DEFAULT_THROWABLE_REQUEST_ATTR_NAMES = Arrays.asList( - // Try the Springboot 2 attrs first. + // Try the Springboot 3 attrs first. // Corresponds to org.springframework.boot.web.reactive.error.DefaultErrorAttributes.ERROR_ATTRIBUTE. "org.springframework.boot.web.reactive.error.DefaultErrorAttributes.ERROR", // Corresponds to org.springframework.boot.web.servlet.error.DefaultErrorAttributes.ERROR_ATTRIBUTE. "org.springframework.boot.web.servlet.error.DefaultErrorAttributes.ERROR", - // Try the Springboot 1 attr next. - // Corresponds to org.springframework.boot.autoconfigure.web.DefaultErrorAttributes.ERROR_ATTRIBUTE. - "org.springframework.boot.autoconfigure.web.DefaultErrorAttributes.ERROR", - // Fall back to the Servlet API value last. // Corresponds to jakarta.servlet.RequestDispatcher.ERROR_EXCEPTION. "jakarta.servlet.error.exception" diff --git a/backstopper-servlet-api/src/test/java/com/nike/backstopper/servletapi/UnhandledServletContainerErrorHelperTest.java b/backstopper-servlet-api/src/test/java/com/nike/backstopper/servletapi/UnhandledServletContainerErrorHelperTest.java index 70fb1a8..0bcd538 100644 --- a/backstopper-servlet-api/src/test/java/com/nike/backstopper/servletapi/UnhandledServletContainerErrorHelperTest.java +++ b/backstopper-servlet-api/src/test/java/com/nike/backstopper/servletapi/UnhandledServletContainerErrorHelperTest.java @@ -60,43 +60,35 @@ protected ProjectSpecificErrorCodeRange getProjectSpecificErrorCodeRange() { } @DataProvider(value = { - "true | true | true | true", - "false | true | true | true", - "false | false | true | true", - "false | false | false | true", - "false | false | false | false", + "true | true | true", + "false | true | true", + "false | false | true", + "false | false | true", + "false | false | false", }, splitBy = "\\|") @Test public void extractOrGenerateErrorForRequest_returns_wrapped_exception_from_request_attrs_if_available( - boolean requestHasExInSpringboot2ReactiveAttr, - boolean requestHasExInSpringboot2ServletAttr, - boolean requestHasExInSpringboot1Attr, + boolean requestHasExInSpringboot3ReactiveAttr, + boolean requestHasExInSpringboot3ServletAttr, boolean requestHasExInServletAttr ) { // given - Throwable springboot2ReactiveAttrEx = new RuntimeException("some springboot 2 reactive request attr exception"); - Throwable springboot2ServletAttrEx = new RuntimeException("some springboot 2 servlet request attr exception"); - Throwable springboot1AttrEx = new RuntimeException("some springboot 1 request attr exception"); + Throwable springboot3ReactiveAttrEx = new RuntimeException("some springboot 3 reactive request attr exception"); + Throwable springboot3ServletAttrEx = new RuntimeException("some springboot 3 servlet request attr exception"); Throwable servletAttrEx = new RuntimeException("some servlet request attr exception"); - if (requestHasExInSpringboot2ReactiveAttr) { - doReturn(springboot2ReactiveAttrEx) + if (requestHasExInSpringboot3ReactiveAttr) { + doReturn(springboot3ReactiveAttrEx) .when(requestMock) .getAttribute("org.springframework.boot.web.reactive.error.DefaultErrorAttributes.ERROR"); } - if (requestHasExInSpringboot2ServletAttr) { - doReturn(springboot2ServletAttrEx) + if (requestHasExInSpringboot3ServletAttr) { + doReturn(springboot3ServletAttrEx) .when(requestMock) .getAttribute("org.springframework.boot.web.servlet.error.DefaultErrorAttributes.ERROR"); } - if (requestHasExInSpringboot1Attr) { - doReturn(springboot1AttrEx) - .when(requestMock) - .getAttribute("org.springframework.boot.autoconfigure.web.DefaultErrorAttributes.ERROR"); - } - if (requestHasExInServletAttr) { doReturn(servletAttrEx).when(requestMock).getAttribute(RequestDispatcher.ERROR_EXCEPTION); } @@ -105,23 +97,17 @@ public void extractOrGenerateErrorForRequest_returns_wrapped_exception_from_requ Throwable result = helper.extractOrGenerateErrorForRequest(requestMock, projectApiErrors); // then - if (requestHasExInSpringboot2ReactiveAttr) { - assertThat(result) - .isInstanceOf(WrapperException.class) - .hasMessage("Caught a container exception.") - .hasCause(springboot2ReactiveAttrEx); - } - else if (requestHasExInSpringboot2ServletAttr) { + if (requestHasExInSpringboot3ReactiveAttr) { assertThat(result) .isInstanceOf(WrapperException.class) .hasMessage("Caught a container exception.") - .hasCause(springboot2ServletAttrEx); + .hasCause(springboot3ReactiveAttrEx); } - else if (requestHasExInSpringboot1Attr) { + else if (requestHasExInSpringboot3ServletAttr) { assertThat(result) .isInstanceOf(WrapperException.class) .hasMessage("Caught a container exception.") - .hasCause(springboot1AttrEx); + .hasCause(springboot3ServletAttrEx); } else if (requestHasExInServletAttr) { assertThat(result) diff --git a/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/DisconnectedClientHelper.java b/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/DisconnectedClientHelper.java new file mode 100644 index 0000000..bb074ec --- /dev/null +++ b/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/DisconnectedClientHelper.java @@ -0,0 +1,59 @@ +/* + * 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 com.nike.backstopper.handler.spring.webflux; + +import org.springframework.core.NestedExceptionUtils; + +import java.util.Set; + +/** + * Copied from spring-web-6.1.12. We need some of the same logic in Backstopper. + * Modified slightly from the original to make the methods static, get rid of the logging and methods we don't need, etc. + */ +class DisconnectedClientHelper { + + private static final Set EXCEPTION_PHRASES = + Set.of("broken pipe", "connection reset"); + + private static final Set EXCEPTION_TYPE_NAMES = + Set.of("AbortedException", "ClientAbortException", + "EOFException", "EofException", "AsyncRequestNotUsableException"); + + /** + * Whether the given exception indicates the client has gone away. + *

Known cases covered: + *

    + *
  • ClientAbortException or EOFException for Tomcat + *
  • EofException for Jetty + *
  • IOException "Broken pipe" or "connection reset by peer" + *
  • SocketException "Connection reset" + *
+ */ + public static boolean isClientDisconnectedException(Throwable ex) { + String message = NestedExceptionUtils.getMostSpecificCause(ex).getMessage(); + if (message != null) { + String text = message.toLowerCase(); + for (String phrase : EXCEPTION_PHRASES) { + if (text.contains(phrase)) { + return true; + } + } + } + return EXCEPTION_TYPE_NAMES.contains(ex.getClass().getSimpleName()); + } + +} diff --git a/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandler.java b/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandler.java index 2d18b8d..8138bdc 100644 --- a/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandler.java +++ b/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandler.java @@ -15,7 +15,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.ObjectProvider; -import org.springframework.core.NestedExceptionUtils; import org.springframework.core.Ordered; import org.springframework.http.codec.HttpMessageReader; import org.springframework.http.codec.HttpMessageWriter; @@ -27,20 +26,18 @@ import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebExceptionHandler; -import java.util.Arrays; import java.util.Collection; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.stream.Collectors; import jakarta.inject.Inject; import jakarta.inject.Named; import jakarta.inject.Singleton; - import reactor.core.publisher.Mono; +import static com.nike.backstopper.handler.spring.webflux.DisconnectedClientHelper.isClientDisconnectedException; + /** * An {@link ApiExceptionHandlerBase} extension that hooks into Spring WebFlux via its * {@link WebExceptionHandler} interface, and specifically the @@ -59,12 +56,6 @@ public class SpringWebfluxApiExceptionHandler extends ApiExceptionHandlerBase DISCONNECTED_CLIENT_EXCEPTIONS = new HashSet<>( - Arrays.asList("AbortedException", "ClientAbortException", "EOFException", "EofException")); - /** * The sort order for where this handler goes in the spring exception handler chain. We default to {@link * Ordered#HIGHEST_PRECEDENCE}, so that this is tried first before any other handlers. @@ -150,8 +141,8 @@ public Mono handle(ServerWebExchange exchange, Throwable ex) { // Before we try to write the response, we should check to see if it's already committed, or if the client // disconnected. // This short circuit logic due to an already-committed response or disconnected client was copied from - // Spring Boot 2's AbstractErrorWebExceptionHandler class. - if (exchange.getResponse().isCommitted() || isDisconnectedClientError(ex)) { + // Spring Boot's AbstractErrorWebExceptionHandler class. + if (exchange.getResponse().isCommitted() || isClientDisconnectedException(ex)) { return Mono.error(ex); } @@ -175,29 +166,13 @@ protected void processWebFluxResponse( } } - // Copied from Spring Boot 2's AbstractErrorWebExceptionHandler class. + // Copied and slightly modified from Spring Boot 3.3.3's AbstractErrorWebExceptionHandler class. protected Mono write(ServerWebExchange exchange, ServerResponse response) { // force content-type since writeTo won't overwrite response header values exchange.getResponse().getHeaders().setContentType(response.headers().getContentType()); return response.writeTo(exchange, new ResponseContext(messageWriters, viewResolvers)); } - /** - * Copied from Spring Boot 2's {@code AbstractErrorWebExceptionHandler} class. - */ - public static boolean isDisconnectedClientError(Throwable ex) { - return DISCONNECTED_CLIENT_EXCEPTIONS.contains(ex.getClass().getSimpleName()) - || isDisconnectedClientErrorMessage(NestedExceptionUtils.getMostSpecificCause(ex).getMessage()); - } - - /** - * Copied from Spring Boot 2's {@code AbstractErrorWebExceptionHandler} class. - */ - public static boolean isDisconnectedClientErrorMessage(String message) { - message = (message != null) ? message.toLowerCase() : ""; - return (message.contains("broken pipe") || message.contains("connection reset by peer")); - } - /** * See the javadocs for {@link #order} for info on what this is for. */ @@ -214,6 +189,7 @@ public void setOrder(int order) { this.order = order; } + // Copied and slightly modified from Springboot 3.3.3's AbstractErrorWebExceptionHandler. public static class ResponseContext implements ServerResponse.Context { private final List> messageWriters; @@ -228,12 +204,12 @@ protected ResponseContext( } @Override - public List> messageWriters() { + public @NotNull List> messageWriters() { return messageWriters; } @Override - public List viewResolvers() { + public @NotNull List viewResolvers() { return viewResolvers; } } diff --git a/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxUnhandledExceptionHandler.java b/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxUnhandledExceptionHandler.java index b476bd6..ab35bef 100644 --- a/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxUnhandledExceptionHandler.java +++ b/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxUnhandledExceptionHandler.java @@ -33,10 +33,9 @@ import jakarta.inject.Inject; import jakarta.inject.Named; import jakarta.inject.Singleton; - import reactor.core.publisher.Mono; -import static com.nike.backstopper.handler.spring.webflux.SpringWebfluxApiExceptionHandler.isDisconnectedClientError; +import static com.nike.backstopper.handler.spring.webflux.DisconnectedClientHelper.isClientDisconnectedException; /** * An extension of {@link UnhandledExceptionHandlerBase} that acts as a final catch-all exception handler for @@ -55,7 +54,7 @@ public class SpringWebfluxUnhandledExceptionHandler /** * The sort order for where this handler goes in the spring exception handler chain. We default to -2 so this gets - * executed after any custom handlers, but before any default spring handlers (Spring Boot 2's + * executed after any custom handlers, but before any default spring handlers (Spring Boot 3's * {@code DefaultErrorWebExceptionHandler} is ordered at -1, see {@code * ErrorWebFluxAutoConfiguration.errorWebExceptionHandler(...)}. And {@code WebFluxResponseStatusExceptionHandler} * is ordered at 0, see {@code WebFluxConfigurationSupport.responseStatusExceptionHandler(...)}). @@ -151,8 +150,8 @@ public Mono handle(ServerWebExchange exchange, Throwable ex) { // Before we try to write the response, we should check to see if it's already committed, or if the client // disconnected. // This short circuit logic due to an already-committed response or disconnected client was copied from - // Spring Boot 2's AbstractErrorWebExceptionHandler class. - if (exchange.getResponse().isCommitted() || isDisconnectedClientError(ex)) { + // Spring Boot's AbstractErrorWebExceptionHandler class. + if (exchange.getResponse().isCommitted() || isClientDisconnectedException(ex)) { return Mono.error(ex); } @@ -176,7 +175,7 @@ protected void processWebFluxResponse( } } - // Copied from Spring Boot 2's AbstractErrorWebExceptionHandler class. + // Copied and slightly modified from Spring Boot 3.3.3's AbstractErrorWebExceptionHandler class. protected Mono write(ServerWebExchange exchange, ServerResponse response) { // force content-type since writeTo won't overwrite response header values exchange.getResponse().getHeaders().setContentType(response.headers().getContentType()); diff --git a/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandlerTest.java b/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandlerTest.java index cc286f2..50a5868 100644 --- a/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandlerTest.java +++ b/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandlerTest.java @@ -48,6 +48,7 @@ import reactor.core.publisher.Mono; import reactor.netty.channel.AbortedException; +import static com.nike.backstopper.handler.spring.webflux.DisconnectedClientHelper.isClientDisconnectedException; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; import static org.mockito.ArgumentMatchers.any; @@ -484,7 +485,7 @@ public static List> isDisconnectedClient @Test public void isDisconnectedClientError_works_as_expected(IsDisconnectedClientErrorScenario scenario) { // when - boolean result = SpringWebfluxApiExceptionHandler.isDisconnectedClientError(scenario.ex); + boolean result = isClientDisconnectedException(scenario.ex); // then assertThat(result).isEqualTo(scenario.expectedResult); diff --git a/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/componenttest/BackstopperSpringWebFluxComponentTest.java b/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/componenttest/BackstopperSpringWebFluxComponentTest.java index 4de192f..9fda339 100644 --- a/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/componenttest/BackstopperSpringWebFluxComponentTest.java +++ b/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/componenttest/BackstopperSpringWebFluxComponentTest.java @@ -1141,7 +1141,7 @@ public String triggerServerErrorExceptionEndpoint(@PathVariable(name = "notFoo") return "we should never reach here"; } - // Can't figure out how to get springboot 2 to trigger a ConversionNotSupportedException naturally, so + // Can't figure out how to get springboot to trigger a ConversionNotSupportedException naturally, so // we'll just throw one ourselves. @GetMapping(path = CONVERSION_NOT_SUPPORTED_EXCEPTION_ENDPOINT_PATH) @ResponseBody diff --git a/backstopper-spring-web-mvc/README.md b/backstopper-spring-web-mvc/README.md index 676036e..aa1a167 100644 --- a/backstopper-spring-web-mvc/README.md +++ b/backstopper-spring-web-mvc/README.md @@ -18,8 +18,6 @@ concrete example of the information covered in this readme.** ### Spring / Spring Boot Versions -// TODO javax-to-jakarta: verify this statement about spring boot 3 - This `backstopper-spring-web-mvc` library can be used in any Spring Web MVC `6.x.x` environment or later. This includes Spring 6 and Spring Boot 3. diff --git a/backstopper-spring-web/README.md b/backstopper-spring-web/README.md index baa0704..2e4218c 100644 --- a/backstopper-spring-web/README.md +++ b/backstopper-spring-web/README.md @@ -13,17 +13,14 @@ does not provide Spring+Backstopper integration by itself. To integrate Backstopper with your Spring application, please choose the correct concrete integration library, depending on which Spring environment your application is running in: -// TODO javax-to-jakarta: Fix these links to other libraries after the refactor is complete. - ### Spring WebFlux based applications * [backstopper-spring-web-flux](../backstopper-spring-web-flux) - For Spring WebFlux applications. ### Spring Web MVC based applications -* [backstopper-spring-boot1](../backstopper-spring-boot1) - For Spring Boot 1 applications. -* [backstopper-spring-boot2-webmvc](../backstopper-spring-boot2-webmvc) - For Spring Boot 2 applications using the -Spring MVC (Servlet) framework. If you want Spring Boot 2 with Spring WebFlux (Netty) framework, then see +* [backstopper-spring-boot3-webmvc](../backstopper-spring-boot3-webmvc) - For Spring Boot 3 applications using the +Spring MVC (Servlet) framework. If you want Spring Boot 3 with Spring WebFlux (Netty) framework, then see [backstopper-spring-web-flux](../backstopper-spring-web-flux) instead. * [backstopper-spring-web-mvc](../backstopper-spring-web-mvc) - For Spring Web MVC applications that are not Spring Boot. diff --git a/testonly/testonly-springboot3_0-webflux/build.gradle b/testonly/testonly-springboot3_0-webflux/build.gradle index 773577f..48d7a2e 100644 --- a/testonly/testonly-springboot3_0-webflux/build.gradle +++ b/testonly/testonly-springboot3_0-webflux/build.gradle @@ -39,6 +39,6 @@ dependencies { ) } -// We're just running tests, not trying to stand up a real Springboot 2 server from gradle. +// We're just running tests, not trying to stand up a real Springboot 3 server from gradle. // Disable the bootJar task so gradle doesn't fall over. bootJar.enabled = false diff --git a/testonly/testonly-springboot3_0-webmvc/src/test/java/serverconfig/classpathscan/Springboot3_0WebMvcClasspathScanConfig.java b/testonly/testonly-springboot3_0-webmvc/src/test/java/serverconfig/classpathscan/Springboot3_0WebMvcClasspathScanConfig.java index 3028fbd..518bbea 100644 --- a/testonly/testonly-springboot3_0-webmvc/src/test/java/serverconfig/classpathscan/Springboot3_0WebMvcClasspathScanConfig.java +++ b/testonly/testonly-springboot3_0-webmvc/src/test/java/serverconfig/classpathscan/Springboot3_0WebMvcClasspathScanConfig.java @@ -21,7 +21,7 @@ */ @SpringBootApplication @ComponentScan(basePackages = { - // Component scan the core Backstopper+Springboot1 support. + // Component scan the core Backstopper+Springboot support. "com.nike.backstopper", // Component scan the controller. "testonly.componenttest.spring.reusable.controller" diff --git a/testonly/testonly-springboot3_0-webmvc/src/test/java/serverconfig/directimport/Springboot3_0WebMvcDirectImportConfig.java b/testonly/testonly-springboot3_0-webmvc/src/test/java/serverconfig/directimport/Springboot3_0WebMvcDirectImportConfig.java index ffc939a..5e95312 100644 --- a/testonly/testonly-springboot3_0-webmvc/src/test/java/serverconfig/directimport/Springboot3_0WebMvcDirectImportConfig.java +++ b/testonly/testonly-springboot3_0-webmvc/src/test/java/serverconfig/directimport/Springboot3_0WebMvcDirectImportConfig.java @@ -23,7 +23,7 @@ */ @SpringBootApplication @Import({ - // Import core Backstopper+Springboot1 support. + // Import core Backstopper+Springboot support. BackstopperSpringboot3WebMvcConfig.class, // Import the controller. SampleController.class diff --git a/testonly/testonly-springboot3_1-webflux/build.gradle b/testonly/testonly-springboot3_1-webflux/build.gradle index 26b4fd3..111346f 100644 --- a/testonly/testonly-springboot3_1-webflux/build.gradle +++ b/testonly/testonly-springboot3_1-webflux/build.gradle @@ -39,6 +39,6 @@ dependencies { ) } -// We're just running tests, not trying to stand up a real Springboot 2 server from gradle. +// We're just running tests, not trying to stand up a real Springboot 3 server from gradle. // Disable the bootJar task so gradle doesn't fall over. bootJar.enabled = false diff --git a/testonly/testonly-springboot3_1-webmvc/src/test/java/serverconfig/classpathscan/Springboot3_1WebMvcClasspathScanConfig.java b/testonly/testonly-springboot3_1-webmvc/src/test/java/serverconfig/classpathscan/Springboot3_1WebMvcClasspathScanConfig.java index cc36dd9..08ca529 100644 --- a/testonly/testonly-springboot3_1-webmvc/src/test/java/serverconfig/classpathscan/Springboot3_1WebMvcClasspathScanConfig.java +++ b/testonly/testonly-springboot3_1-webmvc/src/test/java/serverconfig/classpathscan/Springboot3_1WebMvcClasspathScanConfig.java @@ -21,7 +21,7 @@ */ @SpringBootApplication @ComponentScan(basePackages = { - // Component scan the core Backstopper+Springboot1 support. + // Component scan the core Backstopper+Springboot support. "com.nike.backstopper", // Component scan the controller. "testonly.componenttest.spring.reusable.controller" diff --git a/testonly/testonly-springboot3_1-webmvc/src/test/java/serverconfig/directimport/Springboot3_1WebMvcDirectImportConfig.java b/testonly/testonly-springboot3_1-webmvc/src/test/java/serverconfig/directimport/Springboot3_1WebMvcDirectImportConfig.java index b49bbac..eb937c6 100644 --- a/testonly/testonly-springboot3_1-webmvc/src/test/java/serverconfig/directimport/Springboot3_1WebMvcDirectImportConfig.java +++ b/testonly/testonly-springboot3_1-webmvc/src/test/java/serverconfig/directimport/Springboot3_1WebMvcDirectImportConfig.java @@ -23,7 +23,7 @@ */ @SpringBootApplication @Import({ - // Import core Backstopper+Springboot1 support. + // Import core Backstopper+Springboot support. BackstopperSpringboot3WebMvcConfig.class, // Import the controller. SampleController.class diff --git a/testonly/testonly-springboot3_2-webflux/build.gradle b/testonly/testonly-springboot3_2-webflux/build.gradle index 2346d2c..0f1a56a 100644 --- a/testonly/testonly-springboot3_2-webflux/build.gradle +++ b/testonly/testonly-springboot3_2-webflux/build.gradle @@ -39,6 +39,6 @@ dependencies { ) } -// We're just running tests, not trying to stand up a real Springboot 2 server from gradle. +// We're just running tests, not trying to stand up a real Springboot 3 server from gradle. // Disable the bootJar task so gradle doesn't fall over. bootJar.enabled = false diff --git a/testonly/testonly-springboot3_2-webmvc/src/test/java/serverconfig/classpathscan/Springboot3_2WebMvcClasspathScanConfig.java b/testonly/testonly-springboot3_2-webmvc/src/test/java/serverconfig/classpathscan/Springboot3_2WebMvcClasspathScanConfig.java index 0d53a42..445bdb5 100644 --- a/testonly/testonly-springboot3_2-webmvc/src/test/java/serverconfig/classpathscan/Springboot3_2WebMvcClasspathScanConfig.java +++ b/testonly/testonly-springboot3_2-webmvc/src/test/java/serverconfig/classpathscan/Springboot3_2WebMvcClasspathScanConfig.java @@ -21,7 +21,7 @@ */ @SpringBootApplication @ComponentScan(basePackages = { - // Component scan the core Backstopper+Springboot1 support. + // Component scan the core Backstopper+Springboot support. "com.nike.backstopper", // Component scan the controller. "testonly.componenttest.spring.reusable.controller" diff --git a/testonly/testonly-springboot3_2-webmvc/src/test/java/serverconfig/directimport/Springboot3_2WebMvcDirectImportConfig.java b/testonly/testonly-springboot3_2-webmvc/src/test/java/serverconfig/directimport/Springboot3_2WebMvcDirectImportConfig.java index c9137df..da6c0ba 100644 --- a/testonly/testonly-springboot3_2-webmvc/src/test/java/serverconfig/directimport/Springboot3_2WebMvcDirectImportConfig.java +++ b/testonly/testonly-springboot3_2-webmvc/src/test/java/serverconfig/directimport/Springboot3_2WebMvcDirectImportConfig.java @@ -23,7 +23,7 @@ */ @SpringBootApplication @Import({ - // Import core Backstopper+Springboot1 support. + // Import core Backstopper+Springboot support. BackstopperSpringboot3WebMvcConfig.class, // Import the controller. SampleController.class diff --git a/testonly/testonly-springboot3_3-webflux/build.gradle b/testonly/testonly-springboot3_3-webflux/build.gradle index c89bb02..2feb71d 100644 --- a/testonly/testonly-springboot3_3-webflux/build.gradle +++ b/testonly/testonly-springboot3_3-webflux/build.gradle @@ -39,6 +39,6 @@ dependencies { ) } -// We're just running tests, not trying to stand up a real Springboot 2 server from gradle. +// We're just running tests, not trying to stand up a real Springboot 3 server from gradle. // Disable the bootJar task so gradle doesn't fall over. bootJar.enabled = false diff --git a/testonly/testonly-springboot3_3-webmvc/src/test/java/serverconfig/classpathscan/Springboot3_3WebMvcClasspathScanConfig.java b/testonly/testonly-springboot3_3-webmvc/src/test/java/serverconfig/classpathscan/Springboot3_3WebMvcClasspathScanConfig.java index b3c8f82..bf8fc44 100644 --- a/testonly/testonly-springboot3_3-webmvc/src/test/java/serverconfig/classpathscan/Springboot3_3WebMvcClasspathScanConfig.java +++ b/testonly/testonly-springboot3_3-webmvc/src/test/java/serverconfig/classpathscan/Springboot3_3WebMvcClasspathScanConfig.java @@ -21,7 +21,7 @@ */ @SpringBootApplication @ComponentScan(basePackages = { - // Component scan the core Backstopper+Springboot1 support. + // Component scan the core Backstopper+Springboot support. "com.nike.backstopper", // Component scan the controller. "testonly.componenttest.spring.reusable.controller" diff --git a/testonly/testonly-springboot3_3-webmvc/src/test/java/serverconfig/directimport/Springboot3_3WebMvcDirectImportConfig.java b/testonly/testonly-springboot3_3-webmvc/src/test/java/serverconfig/directimport/Springboot3_3WebMvcDirectImportConfig.java index 2c41567..db5b8a4 100644 --- a/testonly/testonly-springboot3_3-webmvc/src/test/java/serverconfig/directimport/Springboot3_3WebMvcDirectImportConfig.java +++ b/testonly/testonly-springboot3_3-webmvc/src/test/java/serverconfig/directimport/Springboot3_3WebMvcDirectImportConfig.java @@ -23,7 +23,7 @@ */ @SpringBootApplication @Import({ - // Import core Backstopper+Springboot1 support. + // Import core Backstopper+Springboot support. BackstopperSpringboot3WebMvcConfig.class, // Import the controller. SampleController.class From d7417d510a8fcea22fac6efb3fb602b8d5ee81f4 Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Wed, 11 Sep 2024 19:07:10 -0700 Subject: [PATCH 32/42] Reformat readme and user guide to get rid of long lines --- README.md | 191 ++++++++++++++------- USER_GUIDE.md | 454 +++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 491 insertions(+), 154 deletions(-) diff --git a/README.md b/README.md index 4d93c43..0533cf3 100644 --- a/README.md +++ b/README.md @@ -7,24 +7,41 @@ [![Code Coverage][codecov_img]][codecov] [![License][license img]][license] -**Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 7 and greater.** +**Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 7 and +greater.** ## TL;DR -Backstopper guarantees that a consistent error contract which you can define will be sent to callers *no matter the source of the error* when properly integrated into your API framework. No chance of undesired information leaking to the caller like stack traces, and callers can always rely on an error contract they can programmatically process. +Backstopper guarantees that a consistent error contract which you can define will be sent to callers *no matter the +source of the error* when properly integrated into your API framework. No chance of undesired information leaking to the +caller like stack traces, and callers can always rely on an error contract they can programmatically process. -If you prefer hands-on exploration rather than readmes and user guides, the [sample applications](#samples) provide concrete examples of using Backstopper that are simple, compact, and straightforward. +If you prefer hands-on exploration rather than readmes and user guides, the [sample applications](#samples) provide +concrete examples of using Backstopper that are simple, compact, and straightforward. -The rest of this readme is intended to get you oriented and running quickly, and the [User Guide](USER_GUIDE.md) contains more in-depth details. +The rest of this readme is intended to get you oriented and running quickly, and the [User Guide](USER_GUIDE.md) +contains more in-depth details. + ## Additional Features Overview -* Backstopper trivializes the task of creating and throwing errors that will be sent to the caller with the desired error contract, and allows you to gather all of your project-specific API error definitions in one place (e.g. as an enum) for easy access, modification, and addition. -* All errors are logged with full request context and error debugging info along with a UUID. The caller is sent the same UUID in the error response payload so you can instantly connect an individual error response with the application log message containing full debugging details. This makes the usual tedious process of debugging why an API error was shown to the caller simple and straightforward. -* Includes optional support for defining a common set of "core API errors" that can be shared via jar library so that all projects in your organization use the same set of API errors for common error cases. Individual projects are then free to focus on their project-specific API errors. -* Contains optional integration support for Java's JSR 303 Bean Validation system (`@NotNull`, `@Max`, etc) so that validation errors occurring due to invalid payloads sent by callers (among other use cases) will be automatically converted to your predefined set of project API errors. -* Integration libraries are included for several API frameworks already, and adding support for other frameworks is a relatively straightforward process. Backstopper is geared primarily towards HTTP-based APIs, but could be used in other circumstances if desired and without polluting your project with unwanted HTTP API dependencies. +* Backstopper trivializes the task of creating and throwing errors that will be sent to the caller with the desired + error contract, and allows you to gather all of your project-specific API error definitions in one place (e.g. as an + enum) for easy access, modification, and addition. +* All errors are logged with full request context and error debugging info along with a UUID. The caller is sent the + same UUID in the error response payload so you can instantly connect an individual error response with the application + log message containing full debugging details. This makes the usual tedious process of debugging why an API error was + shown to the caller simple and straightforward. +* Includes optional support for defining a common set of "core API errors" that can be shared via jar library so that + all projects in your organization use the same set of API errors for common error cases. Individual projects are then + free to focus on their project-specific API errors. +* Contains optional integration support for Java's JSR 303 Bean Validation system (`@NotNull`, `@Max`, etc) so that + validation errors occurring due to invalid payloads sent by callers (among other use cases) will be automatically + converted to your predefined set of project API errors. +* Integration libraries are included for several API frameworks already, and adding support for other frameworks is a + relatively straightforward process. Backstopper is geared primarily towards HTTP-based APIs, but could be used in + other circumstances if desired and without polluting your project with unwanted HTTP API dependencies. ### Barebones Example (assumes [framework integration](#quickstart_integration) is already done) @@ -38,7 +55,7 @@ public enum MyProjectError implements ApiError { // -- SNIP -- } ``` - + ##### 2a. Use JSR 303 Bean Validation to define object validation (optional) ``` java @@ -50,9 +67,10 @@ public class Payload { } ``` -##### --AND/OR-- +##### --AND/OR-- + ##### 2b. Throw errors manually anytime (doesn't have to be just for validation) - + ``` java throw ApiException.newBuilder() .withApiErrors(MyProjectError.INVALID_EMAIL_ADDRESS) @@ -105,50 +123,78 @@ throw ApiException.newBuilder() ``` + ### General-Purpose Modules * [backstopper-core](backstopper-core/) - The core library providing the majority of the Backstopper functionality. -* [backstopper-reusable-tests-junit5](backstopper-reusable-tests-junit5/) - There are some rules around defining your - project's set of API errors and conventions around integration with Java's JSR 303 Bean Validation system that must be followed for Backstopper to function at its best. This library contains reusable tests that are intended to be included in a Backstopper-enabled project's unit test suite to guarantee that these rules are adhered to and fail the build with descriptive unit test errors when those rules are violated. These are technically optional but ***highly*** recommended - integration is generally quick and easy. -* [backstopper-custom-validators](backstopper-custom-validators/) - This library contains JSR 303 Bean Validation annotations that have proven to be useful and reusable. These are entirely optional. They are also largely dependency free so this library is usable in non-Backstopper projects that utilize JSR 303 validations. -* [backstopper-jackson](backstopper-jackson/) - Contains a few utilities that help integrate Backstopper and Jackson for serializing error contracts to JSON. Optional. -* [backstopper-servlet-api](backstopper-servlet-api/) - Intermediate library intended to ease integration with Servlet-based frameworks. If you're building a Backstopper integration library for a Servlet-based framework that we don't already have support for then you'll want to use this. -* [nike-internal-util](nike-internal-util/) - A small utilities library that provides some reusable helper methods and classes. It is "internal" in the sense that it is not intended to be directly pulled in and used by non-Nike projects. That said you can use it if you want to, just be aware that some liberties might be taken regarding version numbers, backwards compatibility, etc over time when compared with libraries specifically intended for public consumption. +* [backstopper-reusable-tests-junit5](backstopper-reusable-tests-junit5/) - There are some rules around defining your + project's set of API errors and conventions around integration with Java's JSR 303 Bean Validation system that must be + followed for Backstopper to function at its best. This library contains reusable tests that are intended to be + included in a Backstopper-enabled project's unit test suite to guarantee that these rules are adhered to and fail the + build with descriptive unit test errors when those rules are violated. These are technically optional but ***highly*** + recommended - integration is generally quick and easy. +* [backstopper-custom-validators](backstopper-custom-validators/) - This library contains JSR 303 Bean Validation + annotations that have proven to be useful and reusable. These are entirely optional. They are also largely dependency + free so this library is usable in non-Backstopper projects that utilize JSR 303 validations. +* [backstopper-jackson](backstopper-jackson/) - Contains a few utilities that help integrate Backstopper and Jackson for + serializing error contracts to JSON. Optional. +* [backstopper-servlet-api](backstopper-servlet-api/) - Intermediate library intended to ease integration with + Servlet-based frameworks. If you're building a Backstopper integration library for a Servlet-based framework that we + don't already have support for then you'll want to use this. +* [nike-internal-util](nike-internal-util/) - A small utilities library that provides some reusable helper methods and + classes. It is "internal" in the sense that it is not intended to be directly pulled in and used by non-Nike projects. + That said you can use it if you want to, just be aware that some liberties might be taken regarding version numbers, + backwards compatibility, etc over time when compared with libraries specifically intended for public consumption. -### Framework-Specific Modules + +### Framework-Specific Modules // TODO javax-to-jakarta: Explain why jaxrs and jersey3 are not currently supported, but could be if there's interest. -* [backstopper-jaxrs](backstopper-jaxrs) - Integration library for JAX-RS. If you want to integrate Backstopper into a JAX-RS project other than Jersey then start here (see below for the Jersey-specific modules). -* [backstopper-jersey1](backstopper-jersey1/) - Integration library for the Jersey 1 framework. If you want to integrate Backstopper into a project running in Jersey 1 then start here. There is a [Jersey 1 sample project](samples/sample-jersey1/) complete with integration tests you can use as an example. -* [backstopper-jersey2](backstopper-jersey2/) - Integration library for the Jersey 2 framework. If you want to integrate Backstopper into a project running in Jersey 2 then start here. There is a [Jersey 2 sample project](samples/sample-jersey2/) complete with integration tests you can use as an example. -* [backstopper-spring-web-mvc](backstopper-spring-web-mvc/) - Base Integration library for the Spring Web MVC (Servlet) -framework. If you want to integrate Backstopper into a project running in Spring Web MVC then start here. Works for -both Spring 4 and Spring 5, and used as a foundation for Backstopper support in Spring Boot 1 and 2 (when using Web -MVC with Spring Boot - see below for links to Spring Boot specific integration modules). There is a -[Spring Web MVC sample project](samples/sample-spring-web-mvc/) complete with integration tests you can use as an -example. +* [backstopper-jaxrs](backstopper-jaxrs) - Integration library for JAX-RS. If you want to integrate Backstopper into a + JAX-RS project other than Jersey then start here (see below for the Jersey-specific modules). +* [backstopper-jersey1](backstopper-jersey1/) - Integration library for the Jersey 1 framework. If you want to integrate + Backstopper into a project running in Jersey 1 then start here. There is + a [Jersey 1 sample project](samples/sample-jersey1/) complete with integration tests you can use as an example. +* [backstopper-jersey2](backstopper-jersey2/) - Integration library for the Jersey 2 framework. If you want to integrate + Backstopper into a project running in Jersey 2 then start here. There is + a [Jersey 2 sample project](samples/sample-jersey2/) complete with integration tests you can use as an example. +* [backstopper-spring-web-mvc](backstopper-spring-web-mvc/) - Base Integration library for the Spring Web MVC (Servlet) + framework. If you want to integrate Backstopper into a project running in Spring Web MVC then start here. Works for + both Spring 4 and Spring 5, and used as a foundation for Backstopper support in Spring Boot 1 and 2 (when using Web + MVC with Spring Boot - see below for links to Spring Boot specific integration modules). There is a + [Spring Web MVC sample project](samples/sample-spring-web-mvc/) complete with integration tests you can use as an + example. * [backstopper-spring-web-flux](backstopper-spring-web-flux/) - Integration library for the Spring WebFlux (Netty) -framework. If you want to integrate Backstopper into a project running in Spring WebFlux then start here. There is a -[Spring Boot 2 WebFlux sample project](samples/sample-spring-boot2-webflux/) complete with integration tests you can -use as an example. -* [backstopper-spring-boot1](backstopper-spring-boot1/) - Integration library for the Spring Boot 1 framework. -If you want to integrate Backstopper into a project running in Spring Boot 1 then start here. There is a -[Spring Boot 1 sample project](samples/sample-spring-boot1/) complete with integration tests you can use as an example. -* [backstopper-spring-boot2-webmvc](backstopper-spring-boot2-webmvc/) - Integration library for the Spring Boot 2 -framework *if and only if you're using the Spring Web MVC Servlet runtime* (if you're running a Spring -Boot 2 + WebFlux Netty application, then you do not want this library and should use -[backstopper-spring-web-flux](backstopper-spring-web-flux) instead). If you want to integrate Backstopper into a -project running in Spring Boot 2 + Web MVC then start here. There is a -[Spring Boot 2 Web MVC sample project](samples/sample-spring-boot2-webmvc/) complete with integration tests you -can use as an example. - + framework. If you want to integrate Backstopper into a project running in Spring WebFlux then start here. There is a + [Spring Boot 2 WebFlux sample project](samples/sample-spring-boot2-webflux/) complete with integration tests you can + use as an example. +* [backstopper-spring-boot1](backstopper-spring-boot1/) - Integration library for the Spring Boot 1 framework. + If you want to integrate Backstopper into a project running in Spring Boot 1 then start here. There is a + [Spring Boot 1 sample project](samples/sample-spring-boot1/) complete with integration tests you can use as an + example. +* [backstopper-spring-boot2-webmvc](backstopper-spring-boot2-webmvc/) - Integration library for the Spring Boot 2 + framework *if and only if you're using the Spring Web MVC Servlet runtime* (if you're running a Spring + Boot 2 + WebFlux Netty application, then you do not want this library and should use + [backstopper-spring-web-flux](backstopper-spring-web-flux) instead). If you want to integrate Backstopper into a + project running in Spring Boot 2 + Web MVC then start here. There is a + [Spring Boot 2 Web MVC sample project](samples/sample-spring-boot2-webmvc/) complete with integration tests you + can use as an example. + + ### Framework Integration Sample Applications - -Note that the sample apps are an excellent source for framework integration examples, but they are also very helpful for giving you an overview and exploring what you can do with Backstopper regardless of framework: how to create and throw errors, how they show up for the caller in the response, what Backstopper outputs in the application logs when errors occur, and how to find the relevant log message given a specific error response. The `VerifyExpectedErrorsAreReturnedComponentTest` component tests in the sample apps exercise a large portion of Backstopper's functionality - you can learn a lot by running that component test, seeing what the sample app returns in the error responses, and exploring the associated endpoints and framework configuration in the sample apps to see how it all fits together. - + +Note that the sample apps are an excellent source for framework integration examples, but they are also very helpful for +giving you an overview and exploring what you can do with Backstopper regardless of framework: how to create and throw +errors, how they show up for the caller in the response, what Backstopper outputs in the application logs when errors +occur, and how to find the relevant log message given a specific error response. The +`VerifyExpectedErrorsAreReturnedComponentTest` component tests in the sample apps exercise a large portion of +Backstopper's functionality - you can learn a lot by running that component test, seeing what the sample app returns in +the error responses, and exploring the associated endpoints and framework configuration in the sample apps to see how it +all fits together. + * [samples/sample-jersey1](samples/sample-jersey1/) * [samples/sample-jersey2](samples/sample-jersey2/) * [samples/sample-spring-web-mvc](samples/sample-spring-web-mvc/) @@ -157,28 +203,51 @@ Note that the sample apps are an excellent source for framework integration exam * [samples/sample-spring-boot2-webflux](samples/sample-spring-boot2-webflux/) + ## Quickstart -Getting started is a matter of integrating Backstopper into your project and learning how to use its features. The following sections will help guide you in getting started, and the [sample applications](#samples) should be consulted for concrete examples. +Getting started is a matter of integrating Backstopper into your project and learning how to use its features. The +following sections will help guide you in getting started, and the [sample applications](#samples) should be consulted +for concrete examples. + ### Quickstart - Integration -The first thing to do is see if there is a [framework integration plugin library](#framework_modules) already created for the framework you're using. If so then refer to that framework-specific library's readme as well as its [sample project](#samples) to learn how to integrate Backstopper into your project. +The first thing to do is see if there is a [framework integration plugin library](#framework_modules) already created +for the framework you're using. If so then refer to that framework-specific library's readme as well as +its [sample project](#samples) to learn how to integrate Backstopper into your project. -If a framework-specific plugin library does not already exist for your project then you'll need to create your own integration. If the result is potentially reusable for others using the same framework then please consider [contributing](CONTRIBUTING.md) it back to the Backstopper project so others can benefit! The [new framework integrations](USER_GUIDE.md#new_framework_integrations) section has full details, and in particular the [pseudo-code section](USER_GUIDE.md#new_framework_pseudocode) should give you a quick idea of what is required. +If a framework-specific plugin library does not already exist for your project then you'll need to create your own +integration. If the result is potentially reusable for others using the same framework then please +consider [contributing](CONTRIBUTING.md) it back to the Backstopper project so others can benefit! +The [new framework integrations](USER_GUIDE.md#new_framework_integrations) section has full details, and in particular +the [pseudo-code section](USER_GUIDE.md#new_framework_pseudocode) should give you a quick idea of what is required. -**IMPORTANT NOTE: Your project integration should not be considered complete until you have added and enabled the reusable unit tests that enforce Backstopper rules and conventions.** See the [Reusable Unit Tests for Enforcing Backstopper Rules and Conventions](USER_GUIDE.md#reusable_tests) section of the User Guide for information on setting these up. +**IMPORTANT NOTE: Your project integration should not be considered complete until you have added and enabled the +reusable unit tests that enforce Backstopper rules and conventions.** See +the [Reusable Unit Tests for Enforcing Backstopper Rules and Conventions](USER_GUIDE.md#reusable_tests) section of the +User Guide for information on setting these up. + ### Quickstart - Usage -Once your project is properly integrated with Backstopper a large portion of errors should be handled for you (framework errors, errors resulting from validation of incoming payloads, etc), however for most API projects you'll need to throw errors or interact with Backstopper in other situations. Again, the [sample projects](#samples) are excellent for showing how this is done in practice, but here are a few common use cases and how to solve them: +Once your project is properly integrated with Backstopper a large portion of errors should be handled for you (framework +errors, errors resulting from validation of incoming payloads, etc), however for most API projects you'll need to throw +errors or interact with Backstopper in other situations. Again, the [sample projects](#samples) are excellent for +showing how this is done in practice, but here are a few common use cases and how to solve them: + ##### Defining a set of `ApiError`s -Defining groups of `ApiError`s as enums has proven to be a useful pattern. Normally you'd want to break `ApiError`s out into a group of "core errors" that you could share with projects across your organization (see `SampleCoreApiError` for an example) and different sets of `ApiError`s for each individual project (see any of the [sample application](#samples) `SampleProjectApiError` classes for an example). For the purpose of this example here is a mishmash showing how to define an enum of errors with different properties (basic code/message/http-status errors, "mirror" errors, and errors with metadata): +Defining groups of `ApiError`s as enums has proven to be a useful pattern. Normally you'd want to break `ApiError`s out +into a group of "core errors" that you could share with projects across your organization (see `SampleCoreApiError` for +an example) and different sets of `ApiError`s for each individual project (see any of the [sample application](#samples) +`SampleProjectApiError` classes for an example). For the purpose of this example here is a mishmash showing how to +define an enum of errors with different properties (basic code/message/http-status errors, "mirror" errors, and errors +with metadata): ``` java public enum MyProjectApiError implements ApiError { @@ -230,11 +299,17 @@ public enum MyProjectApiError implements ApiError { ``` + ##### Defining a `ProjectApiErrors` for your project -Backstopper needs a `ProjectApiErrors` defined for each project in order to work. If possible you should create an abstract base class that is setup with the core errors for your organization - see `SampleProjectApiErrorsBase` for an example. Then each individual project would simply need to extend the base class and fill in the project-specific set of `ApiError`s and the error range it's using. See any of the [sample application](#samples) `SampleProjectApiErrorsImpl` classes for an example. The javadocs for `ProjectApiErrors` contains in-depth information as well. +Backstopper needs a `ProjectApiErrors` defined for each project in order to work. If possible you should create an +abstract base class that is setup with the core errors for your organization - see `SampleProjectApiErrorsBase` for an +example. Then each individual project would simply need to extend the base class and fill in the project-specific set of +`ApiError`s and the error range it's using. See any of the [sample application](#samples) `SampleProjectApiErrorsImpl` +classes for an example. The javadocs for `ProjectApiErrors` contains in-depth information as well. + ##### Manually throwing an arbitrary error with full control over the resulting error contract, response headers, and logging info ``` java @@ -255,10 +330,12 @@ throw ApiException.newBuilder() ``` + ##### Creating a custom `ApiExceptionHandlerListener` to handle a typed exception -This is only really necessary if you can't (or don't want to) throw an `ApiException` and need Backstopper to properly handle a typed exception it wouldn't otherwise know about. Many projects never need to do this. - +This is only really necessary if you can't (or don't want to) throw an `ApiException` and need Backstopper to properly +handle a typed exception it wouldn't otherwise know about. Many projects never need to do this. + ``` java public static class MyFrameworkExceptionHandlerListener implements ApiExceptionHandlerListener { @Override @@ -286,14 +363,16 @@ public static class MyFrameworkExceptionHandlerListener implements ApiExceptionH } } ``` - -After defining a new `ApiExceptionHandlerListener` you'll need to register it with the `ApiExceptionHandlerBase` running your Backstopper system. This is a procedure that is often different for each framework integration. + +After defining a new `ApiExceptionHandlerListener` you'll need to register it with the `ApiExceptionHandlerBase` running +your Backstopper system. This is a procedure that is often different for each framework integration. ## User Guide For further details please consult the [User Guide](USER_GUIDE.md). + ## License Backstopper is released under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 2e38384..eeb0928 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -1,19 +1,21 @@ # Backstopper User Guide -**Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 7 and greater.** +**Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 7 and +greater.** The [base project README](README.md) covers Backstopper at a surface level, including: * [Overview](README.md#overview) -* Repository library modules (both [general](README.md#general_modules) and [framework-specific](README.md#framework_modules)) +* Repository library modules (both [general](README.md#general_modules) + and [framework-specific](README.md#framework_modules)) * Framework-specific [sample applications](README.md#samples) * [Quickstart](README.md#quickstart). -This User Guide is for a more in-depth exploration of Backstopper. +This User Guide is for a more in-depth exploration of Backstopper. -## Table of Contents +## Table of Contents * [Backstopper Key Components](#key_components) * [JSR 303 Bean Validation Support](#jsr_303_support) @@ -22,127 +24,279 @@ This User Guide is for a more in-depth exploration of Backstopper. * [Backstopper Conventions for JSR 303 Bean Validation Support](#jsr303_conventions) * [Reusable Unit Tests for Enforcing Backstopper Rules and Conventions](#reusable_tests) * [Unit Tests to Guarantee JSR 303 Annotation Message Naming Convention Conformance](#setup_jsr303_convention_unit_tests) - * [Unit Test to Verify Your `ProjectApiErrors` Instance Conforms to Requirements](#setup_project_api_errors_unit_test) + * [Unit Test to Verify Your + `ProjectApiErrors` Instance Conforms to Requirements](#setup_project_api_errors_unit_test) * [Creating New Framework Integrations](#new_framework_integrations) * [New Framework Integration Pseudocode](#new_framework_pseudocode) * [Pseudocode Explanation and Further Details](#new_framework_pseudocode_explanation) * [Backstopper Project Meta-Information](#meta_info) * [Motivation - Why Does Backstopper Exist?](#motivation) * [Goals](#key_goals) - * [Philosophies](#key_philosophies) + * [Philosophies](#key_philosophies) -## Backstopper Key Components -This section should give you a basis for understanding the components of Backstopper and how they interact. See the javadocs for each component for further details. The following list is in rough conceptual order of "closest to error inception" -> "closest to framework response" as the exception flows through the Backstopper system. +## Backstopper Key Components -* **`com.nike.backstopper.exception.*` exceptions** - Predefined typed exceptions that Backstopper knows how to handle that you can throw in your project to trigger desired behavior (assuming you include the basic `ApiExceptionHandlerListener`s when configuring Backstopper). In particular: - * `ApiException` - Generic exception that gives you full control and flexibility over what errors and response headers are returned to the caller and what gets logged in the application logs. **When in doubt, throw one of these.** - * `WrapperException` - Simple wrapper exception that you can use when you want the error handling system to handle the `WrapperException.getCause()` of the wrapper rather than the wrapper itself, but still log the entire stack trace (including the wrapper). This is often necessary in asynchronous scenarios where you want to add stack trace info for the logs but don't want to obscure the true cause of the error. - * `ClientDataValidationError` and `ServersideValidationError` - Used when you want to have Backstopper handle JSR 303 Bean Validation violations. Rather than create and throw these yourself you should use `ClientDataValidationService` and `FailFastServersideValidationService` to inspect objects needing validation and construct and throw the appropriate exception. See the javadocs on these exceptions and their associated services for more info. - * `NetworkExceptionBase` and related exceptions in `com.nike.backstopper.exception.network` - These are intended to help handle errors that occur when your application communicates with downstream services. You can create adapters for whatever client framework you use to convert client errors to these exceptions shortly after they are thrown so that Backstopper does the right thing without needing to create and register custom `ApiExceptionHandlerListener`s. See the javadocs on these exception classes for more details on how they should be used, and `DownstreamNetworkExceptionHandlerListener` for details on how they are used and interpreted by Backstopper. -* **`ApiError`** - The core unit of currency in Backstopper. This is a simple interface that defines an API error. It is very basic and closely resembles the data returned by the default error contract. Each `ApiError` contains the following information: +This section should give you a basis for understanding the components of Backstopper and how they interact. See the +javadocs for each component for further details. The following list is in rough conceptual order of "closest to error +inception" -> "closest to framework response" as the exception flows through the Backstopper system. + +* **`com.nike.backstopper.exception.*` exceptions** - Predefined typed exceptions that Backstopper knows how to handle + that you can throw in your project to trigger desired behavior (assuming you include the basic + `ApiExceptionHandlerListener`s when configuring Backstopper). In particular: + * `ApiException` - Generic exception that gives you full control and flexibility over what errors and response + headers are returned to the caller and what gets logged in the application logs. **When in doubt, throw one of + these.** + * `WrapperException` - Simple wrapper exception that you can use when you want the error handling system to handle + the `WrapperException.getCause()` of the wrapper rather than the wrapper itself, but still log the entire stack + trace (including the wrapper). This is often necessary in asynchronous scenarios where you want to add stack trace + info for the logs but don't want to obscure the true cause of the error. + * `ClientDataValidationError` and `ServersideValidationError` - Used when you want to have Backstopper handle JSR + 303 Bean Validation violations. Rather than create and throw these yourself you should use + `ClientDataValidationService` and `FailFastServersideValidationService` to inspect objects needing validation and + construct and throw the appropriate exception. See the javadocs on these exceptions and their associated services + for more info. + * `NetworkExceptionBase` and related exceptions in `com.nike.backstopper.exception.network` - These are intended to + help handle errors that occur when your application communicates with downstream services. You can create adapters + for whatever client framework you use to convert client errors to these exceptions shortly after they are thrown + so that Backstopper does the right thing without needing to create and register custom + `ApiExceptionHandlerListener`s. See the javadocs on these exception classes for more details on how they should be + used, and `DownstreamNetworkExceptionHandlerListener` for details on how they are used and interpreted by + Backstopper. +* **`ApiError`** - The core unit of currency in Backstopper. This is a simple interface that defines an API error. It is + very basic and closely resembles the data returned by the default error contract. Each `ApiError` contains the + following information: * A project/business error code (not to be confused with HTTP status code). * A human-readable message. * The HTTP status code that the error should map to. - * A human-readable name for the error that is used in the [JSR 303 Bean Validation convention](#jsr303_conventions) to link up a JSR 303 constraint violation to an `ApiError`, and the name will always show up in the log message output whenever that error is returned to the caller. There should only ever be one `ApiError` with a given name in a given project's `ProjectApiErrors`. - * Note that there is a `ApiErrorWithMetadata` class to allow you to wrap an existing `ApiError` with extra metadata without having to redefine the original with copy/pasted values. -* **`ProjectApiErrors`** - Contains information about a given project's errors and how it wants certain things handled. It provides the collections of `ApiErrors` associated with the project - both the "core errors" (a set of core reusable errors intended to be shared among multiple projects in an organization) as well as the "project-specific errors". `ProjectApiErrors` also defines a series of methods intended to indicate to `ApiExceptionHandlerListener`s which `ApiError` should be used in certain situations like resource not found, unsupported media type, generic 400, generic 500, generic 503, etc. -* **`ProjectSpecificErrorCodeRange`** - An interface returned by `ProjectApiErrors.getProjectSpecificErrorCodeRange()` that defines the error code range that all project-specific `ApiError`s must fall into. Core errors are excluded from this restriction. This is an optional feature - if you don't care what error codes are used in your project you can use `ProjectSpecificErrorCodeRange.ALLOW_ALL_ERROR_CODES` to allow any error code, although if you are in an organization that has many services (e.g. microservice environment) then this mechanism is an easy way to make sure different projects don't use the same error codes for fundamentally different errors. See the javadocs on this interface for more information on how to easily enforce these rules across projects. -* **`ApiExceptionHandlerListener`** - These are used in a plugin fashion by the `ApiExceptionHandlerBase` for your project to handle "known" exceptions. As new typed exceptions or error cases come up you can create a new `ApiExceptionHandlerListener` that knows how to handle the new exception and returns a `ApiExceptionHandlerListenerResult` (see below) with information on what to do, and register it with your project's `ApiExceptionHandlerBase`. At that point any time the new exception flows through your system it will be converted to the desired error contract for the caller exactly as you specified, and can be shared with other projects that need to handle the new exception the same way. There are a few framework-independent listeners that are relevant to all Backstopper-enabled projects, and each new framework integration usually defines one or more listeners that knows how to convert framework-specific exceptions into the appropriate `ApiError`s (provided by a project's `ProjectApiErrors`). -* **`ApiExceptionHandlerListenerResult`** - A class defining the result of an `ApiExceptionHandlerListener` inspection of a given exception. If the listener doesn't know what to do with the exception it will return `ApiExceptionHandlerListenerResult.ignoreResponse()` to say that a different listener should handle it. If the listener wants to handle the response it will return `ApiExceptionHandlerListenerResult.handleResponse(...)` with the set of `ApiError`s that should be associated with the exception, (optionally) any extra response headers that you want to be included in the response, and (optionally) any extra details that should show up in the application logs when the error's log message is output. -* **`ApiExceptionHandlerUtils`** - This is used by various Backstopper components and provides numerous helper methods. This is your hook for several pieces of Backstopper behavior, including the format of the message that gets logged when an error is handled. Although it is a "utils" class none of the methods are static so you are free to override anything you wish. -* **`ApiExceptionHandlerBase`** - Handles all "known" exceptions and conditions by running the exception through the project's list of `ApiExceptionHandlerListener`s. If a listener indicates it wants to handle the exception then the result is converted to a `ErrorResponseInfo` and all information about the request and error context is logged in a single log message which is tagged with a UUID that is also included in the response to the caller for trivial lookup of all the relevant debugging information whenever an error is sent to the caller. -* **`UnhandledExceptionHandlerBase`** - Serves as the final safety net catch-all for anything not handled successfully by `ApiExceptionHandlerBase`. Works in a similar way to `ApiExceptionHandlerBase` by logging all context about the error and request, except it is designed to always return a generic service exception and *never* fail. -* **`ErrorResponseInfo`** - This is returned by `ApiExceptionHandlerBase` and `UnhandledExceptionHandlerBase` and indicates to the framework what it should return to the caller. Includes the HTTP status code that should be used, a map of extra headers that should be added to the response, and the response payload in a format suitable for the framework. - -There are other classes and components in Backstopper but the above are the major touchpoints and should give you a good grounding for further exploration. + * A human-readable name for the error that is used in the [JSR 303 Bean Validation convention](#jsr303_conventions) + to link up a JSR 303 constraint violation to an `ApiError`, and the name will always show up in the log message + output whenever that error is returned to the caller. There should only ever be one `ApiError` with a given name + in a given project's `ProjectApiErrors`. + * Note that there is a `ApiErrorWithMetadata` class to allow you to wrap an existing `ApiError` with extra metadata + without having to redefine the original with copy/pasted values. +* **`ProjectApiErrors`** - Contains information about a given project's errors and how it wants certain things handled. + It provides the collections of `ApiErrors` associated with the project - both the "core errors" (a set of core + reusable errors intended to be shared among multiple projects in an organization) as well as the "project-specific + errors". `ProjectApiErrors` also defines a series of methods intended to indicate to `ApiExceptionHandlerListener`s + which `ApiError` should be used in certain situations like resource not found, unsupported media type, generic 400, + generic 500, generic 503, etc. +* **`ProjectSpecificErrorCodeRange`** - An interface returned by `ProjectApiErrors.getProjectSpecificErrorCodeRange()` + that defines the error code range that all project-specific `ApiError`s must fall into. Core errors are excluded from + this restriction. This is an optional feature - if you don't care what error codes are used in your project you can + use `ProjectSpecificErrorCodeRange.ALLOW_ALL_ERROR_CODES` to allow any error code, although if you are in an + organization that has many services (e.g. microservice environment) then this mechanism is an easy way to make sure + different projects don't use the same error codes for fundamentally different errors. See the javadocs on this + interface for more information on how to easily enforce these rules across projects. +* **`ApiExceptionHandlerListener`** - These are used in a plugin fashion by the `ApiExceptionHandlerBase` for your + project to handle "known" exceptions. As new typed exceptions or error cases come up you can create a new + `ApiExceptionHandlerListener` that knows how to handle the new exception and returns a + `ApiExceptionHandlerListenerResult` (see below) with information on what to do, and register it with your project's + `ApiExceptionHandlerBase`. At that point any time the new exception flows through your system it will be converted to + the desired error contract for the caller exactly as you specified, and can be shared with other projects that need to + handle the new exception the same way. There are a few framework-independent listeners that are relevant to all + Backstopper-enabled projects, and each new framework integration usually defines one or more listeners that knows how + to convert framework-specific exceptions into the appropriate `ApiError`s (provided by a project's + `ProjectApiErrors`). +* **`ApiExceptionHandlerListenerResult`** - A class defining the result of an `ApiExceptionHandlerListener` inspection + of a given exception. If the listener doesn't know what to do with the exception it will return + `ApiExceptionHandlerListenerResult.ignoreResponse()` to say that a different listener should handle it. If the + listener wants to handle the response it will return `ApiExceptionHandlerListenerResult.handleResponse(...)` with the + set of `ApiError`s that should be associated with the exception, (optionally) any extra response headers that you want + to be included in the response, and (optionally) any extra details that should show up in the application logs when + the error's log message is output. +* **`ApiExceptionHandlerUtils`** - This is used by various Backstopper components and provides numerous helper methods. + This is your hook for several pieces of Backstopper behavior, including the format of the message that gets logged + when an error is handled. Although it is a "utils" class none of the methods are static so you are free to override + anything you wish. +* **`ApiExceptionHandlerBase`** - Handles all "known" exceptions and conditions by running the exception through the + project's list of `ApiExceptionHandlerListener`s. If a listener indicates it wants to handle the exception then the + result is converted to a `ErrorResponseInfo` and all information about the request and error context is logged in a + single log message which is tagged with a UUID that is also included in the response to the caller for trivial lookup + of all the relevant debugging information whenever an error is sent to the caller. +* **`UnhandledExceptionHandlerBase`** - Serves as the final safety net catch-all for anything not handled successfully + by `ApiExceptionHandlerBase`. Works in a similar way to `ApiExceptionHandlerBase` by logging all context about the + error and request, except it is designed to always return a generic service exception and *never* fail. +* **`ErrorResponseInfo`** - This is returned by `ApiExceptionHandlerBase` and `UnhandledExceptionHandlerBase` and + indicates to the framework what it should return to the caller. Includes the HTTP status code that should be used, a + map of extra headers that should be added to the response, and the response payload in a format suitable for the + framework. + +There are other classes and components in Backstopper but the above are the major touchpoints and should give you a good +grounding for further exploration. [[back to table of contents]][toc] + ## JSR 303 Bean Validation Support -Guaranteeing that JSR 303 Bean Validation violations *will* map to a specific API error (represented by the `ApiError` values returned by each project's `ProjectApiErrors` instance) is one of the major benefits of Backstopper. It requires throwing an exception that Backstopper knows how to handle that wraps the JSR 303 constraint violations, following a specific message naming convention when declaring constraint annotations, and to be safe you should set up a few unit tests that will catch any JSR 303 constraint annotation declarations that don't conform to the naming convention due to typos, copy-paste errors, or any other reason. The following sections cover these concerns and describe how to enable JSR 303 Bean Validation support in Backstopper. - -(Note that JSR 303 integration is optional - you can successfully run Backstopper in an environment that does not include any JSR 303 support and it will work just fine.) +Guaranteeing that JSR 303 Bean Validation violations *will* map to a specific API error (represented by the `ApiError` +values returned by each project's `ProjectApiErrors` instance) is one of the major benefits of Backstopper. It requires +throwing an exception that Backstopper knows how to handle that wraps the JSR 303 constraint violations, following a +specific message naming convention when declaring constraint annotations, and to be safe you should set up a few unit +tests that will catch any JSR 303 constraint annotation declarations that don't conform to the naming convention due to +typos, copy-paste errors, or any other reason. The following sections cover these concerns and describe how to enable +JSR 303 Bean Validation support in Backstopper. + +(Note that JSR 303 integration is optional - you can successfully run Backstopper in an environment that does not +include any JSR 303 support and it will work just fine.) + ### Enabling Basic JSR 303 Validation -Enabling JSR 303 in your application is outside the scope of this User Guide - different containers and frameworks set it up in different ways and may depend on the JSR 303 implementation you're using. It is usually not too difficult - consult your framework's docs and google for tutorials and examples to see if your framework has built-in JSR 303 support (if it does and we already have a [sample application](README.md#samples) for your framework you can consult the sample app for an example on how to enable and use the JSR 303 support in Backstopper for that framework). +Enabling JSR 303 in your application is outside the scope of this User Guide - different containers and frameworks set +it up in different ways and may depend on the JSR 303 implementation you're using. It is usually not too difficult - +consult your framework's docs and google for tutorials and examples to see if your framework has built-in JSR 303 +support (if it does and we already have a [sample application](README.md#samples) for your framework you can consult the +sample app for an example on how to enable and use the JSR 303 support in Backstopper for that framework). -In the worst case you can always manually create a `javax.validation.Validator` and use `ClientDataValidationService` to validate your objects. Just make sure a JSR 303 implementation library is on your classpath (e.g. [Hibernate Validator](http://hibernate.org/validator/) or [Apache BVal](http://bval.apache.org/)) and call `javax.validation.Validation.buildDefaultValidatorFactory().getValidator()`. `Validator` is thread safe so you only technically have to create one and can share it around. +In the worst case you can always manually create a `jakarta.validation.Validator` and use `ClientDataValidationService` +to validate your objects. Just make sure a JSR 303 implementation library is on your classpath ( +e.g. [Hibernate Validator](http://hibernate.org/validator/) or [Apache BVal](http://bval.apache.org/)) and call +`jakarta.validation.Validation.buildDefaultValidatorFactory().getValidator()`. `Validator` is thread safe so you only +technically have to create one and can share it around. [[back to table of contents]][toc] -### Throwing JSR 303 Violations in a Backstopper-Compatible Way -The JSR 303 `javax.validation.Validator` object does not throw exceptions when it sees an object that violates validation constraints, instead it returns a `Set` of `ConstraintViolation`s. Backstopper works by intercepting exceptions and translating them into `ApiError`s, so we need a mechanism to bridge what the JSR 303 `Validator` returns and what Backstopper needs: `ClientDataValidationError`. `ClientDataValidationError` is an exception that Backstopper knows how to handle and it wraps the `ConstraintViolations` returned by the JSR 303 `Validator` so that Backstopper can convert them to your project's `ApiError`s. It's recommended that you use `ClientDataValidationService` for validating your objects since it will automatically throw an appropriate `ClientDataValidationError` without you needing to worry about it. But if you can't or don't want to use that service you can create and throw `ClientDataValidationError` manually yourself. - -There is a similar exception and service for dealing with validating internal server logic (e.g. downstream requests to other services) where if a validation violation occurs you want the original caller to receive a generic HTTP status 5xx type service error since the error had nothing to do with data the caller sent you and they can't do anything about it, but at the same time you want all the debugging information about the JSR 303 errors to show up in the logs. The exception to use in these cases is `ServersideValidationError` and the service that automatically validates objects and throws that exception when it finds violations is `FailFastServersideValidationService`. - -Finally you'll need `ClientDataValidationErrorHandlerListener` and `ServersideValidationErrorHandlerListener` to be registered with Backstopper in your project for these exceptions to be picked up and handled correctly. These are default listeners that should be included with any Backstopper integration (even if you don't plan on using any JSR 303 functionality in your project), so you generally don't need to worry about adding them. +### Throwing JSR 303 Violations in a Backstopper-Compatible Way -Note that some frameworks have different mechanisms for validation where they throw their own typed exceptions that are wrappers around JSR 303 violations. In these cases a framework-specific listener must be provided that knows how to translate the framework exception into `ApiError`s using the Backstopper JSR 303 naming conventions (see below for details on the naming conventions). `ConventionBasedSpringValidationErrorToApiErrorHandlerListener` in the [Spring Web MVC](backstopper-spring-web-mvc/) Backstopper plugin library is one example of this use case. +The JSR 303 `jakarta.validation.Validator` object does not throw exceptions when it sees an object that violates +validation constraints, instead it returns a `Set` of `ConstraintViolation`s. Backstopper works by intercepting +exceptions and translating them into `ApiError`s, so we need a mechanism to bridge what the JSR 303 `Validator` returns +and what Backstopper needs: `ClientDataValidationError`. `ClientDataValidationError` is an exception that Backstopper +knows how to handle and it wraps the `ConstraintViolations` returned by the JSR 303 `Validator` so that Backstopper can +convert them to your project's `ApiError`s. It's recommended that you use `ClientDataValidationService` for validating +your objects since it will automatically throw an appropriate `ClientDataValidationError` without you needing to worry +about it. But if you can't or don't want to use that service you can create and throw `ClientDataValidationError` +manually yourself. + +There is a similar exception and service for dealing with validating internal server logic (e.g. downstream requests to +other services) where if a validation violation occurs you want the original caller to receive a generic HTTP status 5xx +type service error since the error had nothing to do with data the caller sent you and they can't do anything about it, +but at the same time you want all the debugging information about the JSR 303 errors to show up in the logs. The +exception to use in these cases is `ServersideValidationError` and the service that automatically validates objects and +throws that exception when it finds violations is `FailFastServersideValidationService`. + +Finally you'll need `ClientDataValidationErrorHandlerListener` and `ServersideValidationErrorHandlerListener` to be +registered with Backstopper in your project for these exceptions to be picked up and handled correctly. These are +default listeners that should be included with any Backstopper integration (even if you don't plan on using any JSR 303 +functionality in your project), so you generally don't need to worry about adding them. + +Note that some frameworks have different mechanisms for validation where they throw their own typed exceptions that are +wrappers around JSR 303 violations. In these cases a framework-specific listener must be provided that knows how to +translate the framework exception into `ApiError`s using the Backstopper JSR 303 naming conventions (see below for +details on the naming conventions). `ConventionBasedSpringValidationErrorToApiErrorHandlerListener` in +the [Spring Web MVC](backstopper-spring-web-mvc/) Backstopper plugin library is one example of this use case. [[back to table of contents]][toc] + ### Backstopper Conventions for JSR 303 Bean Validation Support -In order for the listeners to be able to accurately map any given JSR 303 annotation violation to a specific `ApiError` for displaying to the client the JSR 303 constraint annotations must be defined with a specific message naming convention: the `message` attribute of the JSR 303 constraint annotation *must* match the `ApiError.getName()` of the `ApiError` instance you want it mapped to, and that `ApiError` must be contained in your project's `ProjectApiErrors.getProjectApiErrors()` collection. +In order for the listeners to be able to accurately map any given JSR 303 annotation violation to a specific `ApiError` +for displaying to the client the JSR 303 constraint annotations must be defined with a specific message naming +convention: the `message` attribute of the JSR 303 constraint annotation *must* match the `ApiError.getName()` of the +`ApiError` instance you want it mapped to, and that `ApiError` must be contained in your project's +`ProjectApiErrors.getProjectApiErrors()` collection. -For example, if your project's `ApiError`s are defined as an enum containing `EMAIL_CANNOT_BE_EMPTY` and `INVALID_EMAIL_ADDRESS` values (and the implementation of the `ApiError.getName()` interface is to just return the enum's `name()` value which is the recommended solution as per [quickstart usage for ApiError](README.md#quickstart_usage_api_error_enum)), then you could annotate an email field in a validatable object like this: +For example, if your project's `ApiError`s are defined as an enum containing `EMAIL_CANNOT_BE_EMPTY` and +`INVALID_EMAIL_ADDRESS` values (and the implementation of the `ApiError.getName()` interface is to just return the +enum's `name()` value which is the recommended solution as +per [quickstart usage for ApiError](README.md#quickstart_usage_api_error_enum)), then you could annotate an email field +in a validatable object like this: ``` java @NotEmpty(message = "EMAIL_CANNOT_BE_EMPTY") @Email(message = "INVALID_EMAIL_ADDRESS") private String email; ``` - -It's easy for typos to cause this naming convention to fail, so make sure you're using the unit tests that alert you when you fail to conform to the naming convention (described in the next step). + +It's easy for typos to cause this naming convention to fail, so make sure you're using the unit tests that alert you +when you fail to conform to the naming convention (described in the next step). [[back to table of contents]][toc] + ## Reusable Unit Tests for Enforcing Backstopper Rules and Conventions -The [backstopper-reusable-tests-junit5](backstopper-reusable-tests-junit5) library contains several reusable unit +The [backstopper-reusable-tests-junit5](backstopper-reusable-tests-junit5) library contains several reusable unit tests for making sure your project conforms to the Backstopper rules and conventions. + ### Unit Tests to Guarantee JSR 303 Annotation Message Naming Convention Conformance ##### Message Naming Convention Unit Test - What/Why/How? -Since the messages in the JSR 303 annotations are Strings the compiler cannot verify that you've followed the naming convention correctly. It's easy to have a typo in an JSR 303 annotation's `message` attribute, leading the error handler to be unable to convert it to an `ApiError` and ultimately causing the client to receive a generic service error instead of what you really intended. - -Any failures to conform to the message naming convention *should* cause your project to fail to build until the problem is fixed. This is trivially easy to do using the base unit test classes provided by the Backstopper reusable tests library. Simply create an extension of `com.nike.backstopper.apierror.contract.jsr303convention.VerifyJsr303ValidationMessagesPointToApiErrorsTest` and implement the `getAnnotationTroller()` and `getProjectApiErrors()` methods. The annotation troller you return from that method is another custom class you'll create that extends `com.nike.backstopper.apierror.contract.jsr303convention.ReflectionBasedJsr303AnnotationTrollerBase` and should be retrieved/exposed as a singleton. **See the javadocs for `ReflectionBasedJsr303AnnotationTrollerBase` for detailed setup, usage information, and example code.** - -Creating these objects and unit test(s) in your project is not time consuming or difficult, but it does need to be done carefully so read those javadocs and consult the [sample applications](README.md#samples) to make things easy on yourself. It is also highly recommended that you verify the unit test is working by creating an incorrect annotation message in your project and making sure that your project fails to build until the message is fixed. - -(Note: If you're using the `@StringConvertsToClassType` JSR 303 annotation from the [backstopper-custom-validators](backstopper-custom-validators/) library then there is a `VerifyEnumsReferencedByStringConvertsToClassTypeJsr303AnnotationsAreJacksonCaseInsensitiveTest` unit test you can extend to make sure you don't run into problems using that annotation's `allowCaseInsensitiveEnumMatch` option in combination with enums and Jackson deserialization.) +Since the messages in the JSR 303 annotations are Strings the compiler cannot verify that you've followed the naming +convention correctly. It's easy to have a typo in an JSR 303 annotation's `message` attribute, leading the error handler +to be unable to convert it to an `ApiError` and ultimately causing the client to receive a generic service error instead +of what you really intended. + +Any failures to conform to the message naming convention *should* cause your project to fail to build until the problem +is fixed. This is trivially easy to do using the base unit test classes provided by the Backstopper reusable tests +library. Simply create an extension of +`com.nike.backstopper.apierror.contract.jsr303convention.VerifyJsr303ValidationMessagesPointToApiErrorsTest` and +implement the `getAnnotationTroller()` and `getProjectApiErrors()` methods. The annotation troller you return from that +method is another custom class you'll create that extends +`com.nike.backstopper.apierror.contract.jsr303convention.ReflectionBasedJsr303AnnotationTrollerBase` and should be +retrieved/exposed as a singleton. **See the javadocs for `ReflectionBasedJsr303AnnotationTrollerBase` for detailed +setup, usage information, and example code.** + +Creating these objects and unit test(s) in your project is not time consuming or difficult, but it does need to be done +carefully so read those javadocs and consult the [sample applications](README.md#samples) to make things easy on +yourself. It is also highly recommended that you verify the unit test is working by creating an incorrect annotation +message in your project and making sure that your project fails to build until the message is fixed. + +(Note: If you're using the `@StringConvertsToClassType` JSR 303 annotation from +the [backstopper-custom-validators](backstopper-custom-validators/) library then there is a +`VerifyEnumsReferencedByStringConvertsToClassTypeJsr303AnnotationsAreJacksonCaseInsensitiveTest` unit test you can +extend to make sure you don't run into problems using that annotation's `allowCaseInsensitiveEnumMatch` option in +combination with enums and Jackson deserialization.) ##### Excluding Specific JSR 303 Annotation Declarations -By default these unit tests will troll *every* JSR 303 annotation declaration in your project looking for problems. But you may have legitimate exclusions that should not be trolled, for example unit tests in your project that are doing negative testing. The methods you are required to implement when extending `ReflectionBasedJsr303AnnotationTrollerBase` give you the mechanisms to define the exclusions for your project. See the javadocs for those methods and the class-level javadocs for `ReflectionBasedJsr303AnnotationTrollerBase` for information on how to implement those methods. +By default these unit tests will troll *every* JSR 303 annotation declaration in your project looking for problems. But +you may have legitimate exclusions that should not be trolled, for example unit tests in your project that are doing +negative testing. The methods you are required to implement when extending `ReflectionBasedJsr303AnnotationTrollerBase` +give you the mechanisms to define the exclusions for your project. See the javadocs for those methods and the +class-level javadocs for `ReflectionBasedJsr303AnnotationTrollerBase` for information on how to implement those methods. ##### Using the JSR 303 Annotation Troller for Your Own Unit Tests -If you have custom logic you want applied to JSR 303 annotations you can create your own custom unit tests. The `ReflectionBasedJsr303AnnotationTrollerBase` extension you create for your project will provide reusable access to inspect any or all JSR 303 annotation declarations in your project, so any time you find yourself saying "I want to look at some or all of the JSR 303 annotations as part of a unit test to make sure that *\[some requirement is satisfied]*" then it's likely the problem can be solved easily by using the annotation troller. +If you have custom logic you want applied to JSR 303 annotations you can create your own custom unit tests. The +`ReflectionBasedJsr303AnnotationTrollerBase` extension you create for your project will provide reusable access to +inspect any or all JSR 303 annotation declarations in your project, so any time you find yourself saying "I want to look +at some or all of the JSR 303 annotations as part of a unit test to make sure that *\[some requirement is satisfied]*" +then it's likely the problem can be solved easily by using the annotation troller. -The `VerifyEnumsReferencedByStringConvertsToClassTypeJsr303AnnotationsAreJacksonCaseInsensitiveTest` unit test is an example of using the annotation troller to resolve one of these situations. See the implementation of that unit test and `VerifyJsr303ValidationMessagesPointToApiErrorsTest` for examples of a few different ways to use the annotation troller. +The `VerifyEnumsReferencedByStringConvertsToClassTypeJsr303AnnotationsAreJacksonCaseInsensitiveTest` unit test is an +example of using the annotation troller to resolve one of these situations. See the implementation of that unit test and +`VerifyJsr303ValidationMessagesPointToApiErrorsTest` for examples of a few different ways to use the annotation troller. [[back to table of contents]][toc] + ### Unit Test to Verify Your `ProjectApiErrors` Instance Conforms to Requirements -There are some guarantees that `ProjectApiErrors` makes on how it behaves, but since each project's instance returns a different set of `ApiError`s we need a unit test in each project to verify that the guarantees/requirements are met for each `ProjectApiErrors`. +There are some guarantees that `ProjectApiErrors` makes on how it behaves, but since each project's instance returns a +different set of `ApiError`s we need a unit test in each project to verify that the guarantees/requirements are met for +each `ProjectApiErrors`. -For your project you simply need to extend `ProjectApiErrorsTestBase` and implement the abstract method to return your project's `ProjectApiErrors` (as usual see the [sample applications](README.md#samples) for concrete examples of this). **If you are using JUnit then the base class' `@Test`s should be picked up and you should be done**. If you're using something else (e.g. TestNG) then depending on how the tests are being run you may need to `@Override` all the test methods, have them simply call the super implementation, and annotate them with your unit test framework's annotations or otherwise set them up so that they run. For example with TestNG: +For your project you simply need to extend `ProjectApiErrorsTestBase` and implement the abstract method to return your +project's `ProjectApiErrors` (as usual see the [sample applications](README.md#samples) for concrete examples of this). +**If you are using JUnit then the base class' `@Test`s should be picked up and you should be done**. If you're using +something else (e.g. TestNG) then depending on how the tests are being run you may need to `@Override` all the test +methods, have them simply call the super implementation, and annotate them with your unit test framework's annotations +or otherwise set them up so that they run. For example with TestNG: ``` java public class MyProjectApiErrorsTest extends ProjectApiErrorsTestBase { @@ -171,20 +325,29 @@ public class MyProjectApiErrorsTest extends ProjectApiErrorsTestBase { } ``` -Again, this overriding passthrough trick may or may not be necessary if you're using something besides JUnit - it depends largely on how the tests are being run (e.g. Maven's surefire plugin is notorious for not allowing you to mix & match JUnit and TestNG, but if you run the tests manually with TestNG's runner and a little config setup you may get it to work without the workarounds). ***In any case inspect your unit test report to make sure the tests are running!*** +Again, this overriding passthrough trick may or may not be necessary if you're using something besides JUnit - it +depends largely on how the tests are being run (e.g. Maven's surefire plugin is notorious for not allowing you to mix & +match JUnit and TestNG, but if you run the tests manually with TestNG's runner and a little config setup you may get it +to work without the workarounds). ***In any case inspect your unit test report to make sure the tests are running!*** [[back to table of contents]][toc] + ## Creating New Framework Integrations -It's recommended that you look through the [key components](#key_components) section of this readme and understand how everything fits together before attempting to create a new framework integration. +It's recommended that you look through the [key components](#key_components) section of this readme and understand how +everything fits together before attempting to create a new framework integration. -Note that [backstopper-servlet-api](backstopper-servlet-api/) provides a base you should use when creating any framework integration for a Servlet-API-based framework. - -Also note that the Backstopper repository contains [framework integrations](README.md#framework_modules) for several different frameworks already. It can be very useful to refer to the classes in these libraries while looking through the documentation below to see concrete examples of what is being discussed. +Note that [backstopper-servlet-api](backstopper-servlet-api/) provides a base you should use when creating any framework +integration for a Servlet-API-based framework. + +Also note that the Backstopper repository contains [framework integrations](README.md#framework_modules) for several +different frameworks already. It can be very useful to refer to the classes in these libraries while looking through the +documentation below to see concrete examples of what is being discussed. + ### New Framework Integration Pseudocode ``` java @@ -249,77 +412,172 @@ public MyFrameworkResponseObj convertErrorResponseInfoToFrameworkResponse( [[back to table of contents]][toc] -### Pseudocode Explanation and Further Details - -The main trick with creating a new framework integration for Backstopper is finding the bottleneck(s) in the framework where errors pass through before they are converted to a response for the caller. The fictional `frameworkErrorHandlingBottleneck(...)` method from the pseudocode represents this bottleneck. If you can't limit it to one spot in your actual framework then you'll need to perform similar actions in all places where errors are converted to caller responses. Hopefully there is only a small handful of these locations. Containers (e.g. servlet containers that run applications as WAR artifacts) are often contributors to this problem since they may try to detect and return some errors to the caller before your application framework has even seen the request, so it's not uncommon to need two Backstopper integrations - one for your framework and another for your container - at least if you want to guarantee Backstopper handling of *all* errors. If you have the option for the container to forward errors to the framework that is often a reasonable solution. -Different frameworks have different error handling solutions so you may need to explore how best to hook into your framework's error handling system. For example Spring Web MVC has the concept of `HandlerExceptionResolver` - a series of handlers that get called in defined order until one of them handles the exception. This matches well with the `ApiExceptionHandlerBase` and `UnhandledExceptionHandlerBase` concepts in Backstopper, so the Spring Web MVC framework integration has `ApiExceptionHandlerBase` and `UnhandledExceptionHandlerBase` implementations that extend Spring's `HandlerExceptionResolver`, and individual projects are configured so that the Backstopper handlers are attempted before any built-in Spring Web MVC handlers in order to guarantee that Backstopper handles all errors. Jersey's error handling system on the other hand uses `ExceptionMapper`s to associate exception types with specific handlers. Since we want to associate *all* errors with Backstopper this means the Jersey/Backstopper framework integration specifies a single `ExceptionMapper` that handles all `Throwable`s, and that single `ExceptionMapper` coordinates the execution of `ApiExceptionHandlerBase` and `UnhandledExceptionHandlerBase`, looking much more like the pseudocode above than the Spring Web MVC integration. - -Once you've found the bottleneck(s) and figured out how to hook into the right places then you can follow a procedure similar to what is outlined in the pseudocode, namely attempting to have your framework's `ApiExceptionHandlerBase` extension handle the exception first, and falling back to your framework's `UnhandledExceptionHandlerBase` extension if that fails. The base classes use constructor injection to take in the dependencies they need, and anything that framework-specific extensions need to implement are defined as abstract methods. See the javadocs on those classes for implementation details, however if your framework extension classes compile and you have a way to hook them up to projects so that the project-specific information can be provided, then you're essentially done. +### Pseudocode Explanation and Further Details -It's recommended that you provide some helpers to make project configuration for Backstopper in your framework easy and convenient. If the default Backstopper functionality is sufficient then projects should be able to integrate Backstopper quickly and easily without defining much beyond registering their `ProjectApiErrors`, but at the same time it should be easy for projects to override default behavior if necessary (e.g. add custom `ApiExceptionHandlerListener`s, use a different `ApiExceptionHandlerUtils` to modify how errors are logged, override methods on your framework's `ApiExceptionHandlerBase` and `UnhandledExceptionHandlerBase` if necessary, etc). See `BackstopperSpringWebMvcConfig` and `Jersey2BackstopperConfigHelper` for good examples. +The main trick with creating a new framework integration for Backstopper is finding the bottleneck(s) in the framework +where errors pass through before they are converted to a response for the caller. The fictional +`frameworkErrorHandlingBottleneck(...)` method from the pseudocode represents this bottleneck. If you can't limit it to +one spot in your actual framework then you'll need to perform similar actions in all places where errors are converted +to caller responses. Hopefully there is only a small handful of these locations. Containers (e.g. servlet containers +that run applications as WAR artifacts) are often contributors to this problem since they may try to detect and return +some errors to the caller before your application framework has even seen the request, so it's not uncommon to need two +Backstopper integrations - one for your framework and another for your container - at least if you want to guarantee +Backstopper handling of *all* errors. If you have the option for the container to forward errors to the framework that +is often a reasonable solution. + +Different frameworks have different error handling solutions so you may need to explore how best to hook into your +framework's error handling system. For example Spring Web MVC has the concept of `HandlerExceptionResolver` - a series +of handlers that get called in defined order until one of them handles the exception. This matches well with the +`ApiExceptionHandlerBase` and `UnhandledExceptionHandlerBase` concepts in Backstopper, so the Spring Web MVC framework +integration has `ApiExceptionHandlerBase` and `UnhandledExceptionHandlerBase` implementations that extend Spring's +`HandlerExceptionResolver`, and individual projects are configured so that the Backstopper handlers are attempted before +any built-in Spring Web MVC handlers in order to guarantee that Backstopper handles all errors. Jersey's error handling +system on the other hand uses `ExceptionMapper`s to associate exception types with specific handlers. Since we want to +associate *all* errors with Backstopper this means the Jersey/Backstopper framework integration specifies a single +`ExceptionMapper` that handles all `Throwable`s, and that single `ExceptionMapper` coordinates the execution of +`ApiExceptionHandlerBase` and `UnhandledExceptionHandlerBase`, looking much more like the pseudocode above than the +Spring Web MVC integration. + +Once you've found the bottleneck(s) and figured out how to hook into the right places then you can follow a procedure +similar to what is outlined in the pseudocode, namely attempting to have your framework's `ApiExceptionHandlerBase` +extension handle the exception first, and falling back to your framework's `UnhandledExceptionHandlerBase` extension if +that fails. The base classes use constructor injection to take in the dependencies they need, and anything that +framework-specific extensions need to implement are defined as abstract methods. See the javadocs on those classes for +implementation details, however if your framework extension classes compile and you have a way to hook them up to +projects so that the project-specific information can be provided, then you're essentially done. + +It's recommended that you provide some helpers to make project configuration for Backstopper in your framework easy and +convenient. If the default Backstopper functionality is sufficient then projects should be able to integrate Backstopper +quickly and easily without defining much beyond registering their `ProjectApiErrors`, but at the same time it should be +easy for projects to override default behavior if necessary (e.g. add custom `ApiExceptionHandlerListener`s, use a +different `ApiExceptionHandlerUtils` to modify how errors are logged, override methods on your framework's +`ApiExceptionHandlerBase` and `UnhandledExceptionHandlerBase` if necessary, etc). See `BackstopperSpringWebMvcConfig` +and `Jersey2BackstopperConfigHelper` for good examples. [[back to table of contents]][toc] + ## Backstopper Project Meta-Information -### Motivation - Why Does Backstopper Exist? -The [overview](README.md#overview) section covers the main points - error handling and error responses are a critically important part of APIs since they tell callers what went wrong and what to do when the inevitable errors occur, and a good error handling system can accelerate the debugging process for API developers/production support/etc while a bad one can make it a chore. +### Motivation - Why Does Backstopper Exist? -Unfortunately error handling is often left as an afterthought; building a good error handling system for an API seems like it should be a straightforward task, but doing it properly and in a way that makes returning and debugging API errors simple and easy turns out to be a difficult and error-prone process in practice. Just as bad - you often find yourself redoing that process over and over whenever changing frameworks (or sometimes even simply changing projects). Backstopper provides a set of libraries to make this process easy and replicable regardless of what framework your API is running in. +The [overview](README.md#overview) section covers the main points - error handling and error responses are a critically +important part of APIs since they tell callers what went wrong and what to do when the inevitable errors occur, and a +good error handling system can accelerate the debugging process for API developers/production support/etc while a bad +one can make it a chore. -Furthermore it integrates seamlessly with the JSR 303 (a.k.a. Bean Validation) specification - the standard Java method for validating objects. JSR 303 Bean Validation is generally easy to use, easy to understand, and is widely integrated into a variety of frameworks (or you can use it independently in a standalone way). You get to see the validation constraints in the model objects themselves without it getting in the way, and it's easy to create new constraint annotations. +Unfortunately error handling is often left as an afterthought; building a good error handling system for an API seems +like it should be a straightforward task, but doing it properly and in a way that makes returning and debugging API +errors simple and easy turns out to be a difficult and error-prone process in practice. Just as bad - you often find +yourself redoing that process over and over whenever changing frameworks (or sometimes even simply changing projects). +Backstopper provides a set of libraries to make this process easy and replicable regardless of what framework your API +is running in. -The drawback with JSR 303 is that it was not built for API situations where you need to conform to a strict error contract. There is no built-in way to connect a given JSR 303 constraint violation and a specific project-defined set of error data (i.e. error code, human-readable message, metadata, etc), and the validation constraint messages are raw strings and therefore easy to typo, require obnoxious copy-pasting, and are difficult to maintain over time as they spread throughout your codebase. +Furthermore it integrates seamlessly with the JSR 303 (a.k.a. Bean Validation) specification - the standard Java method +for validating objects. JSR 303 Bean Validation is generally easy to use, easy to understand, and is widely integrated +into a variety of frameworks (or you can use it independently in a standalone way). You get to see the validation +constraints in the model objects themselves without it getting in the way, and it's easy to create new constraint +annotations. -Backstopper solves these issues in a way that lets you reap the benefits and avoid the drawbacks of JSR 303 Bean Validation when working in an API environment. You just have to follow some [simple conventions](#jsr303_conventions) and integrate a few [reusable unit tests](#reusable_tests) into your Backstopper-enabled project. +The drawback with JSR 303 is that it was not built for API situations where you need to conform to a strict error +contract. There is no built-in way to connect a given JSR 303 constraint violation and a specific project-defined set of +error data (i.e. error code, human-readable message, metadata, etc), and the validation constraint messages are raw +strings and therefore easy to typo, require obnoxious copy-pasting, and are difficult to maintain over time as they +spread throughout your codebase. + +Backstopper solves these issues in a way that lets you reap the benefits and avoid the drawbacks of JSR 303 Bean +Validation when working in an API environment. You just have to follow some [simple conventions](#jsr303_conventions) +and integrate a few [reusable unit tests](#reusable_tests) into your Backstopper-enabled project. [[back to table of contents]][toc] + ### Backstopper Key Goals -* All API errors for a project should be able to live in one location and easily referenced and reused rather than being spread throughout the codebase. -* Core errors should be shareable across multiple projects in the same organization. -* It should be easy to add new error definitions. - * The `ApiError` interface and [enum definition convention](README.md#quickstart_usage_api_error_enum) make all these API-error-related goals achievable. +* All API errors for a project should be able to live in one location and easily referenced and reused rather than being + spread throughout the codebase. +* Core errors should be shareable across multiple projects in the same organization. +* It should be easy to add new error definitions. + * The `ApiError` interface and [enum definition convention](README.md#quickstart_usage_api_error_enum) make all + these API-error-related goals achievable. * It should be easy to add new error handlers for new typed exceptions. - * The [`ApiExceptionHandlerListener`](README.md#quickstart_usage_add_custom_listener) mechanism is designed to solve this. -* Straightforward implementation - no annotation processing, classpath scanning, or other indirection magic in the core Backstopper functionality. - * Core components are instrumented with dependency injection annotations, but they are instrumented in such a way that they are not necessary and classes can be used manually without difficulty (i.e. constructor injection rather than field injection). + * The [`ApiExceptionHandlerListener`](README.md#quickstart_usage_add_custom_listener) mechanism is designed to solve + this. +* Straightforward implementation - no annotation processing, classpath scanning, or other indirection magic in the core + Backstopper functionality. + * Core components are instrumented with dependency injection annotations, but they are instrumented in such a way + that they are not necessary and classes can be used manually without difficulty (i.e. constructor injection rather + than field injection). * Framework-specific implementations can add the magic if it's idiomatic to that framework. - * Classes that must be extended by projects in order for Backstopper to function (e.g. `ProjectApiErrors`) guide the concrete implementation with abstract methods and full javadocs explaining what the methods are and how they should be implemented. If the necessary non-nullable dependencies are supplied and the code compiles successfully then you're likely done. - * Putting in breakpoints and debugging what Backstopper is doing is therefore a reasonably simple activity. If you want to adjust Backstopper behavior you can usually determine where the hooks are and how to change things just by using breakpoints and exploring the code. + * Classes that must be extended by projects in order for Backstopper to function (e.g. `ProjectApiErrors`) guide the + concrete implementation with abstract methods and full javadocs explaining what the methods are and how they + should be implemented. If the necessary non-nullable dependencies are supplied and the code compiles successfully + then you're likely done. + * Putting in breakpoints and debugging what Backstopper is doing is therefore a reasonably simple activity. If you + want to adjust Backstopper behavior you can usually determine where the hooks are and how to change things just by + using breakpoints and exploring the code. * No need to refer to documentation for everything - the code and javadocs are usually sufficient. * It should be easy to add integration for new frameworks. - * The core Backstopper functionality is free of framework dependencies and designed with hooks so that framework integrations can be as lightweight as possible. - + * The core Backstopper functionality is free of framework dependencies and designed with hooks so that framework + integrations can be as lightweight as possible. + [[back to table of contents]][toc] + ### Backstopper Key Philosophies -Backstopper was based in large part on common elements found in the error contracts of API industry leaders circa 2014 (e.g. Facebook, Twitter, and others), and was therefore built with a few philosophies in mind. These are general guidelines - not everyone will agree with these ideas and there will always be legitimate exceptions even if you do agree. Therefore Backstopper should have hooks to allow you to override any of this behavior; if you notice an area where it's not possible to override the default behavior please file an issue and we'll see if there's a way to address it. - -* There should be a common error contract for *all* errors. APIs are intended to be used programmatically by callers, and changing contracts for different error types, HTTP status codes, etc, makes that programmatic integration more difficult and error prone. Metadata and optional information can be added or removed at will, but the core error contract should be static. - * Since some error responses might need to contain multiple individual errors (e.g. validation of a request payload that contains multiple problems), the error contract should include an array of individual errors. - * Since the error contract should be the same for *all* errors to facilitate easy programmatic handling by callers, this means an error response with a single individual error should still result in an error contract that contains an array - it would simply be an array with one error inside. -* There should be a unique error ID for *all* responses which matches a single application log entry tagged with that error ID and contains all debugging info for the request. - * This makes it trivial to find the relevant debugging information when a caller notifies you of an error they received that they don't think they should have received. - * It should be as *difficult* as possible for callers to fail to give you the information you need to debug an error. Therefore the error ID should show up in both the response body payload and headers. Callers interacting with customer support or API developers often provide limited information (e.g. a screenshot of the error they saw) and/or have limited technical expertise, so by the time they contact you it's usually too late (or unrealistic) to have them go back and look in the headers for an error ID. -* The application/business error codes returned by the API (not to be confused with HTTP status code) should be integers rather than string-based. - * Error codes are what API callers program against. They are your contract with callers and therefore they should *never* change. Integer-based codes allow you to completely decouple the interpretation of the error from the error code. - * Integer-based codes make localization simpler - you can have an error docs page localized to multiple languages/regions without any confusion over what to do with the error codes. - * There's already a human-readable message field designed to let you give callers a hint as to what went wrong without consulting a docs page. String-based error codes would overlap with the human-readable-message's purpose but are less capable of providing useful information since error codes should not be verbose (overly long error codes make API integration more frustrating). - * `ApiError.getErrorCode()` returns a string in order to allow for the necessary flexibility for those who want to use string-based codes despite these concerns, so bypassing this philosophy is trivially easy. -* The human-readable-messages in error contract responses are provided as hints. They are not contractual and are therefore subject to change. They do not need to be localized. The error code is what API integrators should code against, not the message. +Backstopper was based in large part on common elements found in the error contracts of API industry leaders circa 2014 ( +e.g. Facebook, Twitter, and others), and was therefore built with a few philosophies in mind. These are general +guidelines - not everyone will agree with these ideas and there will always be legitimate exceptions even if you do +agree. Therefore Backstopper should have hooks to allow you to override any of this behavior; if you notice an area +where it's not possible to override the default behavior please file an issue and we'll see if there's a way to address +it. + +* There should be a common error contract for *all* errors. APIs are intended to be used programmatically by callers, + and changing contracts for different error types, HTTP status codes, etc, makes that programmatic integration more + difficult and error prone. Metadata and optional information can be added or removed at will, but the core error + contract should be static. + * Since some error responses might need to contain multiple individual errors (e.g. validation of a request payload + that contains multiple problems), the error contract should include an array of individual errors. + * Since the error contract should be the same for *all* errors to facilitate easy programmatic handling by callers, + this means an error response with a single individual error should still result in an error contract that contains + an array - it would simply be an array with one error inside. +* There should be a unique error ID for *all* responses which matches a single application log entry tagged with that + error ID and contains all debugging info for the request. + * This makes it trivial to find the relevant debugging information when a caller notifies you of an error they + received that they don't think they should have received. + * It should be as *difficult* as possible for callers to fail to give you the information you need to debug an + error. Therefore the error ID should show up in both the response body payload and headers. Callers interacting + with customer support or API developers often provide limited information (e.g. a screenshot of the error they + saw) and/or have limited technical expertise, so by the time they contact you it's usually too late (or + unrealistic) to have them go back and look in the headers for an error ID. +* The application/business error codes returned by the API (not to be confused with HTTP status code) should be integers + rather than string-based. + * Error codes are what API callers program against. They are your contract with callers and therefore they should + *never* change. Integer-based codes allow you to completely decouple the interpretation of the error from the + error code. + * Integer-based codes make localization simpler - you can have an error docs page localized to multiple + languages/regions without any confusion over what to do with the error codes. + * There's already a human-readable message field designed to let you give callers a hint as to what went wrong + without consulting a docs page. String-based error codes would overlap with the human-readable-message's purpose + but are less capable of providing useful information since error codes should not be verbose (overly long error + codes make API integration more frustrating). + * `ApiError.getErrorCode()` returns a string in order to allow for the necessary flexibility for those who want to + use string-based codes despite these concerns, so bypassing this philosophy is trivially easy. +* The human-readable-messages in error contract responses are provided as hints. They are not contractual and are + therefore subject to change. They do not need to be localized. The error code is what API integrators should code + against, not the message. [[back to table of contents]][toc] + ## License Backstopper is released under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) From 43307fc36924da99fe0fbacfaa290017aaef903d Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Thu, 12 Sep 2024 14:06:51 -0700 Subject: [PATCH 33/42] Update readme and user guide after the javax-to-jakarta refactoring --- README.md | 219 ++++++++++++++++++++++++++++---------------------- USER_GUIDE.md | 84 ++++++++++--------- 2 files changed, 172 insertions(+), 131 deletions(-) diff --git a/README.md b/README.md index 0533cf3..e76de30 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,15 @@ [![Code Coverage][codecov_img]][codecov] [![License][license img]][license] -**Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 7 and +**Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 17 and greater.** +(NOTE: The [Backstopper 1.x branch](https://github.com/Nike-Inc/backstopper/tree/v1.x) contains a version of +Backstopper for Java 7+, and for the `javax` ecosystem. The current Backstopper supports Java 17+ and the `jakarta` +ecosystem. The Backstopper 1.x releases also contain support for JAX-RS 2, Jersey 1 and 2, Spring 4 and 5, and +Springboot 1 and 2 - see +[here](https://github.com/Nike-Inc/backstopper/tree/v1.x?tab=readme-ov-file#framework_modules).) + ## TL;DR Backstopper guarantees that a consistent error contract which you can define will be sent to callers *no matter the @@ -22,6 +28,28 @@ concrete examples of using Backstopper that are simple, compact, and straightfor The rest of this readme is intended to get you oriented and running quickly, and the [User Guide](USER_GUIDE.md) contains more in-depth details. + + +## Table of Contents + + +* [Additional Features Overview](#overview) +* [Barebones Example](#barebones_example) +* [Quickstart](#quickstart) + * [Quickstart - Integration](#quickstart_integration) + * [Quickstart - Usage](#quickstart_usage) + * [Defining a set of API errors](#quickstart_usage_api_error_enum) + * [Defining a ProjectApiErrors for your project](#quickstart_usage_project_api_errors) + * [Manually throwing an error](#quickstart_usage_throw_api_exception) + * [Creating a custom exception listener](#quickstart_usage_add_custom_listener) +* [Integration Modules](#modules) + * [General-Purpose Modules](#general_modules) + * [Framework-Specific Modules](#framework_modules) +* [Sample Applications](#samples) +* [User Guide](USER_GUIDE.md) +* [License](#license) + + ## Additional Features Overview @@ -43,7 +71,9 @@ contains more in-depth details. relatively straightforward process. Backstopper is geared primarily towards HTTP-based APIs, but could be used in other circumstances if desired and without polluting your project with unwanted HTTP API dependencies. -### Barebones Example (assumes [framework integration](#quickstart_integration) is already done) + + +## Barebones Example (assumes [framework integration](#quickstart_integration) is already done) ##### 1. Define your API's errors @@ -113,7 +143,7 @@ throw ApiException.newBuilder() ##### 5. Use the error_id to locate debugging details in the logs (a 5xx error would also include the stack trace) ``` -2016-09-21_12:14:00.620 |-WARN c.n.b.h.j.Jersey1ApiExceptionHandler - ApiExceptionHandlerBase handled +2016-09-21_12:14:00.620 |-WARN c.n.b.h.s.SpringApiExceptionHandler - ApiExceptionHandlerBase handled ↪exception occurred: error_uid=408d516f-68d1-41f3-adb1-b3dc0affaaf2, ↪exception_class=com.nike.backstopper.exception.ClientDataValidationError, returned_http_status_code=400, ↪contributing_errors="INVALID_EMAIL_ADDRESS", request_uri="/profile", request_method="POST", @@ -122,86 +152,6 @@ throw ApiException.newBuilder() ↪constraint_violation_details="Payload.email|org.hibernate.validator.constraints.Email|INVALID_EMAIL_ADDRESS" ``` - - -### General-Purpose Modules - -* [backstopper-core](backstopper-core/) - The core library providing the majority of the Backstopper functionality. -* [backstopper-reusable-tests-junit5](backstopper-reusable-tests-junit5/) - There are some rules around defining your - project's set of API errors and conventions around integration with Java's JSR 303 Bean Validation system that must be - followed for Backstopper to function at its best. This library contains reusable tests that are intended to be - included in a Backstopper-enabled project's unit test suite to guarantee that these rules are adhered to and fail the - build with descriptive unit test errors when those rules are violated. These are technically optional but ***highly*** - recommended - integration is generally quick and easy. -* [backstopper-custom-validators](backstopper-custom-validators/) - This library contains JSR 303 Bean Validation - annotations that have proven to be useful and reusable. These are entirely optional. They are also largely dependency - free so this library is usable in non-Backstopper projects that utilize JSR 303 validations. -* [backstopper-jackson](backstopper-jackson/) - Contains a few utilities that help integrate Backstopper and Jackson for - serializing error contracts to JSON. Optional. -* [backstopper-servlet-api](backstopper-servlet-api/) - Intermediate library intended to ease integration with - Servlet-based frameworks. If you're building a Backstopper integration library for a Servlet-based framework that we - don't already have support for then you'll want to use this. -* [nike-internal-util](nike-internal-util/) - A small utilities library that provides some reusable helper methods and - classes. It is "internal" in the sense that it is not intended to be directly pulled in and used by non-Nike projects. - That said you can use it if you want to, just be aware that some liberties might be taken regarding version numbers, - backwards compatibility, etc over time when compared with libraries specifically intended for public consumption. - - - -### Framework-Specific Modules - -// TODO javax-to-jakarta: Explain why jaxrs and jersey3 are not currently supported, but could be if there's interest. - -* [backstopper-jaxrs](backstopper-jaxrs) - Integration library for JAX-RS. If you want to integrate Backstopper into a - JAX-RS project other than Jersey then start here (see below for the Jersey-specific modules). -* [backstopper-jersey1](backstopper-jersey1/) - Integration library for the Jersey 1 framework. If you want to integrate - Backstopper into a project running in Jersey 1 then start here. There is - a [Jersey 1 sample project](samples/sample-jersey1/) complete with integration tests you can use as an example. -* [backstopper-jersey2](backstopper-jersey2/) - Integration library for the Jersey 2 framework. If you want to integrate - Backstopper into a project running in Jersey 2 then start here. There is - a [Jersey 2 sample project](samples/sample-jersey2/) complete with integration tests you can use as an example. -* [backstopper-spring-web-mvc](backstopper-spring-web-mvc/) - Base Integration library for the Spring Web MVC (Servlet) - framework. If you want to integrate Backstopper into a project running in Spring Web MVC then start here. Works for - both Spring 4 and Spring 5, and used as a foundation for Backstopper support in Spring Boot 1 and 2 (when using Web - MVC with Spring Boot - see below for links to Spring Boot specific integration modules). There is a - [Spring Web MVC sample project](samples/sample-spring-web-mvc/) complete with integration tests you can use as an - example. -* [backstopper-spring-web-flux](backstopper-spring-web-flux/) - Integration library for the Spring WebFlux (Netty) - framework. If you want to integrate Backstopper into a project running in Spring WebFlux then start here. There is a - [Spring Boot 2 WebFlux sample project](samples/sample-spring-boot2-webflux/) complete with integration tests you can - use as an example. -* [backstopper-spring-boot1](backstopper-spring-boot1/) - Integration library for the Spring Boot 1 framework. - If you want to integrate Backstopper into a project running in Spring Boot 1 then start here. There is a - [Spring Boot 1 sample project](samples/sample-spring-boot1/) complete with integration tests you can use as an - example. -* [backstopper-spring-boot2-webmvc](backstopper-spring-boot2-webmvc/) - Integration library for the Spring Boot 2 - framework *if and only if you're using the Spring Web MVC Servlet runtime* (if you're running a Spring - Boot 2 + WebFlux Netty application, then you do not want this library and should use - [backstopper-spring-web-flux](backstopper-spring-web-flux) instead). If you want to integrate Backstopper into a - project running in Spring Boot 2 + Web MVC then start here. There is a - [Spring Boot 2 Web MVC sample project](samples/sample-spring-boot2-webmvc/) complete with integration tests you - can use as an example. - - - -### Framework Integration Sample Applications - -Note that the sample apps are an excellent source for framework integration examples, but they are also very helpful for -giving you an overview and exploring what you can do with Backstopper regardless of framework: how to create and throw -errors, how they show up for the caller in the response, what Backstopper outputs in the application logs when errors -occur, and how to find the relevant log message given a specific error response. The -`VerifyExpectedErrorsAreReturnedComponentTest` component tests in the sample apps exercise a large portion of -Backstopper's functionality - you can learn a lot by running that component test, seeing what the sample app returns in -the error responses, and exploring the associated endpoints and framework configuration in the sample apps to see how it -all fits together. - -* [samples/sample-jersey1](samples/sample-jersey1/) -* [samples/sample-jersey2](samples/sample-jersey2/) -* [samples/sample-spring-web-mvc](samples/sample-spring-web-mvc/) -* [samples/sample-spring-boot1](samples/sample-spring-boot1/) -* [samples/sample-spring-boot2-webmvc](samples/sample-spring-boot2-webmvc/) -* [samples/sample-spring-boot2-webflux](samples/sample-spring-boot2-webflux/) - ## Quickstart @@ -256,14 +206,13 @@ public enum MyProjectApiError implements ApiError { SOME_OTHER_SERVICE_ERROR(GENERIC_SERVICE_ERROR), GENERIC_BAD_REQUEST(20, "Invalid request", 400), // Includes metadata in the response payload sent to the caller - SOME_OTHER_BAD_REQUEST(30, "You failed to pass the required foo", 400, - MapBuilder.builder("missing_field", (Object)"foo").build()), + SOME_OTHER_BAD_REQUEST(30, "You failed to pass the required foo", 400, Map.of("missing_field", "foo")), // Also a mirror for another ApiError, but includes extra metadata that will show up in the response - YET_ANOTHER_BAD_REQUEST(GENERIC_BAD_REQUEST, MapBuilder.builder("field", (Object)"bar").build()); + YET_ANOTHER_BAD_REQUEST(GENERIC_BAD_REQUEST, Map.of("field", "bar")); private final ApiError delegate; - MyProjectApiError(ApiError delegate) { this.delegate = delegate; } + MyProjectApiError(ApiError delegate) {this.delegate = delegate; } MyProjectApiError(ApiError delegate, Map additionalMetadata) { this(new ApiErrorWithMetadata(delegate, additionalMetadata)); @@ -275,7 +224,7 @@ public enum MyProjectApiError implements ApiError { MyProjectApiError(int errorCode, String message, int httpStatusCode, Map metadata) { this(new ApiErrorBase( - "delegated-to-enum-name-" + UUID.randomUUID().toString(), errorCode, message, httpStatusCode, + "delegated-to-enum-name-" + UUID.randomUUID(), errorCode, message, httpStatusCode, metadata )); } @@ -295,7 +244,7 @@ public enum MyProjectApiError implements ApiError { @Override public Map getMetadata() { return delegate.getMetadata(); } -} +} ``` @@ -313,7 +262,7 @@ classes for an example. The javadocs for `ProjectApiErrors` contains in-depth in ##### Manually throwing an arbitrary error with full control over the resulting error contract, response headers, and logging info ``` java -// The only requirement is that you have at least one ApiError. Everything else is optional. +// The only requirement is that you include at least one ApiError. Everything else is optional. throw ApiException.newBuilder() .withApiErrors(MyProjectApiError.FOO_ERROR, MyProjectApiError.BAD_THING_HAPPENED) .withExceptionMessage("Useful message for exception in the logs") @@ -329,6 +278,10 @@ throw ApiException.newBuilder() .build(); ``` +Above is a "kitchen sink" example showing all the options available when throwing an `ApiException` manually. For +automatic input validation and error handling see the User Guide's section on +[Bean Validation](USER_GUIDE.md#jsr_303_support). + ##### Creating a custom `ApiExceptionHandlerListener` to handle a typed exception @@ -337,12 +290,11 @@ This is only really necessary if you can't (or don't want to) throw an `ApiExcep handle a typed exception it wouldn't otherwise know about. Many projects never need to do this. ``` java -public static class MyFrameworkExceptionHandlerListener implements ApiExceptionHandlerListener { +public class MyFrameworkExceptionHandlerListener implements ApiExceptionHandlerListener { @Override public ApiExceptionHandlerListenerResult shouldHandleException(Throwable ex) { - if (ex instanceof MyFrameworkException) { + if (ex instanceof MyFrameworkException myEx) { // The exception is a MyFrameworkException, so this listener should handle it. - MyFrameworkException myEx = (MyFrameworkException)ex; SortedApiErrorSet apiErrors = SortedApiErrorSet.singletonSortedSetOf(MyProjectApiError.SOME_OTHER_BAD_REQUEST); List> extraDetailsForLogging = Arrays.asList( @@ -350,8 +302,8 @@ public static class MyFrameworkExceptionHandlerListener implements ApiExceptionH Pair.of("important_bar_info", myEx.bar()) ); List>> extraResponseHeaders = Arrays.asList( - Pair.of("foo-info", myEx.foo()), - Pair.of("bar-info", myEx.bar()) + Pair.of("foo-info", singletonList(myEx.foo())), + Pair.of("bar-info", singletonList(myEx.bar())) ); return ApiExceptionHandlerListenerResult.handleResponse( apiErrors, extraDetailsForLogging, extraResponseHeaders @@ -367,6 +319,83 @@ public static class MyFrameworkExceptionHandlerListener implements ApiExceptionH After defining a new `ApiExceptionHandlerListener` you'll need to register it with the `ApiExceptionHandlerBase` running your Backstopper system. This is a procedure that is often different for each framework integration. + + +## Integration Modules + + + +### General-Purpose Modules + +* [backstopper-core](backstopper-core/) - The core library providing the majority of the Backstopper functionality. +* [backstopper-reusable-tests-junit5](backstopper-reusable-tests-junit5/) - There are some rules around defining your + project's set of API errors and conventions around integration with Java's JSR 303 Bean Validation system that must be + followed for Backstopper to function at its best. This library contains reusable tests that are intended to be + included in a Backstopper-enabled project's unit test suite to guarantee that these rules are adhered to and fail the + build with descriptive unit test errors when those rules are violated. These are technically optional but ***highly*** + recommended - integration is generally quick and easy. +* [backstopper-custom-validators](backstopper-custom-validators/) - This library contains JSR 303 Bean Validation + annotations that have proven to be useful and reusable. These are entirely optional. They are also largely dependency + free so this library is usable in non-Backstopper projects that utilize JSR 303 validations. +* [backstopper-jackson](backstopper-jackson/) - Contains a few utilities that help integrate Backstopper and Jackson for + serializing error contracts to JSON. Optional. +* [backstopper-servlet-api](backstopper-servlet-api/) - Intermediate library intended to ease integration with + Servlet-based frameworks. If you're building a Backstopper integration library for a Servlet-based framework that we + don't already have support for then you'll want to use this. +* [backstopper-spring-web](backstopper-spring-web/) - Intermediate library intended to ease integration with + Spring-based frameworks. If you're building a Spring integration library for a Spring-based framework that we + don't already have support for then you'll want to use this. +* [nike-internal-util](nike-internal-util/) - A small utilities library that provides some reusable helper methods and + classes. It is "internal" in the sense that it is not intended to be directly pulled in and used by non-Nike projects. + That said you can use it if you want to, just be aware that some liberties might be taken regarding version numbers, + backwards compatibility, etc over time when compared with libraries specifically intended for public consumption. + + + +### Framework-Specific Modules + +* [backstopper-spring-web-mvc](backstopper-spring-web-mvc/) - Base Integration library for the Spring Web MVC (Servlet) + framework. If you want to integrate Backstopper into a project running in Spring Web MVC then start here. Works for + Spring 6+, and used as a foundation for Backstopper support in Spring Boot 3 MVC (see below for links to Spring + Boot specific integration modules). There is a [Spring Web MVC sample project](samples/sample-spring-web-mvc/) + complete with integration tests you can use as an example. +* [backstopper-spring-web-flux](backstopper-spring-web-flux/) - Integration library for the Spring WebFlux (Netty) + framework. If you want to integrate Backstopper into a project running in Spring WebFlux then start here. There is a + [Spring Boot 3 WebFlux sample project](samples/sample-spring-boot3-webflux/) complete with integration tests you can + use as an example. +* [backstopper-spring-boot3-webmvc](backstopper-spring-boot3-webmvc/) - Integration library for the Spring Boot 3 + framework *if and only if you're using the Spring Web MVC Servlet runtime* (if you're running a Spring + Boot 3 + WebFlux Netty application, then you do not want this library and should use + [backstopper-spring-web-flux](backstopper-spring-web-flux) instead). If you want to integrate Backstopper into a + project running in Spring Boot 3 + Web MVC then start here. There is a + [Spring Boot 3 Web MVC sample project](samples/sample-spring-boot3-webmvc/) complete with integration tests you + can use as an example. + +NOTE: Backstopper 1.x releases contain support for the `javax` ecosystem, JAX-RS 2, Jersey 1 and 2, Spring 4 and 5, and +Springboot 1 and 2 - see +[here](https://github.com/Nike-Inc/backstopper/tree/v1.x?tab=readme-ov-file#framework_modules) if you need support +for these older frameworks. + +We have not created support for `jakarta` based JAX-RS or Jersey frameworks in Backstopper 2.x due to lack of +interest, but it could be done. + + + +## Framework Integration Sample Applications + +The sample apps are an excellent source for framework integration examples, but they are also very helpful for +giving you an overview and exploring what you can do with Backstopper regardless of framework: how to create and throw +errors, how they show up for the caller in the response, what Backstopper outputs in the application logs when errors +occur, and how to find the relevant log message given a specific error response. The +`VerifyExpectedErrorsAreReturnedComponentTest` component tests in the sample apps exercise a large portion of +Backstopper's functionality - you can learn a lot by running that component test, seeing what the sample app returns in +the error responses, and exploring the associated endpoints and framework configuration in the sample apps to see how it +all fits together. + +* [samples/sample-spring-web-mvc](samples/sample-spring-web-mvc/) +* [samples/sample-spring-boot3-webmvc](samples/sample-spring-boot3-webmvc/) +* [samples/sample-spring-boot3-webflux](samples/sample-spring-boot3-webflux/) + ## User Guide For further details please consult the [User Guide](USER_GUIDE.md). diff --git a/USER_GUIDE.md b/USER_GUIDE.md index eeb0928..a574da0 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -1,15 +1,21 @@ # Backstopper User Guide -**Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 7 and +**Backstopper is a framework-agnostic API error handling and (optional) model validation solution for Java 17 and greater.** +(NOTE: The [Backstopper 1.x branch](https://github.com/Nike-Inc/backstopper/tree/v1.x) contains a version of +Backstopper for Java 7+, and for the `javax` ecosystem. The current Backstopper supports Java 17+ and the `jakarta` +ecosystem. The Backstopper 1.x releases also contain support for JAX-RS 2, Jersey 1 and 2, Spring 4 and 5, and +Springboot 1 and 2 - see +[here](https://github.com/Nike-Inc/backstopper/tree/v1.x?tab=readme-ov-file#framework_modules).) + The [base project README](README.md) covers Backstopper at a surface level, including: * [Overview](README.md#overview) +* [Quickstart](README.md#quickstart). * Repository library modules (both [general](README.md#general_modules) and [framework-specific](README.md#framework_modules)) * Framework-specific [sample applications](README.md#samples) -* [Quickstart](README.md#quickstart). This User Guide is for a more in-depth exploration of Backstopper. @@ -19,13 +25,12 @@ This User Guide is for a more in-depth exploration of Backstopper. * [Backstopper Key Components](#key_components) * [JSR 303 Bean Validation Support](#jsr_303_support) - * [Enabling Basic JSR 303 Validation](#jsr_303_basic_setup) + * [Enabling Basic JSR 303 Bean Validation](#jsr_303_basic_setup) * [Throwing JSR 303 Violations in a Backstopper-Compatible Way](#backstopper_compatible_jsr_303_exceptions) * [Backstopper Conventions for JSR 303 Bean Validation Support](#jsr303_conventions) * [Reusable Unit Tests for Enforcing Backstopper Rules and Conventions](#reusable_tests) * [Unit Tests to Guarantee JSR 303 Annotation Message Naming Convention Conformance](#setup_jsr303_convention_unit_tests) - * [Unit Test to Verify Your - `ProjectApiErrors` Instance Conforms to Requirements](#setup_project_api_errors_unit_test) + * [Unit Test to Verify Your `ProjectApiErrors` Instance Conforms to Requirements](#setup_project_api_errors_unit_test) * [Creating New Framework Integrations](#new_framework_integrations) * [New Framework Integration Pseudocode](#new_framework_pseudocode) * [Pseudocode Explanation and Further Details](#new_framework_pseudocode_explanation) @@ -133,29 +138,33 @@ grounding for further exploration. ## JSR 303 Bean Validation Support Guaranteeing that JSR 303 Bean Validation violations *will* map to a specific API error (represented by the `ApiError` -values returned by each project's `ProjectApiErrors` instance) is one of the major benefits of Backstopper. It requires -throwing an exception that Backstopper knows how to handle that wraps the JSR 303 constraint violations, following a -specific message naming convention when declaring constraint annotations, and to be safe you should set up a few unit -tests that will catch any JSR 303 constraint annotation declarations that don't conform to the naming convention due to -typos, copy-paste errors, or any other reason. The following sections cover these concerns and describe how to enable -JSR 303 Bean Validation support in Backstopper. +values returned by each project's `ProjectApiErrors` instance) is one of the major benefits of Backstopper. Meeting +this guarantee requires: + +1. Throwing an exception that Backstopper knows how to handle that wraps the JSR 303 constraint violations. +2. Following a specific message naming convention when declaring constraint annotations. +3. Setting up a few unit tests that will catch any JSR 303 constraint annotation declarations that + don't conform to the naming convention due to typos, copy-paste errors, or any other reason. + +The following sections cover these concerns and describe how to enable JSR 303 Bean Validation support in Backstopper. (Note that JSR 303 integration is optional - you can successfully run Backstopper in an environment that does not -include any JSR 303 support and it will work just fine.) +include any JSR 303 support, and it will work just fine.) -### Enabling Basic JSR 303 Validation +### Enabling Basic JSR 303 Bean Validation -Enabling JSR 303 in your application is outside the scope of this User Guide - different containers and frameworks set -it up in different ways and may depend on the JSR 303 implementation you're using. It is usually not too difficult - -consult your framework's docs and google for tutorials and examples to see if your framework has built-in JSR 303 -support (if it does and we already have a [sample application](README.md#samples) for your framework you can consult the -sample app for an example on how to enable and use the JSR 303 support in Backstopper for that framework). +Enabling JSR 303 Bean Validation in your application is outside the scope of this User Guide - different containers and +frameworks set it up in different ways and may depend on the JSR 303 implementation you're using. It is usually not +too difficult - consult your framework's docs and google for tutorials and examples to see if your framework has +built-in JSR 303 support (if it does and we already have a [sample application](README.md#samples) for your +framework you can consult the sample app for an example on how to enable and use the JSR 303 support in Backstopper +for that framework). In the worst case you can always manually create a `jakarta.validation.Validator` and use `ClientDataValidationService` -to validate your objects. Just make sure a JSR 303 implementation library is on your classpath ( -e.g. [Hibernate Validator](http://hibernate.org/validator/) or [Apache BVal](http://bval.apache.org/)) and call +to validate your objects. Just make sure a JSR 303 implementation library is on your classpath (e.g. +[Hibernate Validator](http://hibernate.org/validator/)) and call `jakarta.validation.Validation.buildDefaultValidatorFactory().getValidator()`. `Validator` is thread safe so you only technically have to create one and can share it around. @@ -368,8 +377,10 @@ private MyFrameworkUnhandledExceptionHandler unhandledExceptionHandler; * This is some bottleneck location in the framework where errors (hopefully all of them) are guaranteed * to pass through as they are converted into responses for the caller. */ -public MyFrameworkResponseObj frameworkErrorHandlingBottleneck(Throwable ex, - MyFrameworkRequestObj request) { +public MyFrameworkResponseObj frameworkErrorHandlingBottleneck( + Throwable ex, + MyFrameworkRequestObj request +) { // Try the known exception handler first. try { ErrorResponseInfo errorResponseInfo = @@ -419,12 +430,11 @@ The main trick with creating a new framework integration for Backstopper is find where errors pass through before they are converted to a response for the caller. The fictional `frameworkErrorHandlingBottleneck(...)` method from the pseudocode represents this bottleneck. If you can't limit it to one spot in your actual framework then you'll need to perform similar actions in all places where errors are converted -to caller responses. Hopefully there is only a small handful of these locations. Containers (e.g. servlet containers -that run applications as WAR artifacts) are often contributors to this problem since they may try to detect and return -some errors to the caller before your application framework has even seen the request, so it's not uncommon to need two -Backstopper integrations - one for your framework and another for your container - at least if you want to guarantee -Backstopper handling of *all* errors. If you have the option for the container to forward errors to the framework that -is often a reasonable solution. +to caller responses. Hopefully there is only a small handful of these locations. Containers (e.g. servlet containers) +are often contributors to this problem since they may try to detect and return some errors to the caller before your +application framework has even seen the request, so it's not uncommon to need two Backstopper integrations - one for +your framework and another for your container - at least if you want to guarantee Backstopper handling of *all* +errors. If you have the option for the container to forward errors to the framework that is often a reasonable solution. Different frameworks have different error handling solutions so you may need to explore how best to hook into your framework's error handling system. For example Spring Web MVC has the concept of `HandlerExceptionResolver` - a series @@ -434,8 +444,9 @@ integration has `ApiExceptionHandlerBase` and `UnhandledExceptionHandlerBase` im `HandlerExceptionResolver`, and individual projects are configured so that the Backstopper handlers are attempted before any built-in Spring Web MVC handlers in order to guarantee that Backstopper handles all errors. Jersey's error handling system on the other hand uses `ExceptionMapper`s to associate exception types with specific handlers. Since we want to -associate *all* errors with Backstopper this means the Jersey/Backstopper framework integration specifies a single -`ExceptionMapper` that handles all `Throwable`s, and that single `ExceptionMapper` coordinates the execution of +associate *all* errors with Backstopper this means the Jersey/Backstopper framework integration (in the +[Backstopper 1.x branch](https://github.com/Nike-Inc/backstopper/tree/v1.x)) specifies a single `ExceptionMapper` +that handles all `Throwable`s, and that single `ExceptionMapper` coordinates the execution of `ApiExceptionHandlerBase` and `UnhandledExceptionHandlerBase`, looking much more like the pseudocode above than the Spring Web MVC integration. @@ -453,7 +464,8 @@ quickly and easily without defining much beyond registering their `ProjectApiErr easy for projects to override default behavior if necessary (e.g. add custom `ApiExceptionHandlerListener`s, use a different `ApiExceptionHandlerUtils` to modify how errors are logged, override methods on your framework's `ApiExceptionHandlerBase` and `UnhandledExceptionHandlerBase` if necessary, etc). See `BackstopperSpringWebMvcConfig` -and `Jersey2BackstopperConfigHelper` for good examples. +and `Jersey2BackstopperConfigHelper` (in the +[Backstopper 1.x branch](https://github.com/Nike-Inc/backstopper/tree/v1.x)) for good examples. [[back to table of contents]][toc] @@ -477,7 +489,7 @@ yourself redoing that process over and over whenever changing frameworks (or som Backstopper provides a set of libraries to make this process easy and replicable regardless of what framework your API is running in. -Furthermore it integrates seamlessly with the JSR 303 (a.k.a. Bean Validation) specification - the standard Java method +Furthermore it integrates seamlessly with the JSR 303 (a.k.a. Bean Validation) specification - the standard Java system for validating objects. JSR 303 Bean Validation is generally easy to use, easy to understand, and is widely integrated into a variety of frameworks (or you can use it independently in a standalone way). You get to see the validation constraints in the model objects themselves without it getting in the way, and it's easy to create new constraint @@ -522,7 +534,7 @@ and integrate a few [reusable unit tests](#reusable_tests) into your Backstopper want to adjust Backstopper behavior you can usually determine where the hooks are and how to change things just by using breakpoints and exploring the code. * No need to refer to documentation for everything - the code and javadocs are usually sufficient. -* It should be easy to add integration for new frameworks. +* It should be easy to add an integration for new frameworks. * The core Backstopper functionality is free of framework dependencies and designed with hooks so that framework integrations can be as lightweight as possible. @@ -532,8 +544,8 @@ and integrate a few [reusable unit tests](#reusable_tests) into your Backstopper ### Backstopper Key Philosophies -Backstopper was based in large part on common elements found in the error contracts of API industry leaders circa 2014 ( -e.g. Facebook, Twitter, and others), and was therefore built with a few philosophies in mind. These are general +Backstopper was based in large part on common elements found in the error contracts of API industry leaders circa 2014 +(e.g. Facebook, Twitter, and others), and was therefore built with a few philosophies in mind. These are general guidelines - not everyone will agree with these ideas and there will always be legitimate exceptions even if you do agree. Therefore Backstopper should have hooks to allow you to override any of this behavior; if you notice an area where it's not possible to override the default behavior please file an issue and we'll see if there's a way to address @@ -541,7 +553,7 @@ it. * There should be a common error contract for *all* errors. APIs are intended to be used programmatically by callers, and changing contracts for different error types, HTTP status codes, etc, makes that programmatic integration more - difficult and error prone. Metadata and optional information can be added or removed at will, but the core error + difficult and error-prone. Metadata and optional information can be added or removed at will, but the core error contract should be static. * Since some error responses might need to contain multiple individual errors (e.g. validation of a request payload that contains multiple problems), the error contract should include an array of individual errors. From 6d966da1dd250d5fc7aecd007ea802d9ae66fb81 Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Thu, 12 Sep 2024 14:10:18 -0700 Subject: [PATCH 34/42] Optimize imports --- .../backstopper/service/ClientDataValidationServiceTest.java | 3 +-- .../springboot/componenttest/SanityCheckComponentTest.java | 3 +-- .../webflux/SpringWebfluxApiExceptionHandlerUtils.java | 1 - .../componenttest/BackstopperSpringWebFluxComponentTest.java | 5 ++--- .../handler/spring/SpringApiExceptionHandlerTest.java | 4 ---- ...OffSpringWebMvcFrameworkExceptionHandlerListenerTest.java | 1 - ...OffSpringCommonFrameworkExceptionHandlerListenerTest.java | 1 - .../config/SampleSpringboot3WebFluxSpringConfig.java | 1 - .../classpathscan/Spring_6_0_WebMvcClasspathScanConfig.java | 1 - .../classpathscan/Spring_6_1_WebMvcClasspathScanConfig.java | 1 - .../componenttest/spring/reusable/model/SampleModel.java | 3 +-- .../jettyserver/SpringMvcJettyComponentTestServer.java | 1 - .../componenttest/spring/reusable/model/SampleModel.java | 3 +-- .../spring/reusable/testutil/ExplodingServletFilter.java | 1 - 14 files changed, 6 insertions(+), 23 deletions(-) diff --git a/backstopper-core/src/test/java/com/nike/backstopper/service/ClientDataValidationServiceTest.java b/backstopper-core/src/test/java/com/nike/backstopper/service/ClientDataValidationServiceTest.java index a84cf11..29f09f2 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/service/ClientDataValidationServiceTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/service/ClientDataValidationServiceTest.java @@ -20,13 +20,12 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder; -import static org.mockito.BDDMockito.given; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.verifyNoMoreInteractions; /** * Verifies the functionality of {@link com.nike.backstopper.service.ClientDataValidationService} diff --git a/backstopper-spring-boot3-webmvc/src/test/java/com/nike/backstopper/handler/springboot/componenttest/SanityCheckComponentTest.java b/backstopper-spring-boot3-webmvc/src/test/java/com/nike/backstopper/handler/springboot/componenttest/SanityCheckComponentTest.java index fdb34ca..c598ccc 100644 --- a/backstopper-spring-boot3-webmvc/src/test/java/com/nike/backstopper/handler/springboot/componenttest/SanityCheckComponentTest.java +++ b/backstopper-spring-boot3-webmvc/src/test/java/com/nike/backstopper/handler/springboot/componenttest/SanityCheckComponentTest.java @@ -41,6 +41,7 @@ import java.util.Map; import java.util.UUID; +import io.restassured.response.ExtractableResponse; import jakarta.inject.Singleton; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -49,8 +50,6 @@ import jakarta.validation.Validation; import jakarta.validation.Validator; -import io.restassured.response.ExtractableResponse; - import static com.nike.backstopper.handler.springboot.componenttest.SanityCheckComponentTest.SanityCheckController.ERROR_THROWING_ENDPOINT_PATH; import static com.nike.backstopper.handler.springboot.componenttest.SanityCheckComponentTest.SanityCheckController.NON_ERROR_ENDPOINT_PATH; import static com.nike.backstopper.handler.springboot.componenttest.SanityCheckComponentTest.SanityCheckController.NON_ERROR_RESPONSE_PAYLOAD; diff --git a/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandlerUtils.java b/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandlerUtils.java index 1402c23..e0b3182 100644 --- a/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandlerUtils.java +++ b/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandlerUtils.java @@ -12,7 +12,6 @@ import jakarta.inject.Named; import jakarta.inject.Singleton; - import reactor.core.publisher.Mono; /** diff --git a/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/componenttest/BackstopperSpringWebFluxComponentTest.java b/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/componenttest/BackstopperSpringWebFluxComponentTest.java index 9fda339..c5cdb1d 100644 --- a/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/componenttest/BackstopperSpringWebFluxComponentTest.java +++ b/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/componenttest/BackstopperSpringWebFluxComponentTest.java @@ -83,13 +83,12 @@ import java.util.Map; import java.util.UUID; +import io.restassured.http.ContentType; +import io.restassured.response.ExtractableResponse; import jakarta.inject.Singleton; import jakarta.validation.Valid; import jakarta.validation.Validation; import jakarta.validation.Validator; - -import io.restassured.http.ContentType; -import io.restassured.response.ExtractableResponse; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; diff --git a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerTest.java b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerTest.java index 6fd14ca..4664fd8 100644 --- a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerTest.java +++ b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerTest.java @@ -13,11 +13,7 @@ import java.util.Collections; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; diff --git a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringWebMvcFrameworkExceptionHandlerListenerTest.java b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringWebMvcFrameworkExceptionHandlerListenerTest.java index 25e670f..4b8b111 100644 --- a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringWebMvcFrameworkExceptionHandlerListenerTest.java +++ b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringWebMvcFrameworkExceptionHandlerListenerTest.java @@ -24,7 +24,6 @@ import org.springframework.web.bind.ServletRequestBindingException; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import org.springframework.web.multipart.support.MissingServletRequestPartException; import java.lang.reflect.Method; diff --git a/backstopper-spring-web/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListenerTest.java b/backstopper-spring-web/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListenerTest.java index f75d72f..079a0bc 100644 --- a/backstopper-spring-web/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListenerTest.java +++ b/backstopper-spring-web/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListenerTest.java @@ -64,7 +64,6 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; diff --git a/samples/sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample/config/SampleSpringboot3WebFluxSpringConfig.java b/samples/sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample/config/SampleSpringboot3WebFluxSpringConfig.java index c192f59..00ab501 100644 --- a/samples/sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample/config/SampleSpringboot3WebFluxSpringConfig.java +++ b/samples/sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample/config/SampleSpringboot3WebFluxSpringConfig.java @@ -25,7 +25,6 @@ import jakarta.validation.Validation; import jakarta.validation.Validator; - import reactor.core.publisher.Mono; import static com.nike.backstopper.springboot3webfluxsample.controller.SampleController.SAMPLE_FROM_ROUTER_FUNCTION_PATH; diff --git a/testonly/testonly-spring-6_0-webmvc/src/test/java/serverconfig/classpathscan/Spring_6_0_WebMvcClasspathScanConfig.java b/testonly/testonly-spring-6_0-webmvc/src/test/java/serverconfig/classpathscan/Spring_6_0_WebMvcClasspathScanConfig.java index c194b14..886f20f 100644 --- a/testonly/testonly-spring-6_0-webmvc/src/test/java/serverconfig/classpathscan/Spring_6_0_WebMvcClasspathScanConfig.java +++ b/testonly/testonly-spring-6_0-webmvc/src/test/java/serverconfig/classpathscan/Spring_6_0_WebMvcClasspathScanConfig.java @@ -9,7 +9,6 @@ import jakarta.validation.Validation; import jakarta.validation.Validator; - import testonly.componenttest.spring.reusable.error.SampleProjectApiErrorsImpl; /** diff --git a/testonly/testonly-spring-6_1-webmvc/src/test/java/serverconfig/classpathscan/Spring_6_1_WebMvcClasspathScanConfig.java b/testonly/testonly-spring-6_1-webmvc/src/test/java/serverconfig/classpathscan/Spring_6_1_WebMvcClasspathScanConfig.java index 3a4e209..fb071d8 100644 --- a/testonly/testonly-spring-6_1-webmvc/src/test/java/serverconfig/classpathscan/Spring_6_1_WebMvcClasspathScanConfig.java +++ b/testonly/testonly-spring-6_1-webmvc/src/test/java/serverconfig/classpathscan/Spring_6_1_WebMvcClasspathScanConfig.java @@ -9,7 +9,6 @@ import jakarta.validation.Validation; import jakarta.validation.Validator; - import testonly.componenttest.spring.reusable.error.SampleProjectApiErrorsImpl; /** diff --git a/testonly/testonly-spring-webflux-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/model/SampleModel.java b/testonly/testonly-spring-webflux-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/model/SampleModel.java index 3396a43..bfdcb33 100644 --- a/testonly/testonly-spring-webflux-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/model/SampleModel.java +++ b/testonly/testonly-spring-webflux-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/model/SampleModel.java @@ -4,11 +4,10 @@ import com.nike.backstopper.apierror.sample.SampleCoreApiError; import com.nike.backstopper.validation.constraints.StringConvertsToClassType; -import jakarta.validation.constraints.NotBlank; import org.hibernate.validator.constraints.Range; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; - import testonly.componenttest.spring.reusable.error.SampleProjectApiError; import testonly.componenttest.spring.reusable.error.SampleProjectApiErrorsImpl; diff --git a/testonly/testonly-spring-webmvc-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/jettyserver/SpringMvcJettyComponentTestServer.java b/testonly/testonly-spring-webmvc-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/jettyserver/SpringMvcJettyComponentTestServer.java index c259524..fa6163c 100644 --- a/testonly/testonly-spring-webmvc-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/jettyserver/SpringMvcJettyComponentTestServer.java +++ b/testonly/testonly-spring-webmvc-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/jettyserver/SpringMvcJettyComponentTestServer.java @@ -13,7 +13,6 @@ import java.util.EnumSet; import jakarta.servlet.DispatcherType; - import testonly.componenttest.spring.reusable.testutil.ExplodingServletFilter; /** diff --git a/testonly/testonly-spring-webmvc-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/model/SampleModel.java b/testonly/testonly-spring-webmvc-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/model/SampleModel.java index 3396a43..bfdcb33 100644 --- a/testonly/testonly-spring-webmvc-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/model/SampleModel.java +++ b/testonly/testonly-spring-webmvc-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/model/SampleModel.java @@ -4,11 +4,10 @@ import com.nike.backstopper.apierror.sample.SampleCoreApiError; import com.nike.backstopper.validation.constraints.StringConvertsToClassType; -import jakarta.validation.constraints.NotBlank; import org.hibernate.validator.constraints.Range; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; - import testonly.componenttest.spring.reusable.error.SampleProjectApiError; import testonly.componenttest.spring.reusable.error.SampleProjectApiErrorsImpl; diff --git a/testonly/testonly-spring-webmvc-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/testutil/ExplodingServletFilter.java b/testonly/testonly-spring-webmvc-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/testutil/ExplodingServletFilter.java index 0a2f650..712fb0d 100644 --- a/testonly/testonly-spring-webmvc-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/testutil/ExplodingServletFilter.java +++ b/testonly/testonly-spring-webmvc-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/testutil/ExplodingServletFilter.java @@ -10,7 +10,6 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; - import testonly.componenttest.spring.reusable.error.SampleProjectApiError; /** From 84cebd372f6ebdaaef22653b36d98c55bacc27a0 Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Thu, 12 Sep 2024 15:36:36 -0700 Subject: [PATCH 35/42] IDE automated cleanup --- .../backstopper/apierror/ApiErrorBase.java | 2 +- .../projectspecificinfo/ProjectApiErrors.java | 4 +-- .../apierror/sample/SampleCoreApiError.java | 4 +-- .../sample/SampleProjectApiErrorsBase.java | 2 +- .../backstopper/exception/ApiException.java | 6 ++-- .../handler/ApiExceptionHandlerBase.java | 5 ++- .../GenericApiExceptionHandlerListener.java | 4 +-- .../backstopper/model/DefaultErrorDTO.java | 2 +- .../nike/backstopper/util/ApiErrorUtil.java | 3 +- .../apierror/ApiErrorBaseTest.java | 2 +- .../apierror/ApiErrorComparatorTest.java | 4 +-- .../apierror/ApiErrorWithMetadataTest.java | 6 ++-- .../apierror/SortedApiErrorSetTest.java | 6 ++-- .../ProjectApiErrorsTestBase.java | 12 +++---- .../range/IntegerRangeTest.java | 2 +- .../BarebonesCoreApiErrorForTesting.java | 2 +- .../testutil/ProjectApiErrorsForTesting.java | 2 +- .../exception/ApiExceptionTest.java | 32 ++++++++--------- .../handler/ApiExceptionHandlerBaseTest.java | 11 +++--- .../handler/ApiExceptionHandlerUtilsTest.java | 31 +++++++++-------- .../handler/ErrorResponseInfoTest.java | 6 +++- .../UnhandledExceptionHandlerBaseTest.java | 8 ++--- ...ApiExceptionHandlerListenerResultTest.java | 6 ++-- ...ataValidationErrorHandlerListenerTest.java | 34 ++++++++++++------- ...amNetworkExceptionHandlerListenerTest.java | 2 +- ...enericApiExceptionHandlerListenerTest.java | 2 +- ...ideValidationErrorHandlerListenerTest.java | 8 ++--- .../model/DefaultErrorContractDTOTest.java | 6 ++-- .../ClientDataValidationServiceTest.java | 21 ++++++------ ...ilFastServersideValidationServiceTest.java | 2 +- .../service/NoOpJsr303ValidatorTest.java | 4 +-- .../StringConvertsToClassTypeValidator.java | 12 ++----- ...tringConvertsToClassTypeValidatorTest.java | 10 +++--- ...tilWithDefaultErrorContractDTOSupport.java | 6 ++-- ...ithDefaultErrorContractDTOSupportTest.java | 2 +- ...equestInfoForLoggingServletApiAdapter.java | 2 +- ...ApiExceptionHandlerServletApiBaseTest.java | 11 ++++-- ...ledExceptionHandlerServletApiBaseTest.java | 9 ++++- ...stInfoForLoggingServletApiAdapterTest.java | 8 ++--- .../SanityCheckComponentTest.java | 2 +- ...BackstopperSpringWebFluxComponentTest.java | 2 +- ...FrameworkExceptionHandlerListenerTest.java | 2 +- ...bMvcFrameworkExceptionHandlerListener.java | 14 ++++---- .../handler/ClientfacingErrorITest.java | 4 +-- .../spring/SpringApiExceptionHandlerTest.java | 2 +- .../SpringApiExceptionHandlerUtilsTest.java | 4 +-- .../SpringUnhandledExceptionHandlerTest.java | 2 +- ...FrameworkExceptionHandlerListenerTest.java | 6 ++-- ...idationErrorToApiErrorHandlerListener.java | 3 +- ...mmonFrameworkExceptionHandlerListener.java | 24 +++++-------- ...FrameworkExceptionHandlerListenerTest.java | 8 ++--- .../java/com/nike/internal/util/Pair.java | 5 ++- .../com/nike/internal/util/StringUtils.java | 4 +-- .../error/SampleProjectApiError.java | 4 +-- .../error/SampleProjectApiError.java | 4 +-- .../reusable/error/SampleProjectApiError.java | 4 +-- 56 files changed, 196 insertions(+), 199 deletions(-) diff --git a/backstopper-core/src/main/java/com/nike/backstopper/apierror/ApiErrorBase.java b/backstopper-core/src/main/java/com/nike/backstopper/apierror/ApiErrorBase.java index 2b2d19e..dd8246b 100644 --- a/backstopper-core/src/main/java/com/nike/backstopper/apierror/ApiErrorBase.java +++ b/backstopper-core/src/main/java/com/nike/backstopper/apierror/ApiErrorBase.java @@ -41,7 +41,7 @@ public ApiErrorBase(String name, String errorCode, String message, int httpStatu } this.metadata = (metadata.isEmpty()) - ? Collections.emptyMap() + ? Collections.emptyMap() : Collections.unmodifiableMap(new HashMap<>(metadata)); } diff --git a/backstopper-core/src/main/java/com/nike/backstopper/apierror/projectspecificinfo/ProjectApiErrors.java b/backstopper-core/src/main/java/com/nike/backstopper/apierror/projectspecificinfo/ProjectApiErrors.java index f25a422..4045b33 100644 --- a/backstopper-core/src/main/java/com/nike/backstopper/apierror/projectspecificinfo/ProjectApiErrors.java +++ b/backstopper-core/src/main/java/com/nike/backstopper/apierror/projectspecificinfo/ProjectApiErrors.java @@ -336,7 +336,7 @@ public List getStatusCodePriorityOrder() { * the possibility of null being returned and have a strategy for picking a winner. */ public Integer determineHighestPriorityHttpStatusCode(Collection apiErrors) { - if (apiErrors == null || apiErrors.size() == 0) { + if (apiErrors == null || apiErrors.isEmpty()) { return null; } @@ -368,7 +368,7 @@ public Integer determineHighestPriorityHttpStatusCode(Collection apiEr logger.error( "None of the HTTP status codes in the ApiErrors passed to determineHighestPriorityHttpStatusCode() were" + " found in the getStatusCodePriorityOrder() list. Offending set of http status codes (these should be " - + "added to the getStatusCodePriorityOrder() list for this project): " + validStatusCodePossibilities + + "added to the getStatusCodePriorityOrder() list for this project): {}", validStatusCodePossibilities ); return null; diff --git a/backstopper-core/src/main/java/com/nike/backstopper/apierror/sample/SampleCoreApiError.java b/backstopper-core/src/main/java/com/nike/backstopper/apierror/sample/SampleCoreApiError.java index c3825e1..6741e0e 100644 --- a/backstopper-core/src/main/java/com/nike/backstopper/apierror/sample/SampleCoreApiError.java +++ b/backstopper-core/src/main/java/com/nike/backstopper/apierror/sample/SampleCoreApiError.java @@ -20,7 +20,7 @@ /** * A sample/example of some core errors that many APIs are likely to need. Any given {@link ProjectApiErrors} could - * return these as its {@link ProjectApiErrors#getCoreApiErrors()} if the error codes and messages associated with these + * return these as its {@code ProjectApiErrors#getCoreApiErrors()} if the error codes and messages associated with these * are fine for your project (see {@link SampleProjectApiErrorsBase} for a base implementation that does exactly that). * *

In practice most organizations should copy/paste this class and customize the error codes and messages for their @@ -64,7 +64,7 @@ public enum SampleCoreApiError implements ApiError { SampleCoreApiError(int errorCode, String message, int httpStatusCode) { this(new ApiErrorBase( - "delegated-to-enum-wrapper-" + UUID.randomUUID().toString(), errorCode, message, httpStatusCode + "delegated-to-enum-wrapper-" + UUID.randomUUID(), errorCode, message, httpStatusCode )); } diff --git a/backstopper-core/src/main/java/com/nike/backstopper/apierror/sample/SampleProjectApiErrorsBase.java b/backstopper-core/src/main/java/com/nike/backstopper/apierror/sample/SampleProjectApiErrorsBase.java index 2eaf163..c732d65 100644 --- a/backstopper-core/src/main/java/com/nike/backstopper/apierror/sample/SampleProjectApiErrorsBase.java +++ b/backstopper-core/src/main/java/com/nike/backstopper/apierror/sample/SampleProjectApiErrorsBase.java @@ -26,7 +26,7 @@ public abstract class SampleProjectApiErrorsBase extends ProjectApiErrors { private static final List SAMPLE_CORE_API_ERRORS_AS_LIST = - Arrays.asList(SampleCoreApiError.values()); + Arrays.asList(SampleCoreApiError.values()); @Override protected List getCoreApiErrors() { diff --git a/backstopper-core/src/main/java/com/nike/backstopper/exception/ApiException.java b/backstopper-core/src/main/java/com/nike/backstopper/exception/ApiException.java index 6c5109a..51a2c43 100644 --- a/backstopper-core/src/main/java/com/nike/backstopper/exception/ApiException.java +++ b/backstopper-core/src/main/java/com/nike/backstopper/exception/ApiException.java @@ -253,9 +253,9 @@ public StackTraceLoggingBehavior getStackTraceLoggingBehavior() { */ @SuppressWarnings("WeakerAccess") public static class Builder { - private List apiErrors = new ArrayList<>(); - private List> extraDetailsForLogging = new ArrayList<>(); - private List>> extraResponseHeaders = new ArrayList<>(); + private final List apiErrors = new ArrayList<>(); + private final List> extraDetailsForLogging = new ArrayList<>(); + private final List>> extraResponseHeaders = new ArrayList<>(); private String message; private Throwable cause; private StackTraceLoggingBehavior stackTraceLoggingBehavior; diff --git a/backstopper-core/src/main/java/com/nike/backstopper/handler/ApiExceptionHandlerBase.java b/backstopper-core/src/main/java/com/nike/backstopper/handler/ApiExceptionHandlerBase.java index f14b312..219ae8e 100644 --- a/backstopper-core/src/main/java/com/nike/backstopper/handler/ApiExceptionHandlerBase.java +++ b/backstopper-core/src/main/java/com/nike/backstopper/handler/ApiExceptionHandlerBase.java @@ -280,8 +280,7 @@ protected ErrorResponseInfo doHandleApiException( // Add connection type to our extra logging data if appropriate. This particular log message is here so it can // be done in one spot rather than trying to track down all the different places we're handling // NetworkExceptionBase subclasses (and possibly missing some by accident). - if (coreException instanceof NetworkExceptionBase) { - NetworkExceptionBase neb = ((NetworkExceptionBase)coreException); + if (coreException instanceof NetworkExceptionBase neb) { extraDetailsForLogging.add(Pair.of("connection_type", neb.getConnectionType())); } @@ -306,7 +305,7 @@ protected ErrorResponseInfo doHandleApiException( + "investigated and fixed. Search for %s=%s in the logs to find the log message that contains the " + "details of the request along with the full stack trace of the original exception. " + "unfiltered_api_errors=%s", - trackingLogKey, trackingUuid.toString(), utils.concatenateErrorCollection(clientErrors) + trackingLogKey, trackingUuid, utils.concatenateErrorCollection(clientErrors) )); filteredClientErrors = Collections.singletonList(genericServiceError); highestPriorityStatusCode = genericServiceError.getHttpStatusCode(); diff --git a/backstopper-core/src/main/java/com/nike/backstopper/handler/listener/impl/GenericApiExceptionHandlerListener.java b/backstopper-core/src/main/java/com/nike/backstopper/handler/listener/impl/GenericApiExceptionHandlerListener.java index 74e5af6..ba9a496 100644 --- a/backstopper-core/src/main/java/com/nike/backstopper/handler/listener/impl/GenericApiExceptionHandlerListener.java +++ b/backstopper-core/src/main/java/com/nike/backstopper/handler/listener/impl/GenericApiExceptionHandlerListener.java @@ -24,11 +24,9 @@ public class GenericApiExceptionHandlerListener implements ApiExceptionHandlerLi @Override public ApiExceptionHandlerListenerResult shouldHandleException(Throwable ex) { // We only care about ApiExceptions. - if (!(ex instanceof ApiException)) + if (!(ex instanceof ApiException apiException)) return ApiExceptionHandlerListenerResult.ignoreResponse(); - ApiException apiException = ((ApiException)ex); - // Add all the ApiErrors from the exception. SortedApiErrorSet errors = new SortedApiErrorSet(); errors.addAll(apiException.getApiErrors()); diff --git a/backstopper-core/src/main/java/com/nike/backstopper/model/DefaultErrorDTO.java b/backstopper-core/src/main/java/com/nike/backstopper/model/DefaultErrorDTO.java index 29a2476..9403a39 100644 --- a/backstopper-core/src/main/java/com/nike/backstopper/model/DefaultErrorDTO.java +++ b/backstopper-core/src/main/java/com/nike/backstopper/model/DefaultErrorDTO.java @@ -80,7 +80,7 @@ public DefaultErrorDTO(String code, String message, Map metadata if (metadata == null) metadata = Collections.emptyMap(); this.metadata = (metadata.isEmpty()) - ? Collections.emptyMap() + ? Collections.emptyMap() : Collections.unmodifiableMap(new HashMap<>(metadata)); } } diff --git a/backstopper-core/src/main/java/com/nike/backstopper/util/ApiErrorUtil.java b/backstopper-core/src/main/java/com/nike/backstopper/util/ApiErrorUtil.java index e649922..4ab7d1e 100644 --- a/backstopper-core/src/main/java/com/nike/backstopper/util/ApiErrorUtil.java +++ b/backstopper-core/src/main/java/com/nike/backstopper/util/ApiErrorUtil.java @@ -25,8 +25,7 @@ public static int generateApiErrorHashCode(ApiError apiError) { public static boolean isApiErrorEqual(ApiError apiError, Object o) { if (apiError == o) return true; if (apiError == null) return false; - if (o == null || !(o instanceof ApiError)) return false; - ApiError that = (ApiError) o; + if (o == null || !(o instanceof ApiError that)) return false; return apiError.getHttpStatusCode() == that.getHttpStatusCode() && Objects.equals(apiError.getName(), that.getName()) && Objects.equals(apiError.getErrorCode(), that.getErrorCode()) && diff --git a/backstopper-core/src/test/java/com/nike/backstopper/apierror/ApiErrorBaseTest.java b/backstopper-core/src/test/java/com/nike/backstopper/apierror/ApiErrorBaseTest.java index 5a85f90..1b4828b 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/apierror/ApiErrorBaseTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/apierror/ApiErrorBaseTest.java @@ -74,7 +74,7 @@ public void noMetadataStringErrorCodeConstructor_creates_as_expected() { // then assertThat(aeb.getName()).isEqualTo(name); - assertThat(aeb.getErrorCode()).isEqualTo(String.valueOf(errorCode)); + assertThat(aeb.getErrorCode()).isEqualTo(errorCode); assertThat(aeb.getMessage()).isEqualTo(message); assertThat(aeb.getHttpStatusCode()).isEqualTo(httpStatusCode); } diff --git a/backstopper-core/src/test/java/com/nike/backstopper/apierror/ApiErrorComparatorTest.java b/backstopper-core/src/test/java/com/nike/backstopper/apierror/ApiErrorComparatorTest.java index 6453702..33b7468 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/apierror/ApiErrorComparatorTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/apierror/ApiErrorComparatorTest.java @@ -19,7 +19,7 @@ * Tests the functionality of {@link com.nike.backstopper.apierror.ApiErrorComparator} */ public class ApiErrorComparatorTest { - private ApiErrorComparator comparator = new ApiErrorComparator(); + private final ApiErrorComparator comparator = new ApiErrorComparator(); @Test public void should_return_0_for_reference_equality() { @@ -109,7 +109,7 @@ public void should_return_0_if_names_and_metadata_are_equal() { public void should_use_hashCode_comparison_when_names_are_equal_and_metadata_is_different() { // given ApiError apiError = new ApiErrorBase(UUID.randomUUID().toString(), 42, "foo", 400); - ApiError errorWithMetadata = new ApiErrorWithMetadata(apiError, Pair.of("bar", (Object)UUID.randomUUID().toString())); + ApiError errorWithMetadata = new ApiErrorWithMetadata(apiError, Pair.of("bar", UUID.randomUUID().toString())); assertThat(apiError.hashCode()).isNotEqualTo(errorWithMetadata.hashCode()); // when diff --git a/backstopper-core/src/test/java/com/nike/backstopper/apierror/ApiErrorWithMetadataTest.java b/backstopper-core/src/test/java/com/nike/backstopper/apierror/ApiErrorWithMetadataTest.java index 61bb3ec..3117b57 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/apierror/ApiErrorWithMetadataTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/apierror/ApiErrorWithMetadataTest.java @@ -97,7 +97,7 @@ public void constructor_throws_IllegalArgumentException_if_delegate_is_null() { @Test public void convenience_constructor_throws_IllegalArgumentException_if_delegate_is_null() { // when - Throwable ex = catchThrowable(() -> new ApiErrorWithMetadata(null, Pair.of("foo", (Object)"bar"))); + Throwable ex = catchThrowable(() -> new ApiErrorWithMetadata(null, Pair.of("foo", "bar"))); // then assertThat(ex) @@ -152,7 +152,7 @@ public void convenience_constructor_supports_delegate_with_null_or_empty_metadat @Test public void constructor_supports_null_or_empty_extra_metadata(boolean useNull) { // given - Map extraMetadataToUse = (useNull) ? null : Collections.emptyMap(); + Map extraMetadataToUse = (useNull) ? null : Collections.emptyMap(); // when ApiErrorWithMetadata awm = new ApiErrorWithMetadata(delegateWithMetadata, extraMetadataToUse); @@ -270,7 +270,7 @@ public void equals_returns_expected_result(boolean changeName, boolean changeErr changeMetadata? metadata2 : metadata); ApiErrorWithMetadata awm = new ApiErrorWithMetadata(aeb, extraMetadata); - ApiErrorWithMetadata awm2 = new ApiErrorWithMetadata(aeb2, hasExtraMetadata? extraMetadata : Collections.emptyMap()); + ApiErrorWithMetadata awm2 = new ApiErrorWithMetadata(aeb2, hasExtraMetadata? extraMetadata : Collections.emptyMap()); // then assertThat(awm.equals(awm2)).isEqualTo(isEqual); diff --git a/backstopper-core/src/test/java/com/nike/backstopper/apierror/SortedApiErrorSetTest.java b/backstopper-core/src/test/java/com/nike/backstopper/apierror/SortedApiErrorSetTest.java index 6510767..eb556b4 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/apierror/SortedApiErrorSetTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/apierror/SortedApiErrorSetTest.java @@ -41,7 +41,7 @@ public void default_constructor_uses_ApiErrorComparator() { assertThat(set).isEmpty(); } - private Random random = new Random(); + private final Random random = new Random(); private ApiError generateRandomApiError() { return new ApiErrorBase(UUID.randomUUID().toString(), random.nextInt(), UUID.randomUUID().toString(), random.nextInt()); @@ -128,8 +128,8 @@ public void singletonSortedSetOf_returns_singleton_set_with_supplied_arg_and_def public void default_config_supports_multiple_errors_that_differ_only_by_metadata() { // given ApiError baseError = new ApiErrorBase(UUID.randomUUID().toString(), 42, "foo", 400); - ApiError errorWithMetadata1 = new ApiErrorWithMetadata(baseError, Pair.of("foo", (Object)"bar")); - ApiError errorWithMetadata2 = new ApiErrorWithMetadata(baseError, Pair.of("foo", (Object)"notbar")); + ApiError errorWithMetadata1 = new ApiErrorWithMetadata(baseError, Pair.of("foo", "bar")); + ApiError errorWithMetadata2 = new ApiErrorWithMetadata(baseError, Pair.of("foo", "notbar")); SortedApiErrorSet set = new SortedApiErrorSet(); assertThat(set).isEmpty(); diff --git a/backstopper-core/src/test/java/com/nike/backstopper/apierror/projectspecificinfo/ProjectApiErrorsTestBase.java b/backstopper-core/src/test/java/com/nike/backstopper/apierror/projectspecificinfo/ProjectApiErrorsTestBase.java index b12b3eb..1d462af 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/apierror/projectspecificinfo/ProjectApiErrorsTestBase.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/apierror/projectspecificinfo/ProjectApiErrorsTestBase.java @@ -65,7 +65,7 @@ public void determineHighestPriorityHttpStatusCodeShouldReturnNullForNullErrorCo @Test public void determineHighestPriorityHttpStatusCodeShouldReturnNullForEmptyErrorCollection() { - assertThat(getProjectApiErrors().determineHighestPriorityHttpStatusCode(Collections.emptyList()), nullValue()); + assertThat(getProjectApiErrors().determineHighestPriorityHttpStatusCode(Collections.emptyList()), nullValue()); } @Test @@ -200,7 +200,7 @@ public void convertToApiErrorShouldUseFallbackOnInvalidValue() { @Test(expected = IllegalStateException.class) public void verifyErrorsAreInRangeShouldThrowExceptionIfListIncludesNonCoreApiErrorAndRangeIsNull() { - ProjectApiErrorsForTesting.withProjectSpecificData(Collections.singletonList(new ApiErrorBase("blah", 99001, "stuff", 400)), null); + ProjectApiErrorsForTesting.withProjectSpecificData(Collections.singletonList(new ApiErrorBase("blah", 99001, "stuff", 400)), null); } @Test @@ -224,7 +224,7 @@ public void verifyErrorsAreInRangeShouldNotThrowExceptionIfListIncludesCoreApiEr @Test(expected = IllegalStateException.class) public void verifyErrorsAreInRangeShouldThrowExceptionIfListIncludesErrorOutOfRange() { ProjectApiErrorsForTesting.withProjectSpecificData( - Collections.singletonList(new ApiErrorBase("blah", 1, "stuff", 400)), + Collections.singletonList(new ApiErrorBase("blah", 1, "stuff", 400)), new ProjectSpecificErrorCodeRange() { @Override public boolean isInRange(ApiError error) { @@ -305,11 +305,7 @@ private boolean areWrappersOfEachOther(ApiError error1, ApiError error2) { boolean errorCodeMatches = Objects.equals(error1.getErrorCode(), error2.getErrorCode()); boolean messageMatches = Objects.equals(error1.getMessage(), error2.getMessage()); boolean httpStatusCodeMatches = error1.getHttpStatusCode() == error2.getHttpStatusCode(); - if (errorCodeMatches && messageMatches && httpStatusCodeMatches) { - return true; - } - - return false; + return errorCodeMatches && messageMatches && httpStatusCodeMatches; } @Test diff --git a/backstopper-core/src/test/java/com/nike/backstopper/apierror/projectspecificinfo/range/IntegerRangeTest.java b/backstopper-core/src/test/java/com/nike/backstopper/apierror/projectspecificinfo/range/IntegerRangeTest.java index 33c01fb..911e13f 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/apierror/projectspecificinfo/range/IntegerRangeTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/apierror/projectspecificinfo/range/IntegerRangeTest.java @@ -18,7 +18,7 @@ @RunWith(DataProviderRunner.class) public class IntegerRangeTest { - private IntegerRange rangeOfOneToFour = IntegerRange.of(1, 4); + private final IntegerRange rangeOfOneToFour = IntegerRange.of(1, 4); @Test public void constructor_throws_exception_if_upper_range_less_than_lower_range() { diff --git a/backstopper-core/src/test/java/com/nike/backstopper/apierror/testutil/BarebonesCoreApiErrorForTesting.java b/backstopper-core/src/test/java/com/nike/backstopper/apierror/testutil/BarebonesCoreApiErrorForTesting.java index 9984eef..07cf64c 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/apierror/testutil/BarebonesCoreApiErrorForTesting.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/apierror/testutil/BarebonesCoreApiErrorForTesting.java @@ -49,7 +49,7 @@ public enum BarebonesCoreApiErrorForTesting implements ApiError { } BarebonesCoreApiErrorForTesting(int errorCode, String message, int httpStatusCode) { - this(new ApiErrorBase("delegated-to-enum-wrapper-" + UUID.randomUUID().toString(), errorCode, message, httpStatusCode)); + this(new ApiErrorBase("delegated-to-enum-wrapper-" + UUID.randomUUID(), errorCode, message, httpStatusCode)); } @Override diff --git a/backstopper-core/src/test/java/com/nike/backstopper/apierror/testutil/ProjectApiErrorsForTesting.java b/backstopper-core/src/test/java/com/nike/backstopper/apierror/testutil/ProjectApiErrorsForTesting.java index 91cd7a1..a181bac 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/apierror/testutil/ProjectApiErrorsForTesting.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/apierror/testutil/ProjectApiErrorsForTesting.java @@ -16,7 +16,7 @@ */ public abstract class ProjectApiErrorsForTesting extends ProjectApiErrors { - private static final List BAREBONES_CORE_API_ERRORS_AS_LIST = Arrays.asList(BarebonesCoreApiErrorForTesting.values()); + private static final List BAREBONES_CORE_API_ERRORS_AS_LIST = Arrays.asList(BarebonesCoreApiErrorForTesting.values()); public static ProjectApiErrorsForTesting withProjectSpecificData(final List projectSpecificErrors, final ProjectSpecificErrorCodeRange projectSpecificErrorCodeRange) { diff --git a/backstopper-core/src/test/java/com/nike/backstopper/exception/ApiExceptionTest.java b/backstopper-core/src/test/java/com/nike/backstopper/exception/ApiExceptionTest.java index 5393374..2857731 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/exception/ApiExceptionTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/exception/ApiExceptionTest.java @@ -27,23 +27,23 @@ @RunWith(DataProviderRunner.class) public class ApiExceptionTest { - private ApiError apiError1 = BarebonesCoreApiErrorForTesting.GENERIC_SERVICE_ERROR; - private ApiError apiError2 = BarebonesCoreApiErrorForTesting.SERVERSIDE_VALIDATION_ERROR; - private ApiError apiError3 = BarebonesCoreApiErrorForTesting.TYPE_CONVERSION_ERROR; - private ApiError apiError4 = BarebonesCoreApiErrorForTesting.OUTSIDE_DEPENDENCY_RETURNED_AN_UNRECOVERABLE_ERROR; + private final ApiError apiError1 = BarebonesCoreApiErrorForTesting.GENERIC_SERVICE_ERROR; + private final ApiError apiError2 = BarebonesCoreApiErrorForTesting.SERVERSIDE_VALIDATION_ERROR; + private final ApiError apiError3 = BarebonesCoreApiErrorForTesting.TYPE_CONVERSION_ERROR; + private final ApiError apiError4 = BarebonesCoreApiErrorForTesting.OUTSIDE_DEPENDENCY_RETURNED_AN_UNRECOVERABLE_ERROR; - private Pair logPair1 = Pair.of("key1", "val1"); - private Pair logPair2 = Pair.of("key2", "val2"); - private Pair logPair3 = Pair.of("key3", "val3"); - private Pair logPair4 = Pair.of("key4", "val4"); + private final Pair logPair1 = Pair.of("key1", "val1"); + private final Pair logPair2 = Pair.of("key2", "val2"); + private final Pair logPair3 = Pair.of("key3", "val3"); + private final Pair logPair4 = Pair.of("key4", "val4"); - private Pair> headerPair1 = Pair.of("h1", singletonList("v1")); - private Pair> headerPair2 = Pair.of("h2", Arrays.asList("v2.1", "v2.2")); - private Pair> headerPair3 = Pair.of("h3", singletonList("v3")); - private Pair> headerPair4 = Pair.of("h4", Arrays.asList("v4.1", "v4.2")); + private final Pair> headerPair1 = Pair.of("h1", singletonList("v1")); + private final Pair> headerPair2 = Pair.of("h2", Arrays.asList("v2.1", "v2.2")); + private final Pair> headerPair3 = Pair.of("h3", singletonList("v3")); + private final Pair> headerPair4 = Pair.of("h4", Arrays.asList("v4.1", "v4.2")); - private String exceptionMessage = "some ex msg"; - private Exception cause = new Exception("intentional test exception"); + private final String exceptionMessage = "some ex msg"; + private final Exception cause = new Exception("intentional test exception"); @DataProvider(value = { "true | FORCE_STACK_TRACE", @@ -188,7 +188,7 @@ public void no_cause_constructors_fail_when_passed_null_or_empty_apiErrors_list( // given List> logInfoList = Collections.emptyList(); List>> responseHeaders = Collections.emptyList(); - List apiErrors = (useNull) ? null : Collections.emptyList(); + List apiErrors = (useNull) ? null : Collections.emptyList(); // expect if (useConstructorWithResponseHeaders) @@ -210,7 +210,7 @@ public void with_cause_constructors_fail_when_passed_null_or_empty_apiErrors_lis // given List> logInfoList = Collections.emptyList(); List>> responseHeaders = Collections.emptyList(); - List apiErrors = (useNull) ? null : Collections.emptyList(); + List apiErrors = (useNull) ? null : Collections.emptyList(); // expect if (useConstructorWithResponseHeaders) diff --git a/backstopper-core/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerBaseTest.java b/backstopper-core/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerBaseTest.java index 9db4930..f625682 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerBaseTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerBaseTest.java @@ -262,9 +262,9 @@ public void shouldFilterOutLowerPriorityErrorsWhenGivenErrorsWithMixedHttpStatus @Test public void shouldDeduplicateRepeatedErrors() throws UnexpectedMajorExceptionHandlingError { - List repeatedErrors = Arrays.asList(testProjectApiErrors.getGenericServiceError(), testProjectApiErrors.getGenericServiceError(), testProjectApiErrors.getGenericServiceError()); + List repeatedErrors = Arrays.asList(testProjectApiErrors.getGenericServiceError(), testProjectApiErrors.getGenericServiceError(), testProjectApiErrors.getGenericServiceError()); ErrorResponseInfo result = handler.maybeHandleException(ApiException.newBuilder().withApiErrors(repeatedErrors).build(), reqMock); - validateResponse(result, Arrays.asList((ApiError)testProjectApiErrors.getGenericServiceError())); + validateResponse(result, singletonList(testProjectApiErrors.getGenericServiceError())); } @Test @@ -337,7 +337,8 @@ public void handleExceptionShouldAddErrorIdToResponseHeader() { ApiExceptionHandlerBase handler = new TestApiExceptionHandler(); ErrorResponseInfo result = handler.doHandleApiException(singletonSortedSetOf(CUSTOM_API_ERROR), new ArrayList>(), null, new Exception(), reqMock); - assertThat(result.headersToAddToResponse.get("error_uid"), is(Arrays.asList(result.frameworkRepresentationObj.erv.error_id))); + assertThat(result.headersToAddToResponse.get("error_uid"), is( + singletonList(result.frameworkRepresentationObj.erv.error_id))); } @Test @@ -629,7 +630,7 @@ public void unwrapAndFindCoreException_returns_passed_in_arg_if_exception_has_ca } private static class CustomExceptionOfDoom extends Exception { } - private static ApiError CUSTOM_API_ERROR = new ApiErrorBase("CUSTOM_API_ERROR", 99042, "some message", 400); + private static final ApiError CUSTOM_API_ERROR = new ApiErrorBase("CUSTOM_API_ERROR", 99042, "some message", 400); private static class CustomExceptionOfDoomHandlerListener implements ApiExceptionHandlerListener { @Override @@ -652,7 +653,7 @@ public void shouldIgnoreUnknownExceptionTypes() throws UnexpectedMajorExceptionH @Test public void shouldUseCustomHandlerListenersIfSet() throws UnexpectedMajorExceptionHandlingError { ErrorResponseInfo result = handler.maybeHandleException(new CustomExceptionOfDoom(), reqMock); - validateResponse(result, Arrays.asList(CUSTOM_API_ERROR)); + validateResponse(result, singletonList(CUSTOM_API_ERROR)); } private static class TestApiExceptionHandler extends ApiExceptionHandlerBase { diff --git a/backstopper-core/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerUtilsTest.java b/backstopper-core/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerUtilsTest.java index e92e222..83868f6 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerUtilsTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerUtilsTest.java @@ -51,7 +51,7 @@ public class ApiExceptionHandlerUtilsTest { private RequestInfoForLogging reqMock; private static final ProjectApiErrors testProjectApiErrors = ProjectApiErrorsForTesting.withProjectSpecificData(null, null); - private ApiExceptionHandlerUtils impl = DEFAULT_IMPL; + private final ApiExceptionHandlerUtils impl = DEFAULT_IMPL; @Before public void setupMethod() { @@ -172,7 +172,7 @@ private void verifyBuildErrorMessageForLogs(boolean requestHasDtraceId, String d when(reqMock.getHeader(fh.headerName)).thenReturn(fh.headerValues.get(0)); } - List contributingErrors = Arrays.asList(BarebonesCoreApiErrorForTesting.GENERIC_SERVICE_ERROR, BarebonesCoreApiErrorForTesting.GENERIC_BAD_REQUEST); + List contributingErrors = Arrays.asList(BarebonesCoreApiErrorForTesting.GENERIC_SERVICE_ERROR, BarebonesCoreApiErrorForTesting.GENERIC_BAD_REQUEST); int httpStatusCodeToUse = testProjectApiErrors.determineHighestPriorityHttpStatusCode(contributingErrors); Exception exceptionCause = new Exception(); @@ -217,7 +217,7 @@ private void verifyBuildErrorMessageForLogs(boolean requestHasDtraceId, String d @Test public void buildErrorMessageForLogsReturnsExpectedStringForMissingDtraceId() { verifyBuildErrorMessageForLogs(false, null, null); - verifyBuildErrorMessageForLogs(false, null, Arrays.asList(Pair.of("foo", "bar"))); + verifyBuildErrorMessageForLogs(false, null, List.of(Pair.of("foo", "bar"))); } @Test @@ -316,7 +316,7 @@ public void buildErrorMessageForLogs_includes_orig_forwarded_request_uri_when_av @Test public void parseSpecificHeaderToStringShouldWorkForHappyPathWithOneHeaderVal() { - when(reqMock.getHeaders("foo")).thenReturn(Arrays.asList("fooval")); + when(reqMock.getHeaders("foo")).thenReturn(List.of("fooval")); String result = impl.parseSpecificHeaderToString(reqMock, "foo"); assertThat(result, is("foo=fooval")); @@ -341,7 +341,7 @@ public void parseSpecificHeaderToStringShouldReturnBlankStringIfHeadersIsNull() @Test public void parseSpecificHeaderToStringShouldReturnBlankStringIfHeadersIsEmpty() { - when(reqMock.getHeaders("foo")).thenReturn(Collections.emptyList()); + when(reqMock.getHeaders("foo")).thenReturn(Collections.emptyList()); String result = impl.parseSpecificHeaderToString(reqMock, "foo"); assertThat(result, is("")); @@ -360,10 +360,10 @@ public void parseSpecificHeaderToStringShouldReturnBlankStringIfUnexpectedExcept @Test public void parseRequestHeadersToStringShouldWorkForHappyPath() { when(reqMock.getHeadersMap()).thenReturn(new TreeMap<>(MapBuilder.>builder() - .put("header1", Arrays.asList("h1val")) + .put("header1", List.of("h1val")) .put("header2", Arrays.asList("h2val1", "h2val2")) .build())); - when(reqMock.getHeaders("header1")).thenReturn(Arrays.asList("h1val")); + when(reqMock.getHeaders("header1")).thenReturn(List.of("h1val")); when(reqMock.getHeaders("header2")).thenReturn(Arrays.asList("h2val1", "h2val2")); String result = impl.parseRequestHeadersToString(reqMock); @@ -374,11 +374,11 @@ public void parseRequestHeadersToStringShouldWorkForHappyPath() { public void parseRequestHeadersToStringShouldWorkForSpecialHeadersPath() { ApiExceptionHandlerUtils customImpl = new ApiExceptionHandlerUtils(true, new HashSet<>(Arrays.asList("Authorization", "X-Some-Alt-Authorization")), null); when(reqMock.getHeadersMap()).thenReturn(new TreeMap<>(MapBuilder.>builder() - .put("Authorization", Arrays.asList("secret secret")) - .put("X-Some-Alt-Authorization", Arrays.asList("secret1 secret2")) + .put("Authorization", List.of("secret secret")) + .put("X-Some-Alt-Authorization", List.of("secret1 secret2")) .build())); - when(reqMock.getHeaders("Authorization")).thenReturn(Arrays.asList("secret secret")); - when(reqMock.getHeaders("X-Some-Alt-Authorization")).thenReturn(Arrays.asList("secret1 secret2")); + when(reqMock.getHeaders("Authorization")).thenReturn(List.of("secret secret")); + when(reqMock.getHeaders("X-Some-Alt-Authorization")).thenReturn(List.of("secret1 secret2")); String result = customImpl.parseRequestHeadersToString(reqMock); assertThat(result, is("Authorization=[MASKED],X-Some-Alt-Authorization=[MASKED]")); @@ -386,7 +386,7 @@ public void parseRequestHeadersToStringShouldWorkForSpecialHeadersPath() { @Test public void parseSpecificHeaderToStringShouldWorkForHeaderValInLowerCase() { - when(reqMock.getHeaders("authorization")).thenReturn(Arrays.asList("fooval")); + when(reqMock.getHeaders("authorization")).thenReturn(List.of("fooval")); String result = impl.parseSpecificHeaderToString(reqMock, "authorization"); assertThat(result, is("authorization=[MASKED]")); @@ -403,7 +403,7 @@ public void parseRequestHeadersToStringShouldReturnBlankStringIfHeadersMapIsNull @Test public void parseRequestHeadersToStringShouldReturnBlankStringIfHeaderNamesIsEmpty() { - when(reqMock.getHeadersMap()).thenReturn(Collections.>emptyMap()); + when(reqMock.getHeadersMap()).thenReturn(Collections.emptyMap()); String result = impl.parseRequestHeadersToString(reqMock); assertThat(result, is("")); @@ -421,13 +421,14 @@ public void parseRequestHeadersToStringShouldReturnBlankStringIfUnexpectedExcept @Test public void concatenateErrorCollectionShouldWorkWithOneItemInCollection() { - String result = impl.concatenateErrorCollection(Arrays.asList(BarebonesCoreApiErrorForTesting.MISSING_EXPECTED_CONTENT)); + String result = impl.concatenateErrorCollection( + List.of(BarebonesCoreApiErrorForTesting.MISSING_EXPECTED_CONTENT)); assertThat(result, is("MISSING_EXPECTED_CONTENT")); } @Test public void concatenateErrorCollectionShouldWorkWithMultipleItemsInCollection() { - String result = impl.concatenateErrorCollection(Arrays.asList(BarebonesCoreApiErrorForTesting.MISSING_EXPECTED_CONTENT, BarebonesCoreApiErrorForTesting.TYPE_CONVERSION_ERROR)); + String result = impl.concatenateErrorCollection(Arrays.asList(BarebonesCoreApiErrorForTesting.MISSING_EXPECTED_CONTENT, BarebonesCoreApiErrorForTesting.TYPE_CONVERSION_ERROR)); assertThat(result, is("MISSING_EXPECTED_CONTENT,TYPE_CONVERSION_ERROR")); } diff --git a/backstopper-core/src/test/java/com/nike/backstopper/handler/ErrorResponseInfoTest.java b/backstopper-core/src/test/java/com/nike/backstopper/handler/ErrorResponseInfoTest.java index d9e4ddd..f504502 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/handler/ErrorResponseInfoTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/handler/ErrorResponseInfoTest.java @@ -21,7 +21,11 @@ public class ErrorResponseInfoTest { @Test public void constructorSetsValues() { Object frameworkObj = new Object(); - Map> headersMap = MapBuilder.>builder().put("header1", Arrays.asList("val1")).put("header2", Arrays.asList("h2val1, h2val2")).build(); + Map> headersMap = MapBuilder + .>builder() + .put("header1", List.of("val1")) + .put("header2", List.of("h2val1, h2val2")) + .build(); ErrorResponseInfo responseInfo = new ErrorResponseInfo<>(42, frameworkObj, headersMap); assertThat(responseInfo.frameworkRepresentationObj, is(frameworkObj)); assertThat(responseInfo.headersToAddToResponse, is(headersMap)); diff --git a/backstopper-core/src/test/java/com/nike/backstopper/handler/UnhandledExceptionHandlerBaseTest.java b/backstopper-core/src/test/java/com/nike/backstopper/handler/UnhandledExceptionHandlerBaseTest.java index 91d4523..b7163e7 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/handler/UnhandledExceptionHandlerBaseTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/handler/UnhandledExceptionHandlerBaseTest.java @@ -57,8 +57,8 @@ public class UnhandledExceptionHandlerBaseTest { private RequestInfoForLogging reqMock; private static final ProjectApiErrors testProjectApiErrors = ProjectApiErrorsForTesting.withProjectSpecificData(null, null); - private List errorsExpectedToBeUsed = Collections.singletonList(BarebonesCoreApiErrorForTesting.GENERIC_SERVICE_ERROR); - private int httpStatusCodeExpectedToBeUsed = testProjectApiErrors.determineHighestPriorityHttpStatusCode(errorsExpectedToBeUsed); + private final List errorsExpectedToBeUsed = Collections.singletonList(BarebonesCoreApiErrorForTesting.GENERIC_SERVICE_ERROR); + private final int httpStatusCodeExpectedToBeUsed = testProjectApiErrors.determineHighestPriorityHttpStatusCode(errorsExpectedToBeUsed); private ApiExceptionHandlerUtils utilsSpy; @Before @@ -79,7 +79,7 @@ public void handleException_should_delegate_to_ApiExceptionHandlerUtils_for_buil @Override public Object answer(InvocationOnMock invocation) throws Throwable { StringBuilder sb = (StringBuilder) invocation.getArguments()[0]; - sb.append(UUID.randomUUID().toString()); + sb.append(UUID.randomUUID()); sbHolder.add(sb); return UUID.randomUUID().toString(); } @@ -294,7 +294,7 @@ protected ErrorResponseInfo generateLastDitchFallbackErrorResponseInfo( return new ErrorResponseInfo<>( projectApiErrors.getGenericServiceError().getHttpStatusCode(), new TestDTO(new DefaultErrorContractDTO(UUID.randomUUID().toString(), singletonList(projectApiErrors.getGenericServiceError()))), - Collections.>emptyMap() + Collections.emptyMap() ); } } diff --git a/backstopper-core/src/test/java/com/nike/backstopper/handler/listener/ApiExceptionHandlerListenerResultTest.java b/backstopper-core/src/test/java/com/nike/backstopper/handler/listener/ApiExceptionHandlerListenerResultTest.java index 7602679..f7db11e 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/handler/listener/ApiExceptionHandlerListenerResultTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/handler/listener/ApiExceptionHandlerListenerResultTest.java @@ -52,7 +52,7 @@ public void ignoreResponse_should_return_instance_with_correct_values() { @Test public void handleResponse_one_arg_should_work_as_expected(boolean useNull) { // given - SortedApiErrorSet errors = (useNull) ? null : new SortedApiErrorSet(Arrays.asList( + SortedApiErrorSet errors = (useNull) ? null : new SortedApiErrorSet(Arrays.asList( BarebonesCoreApiErrorForTesting.GENERIC_SERVICE_ERROR, BarebonesCoreApiErrorForTesting.MALFORMED_REQUEST )); @@ -90,7 +90,7 @@ private void verifyErrors(ApiExceptionHandlerListenerResult val, SortedApiErrorS @Test public void handleResponse_two_args_should_work_as_expected(boolean useNullErrors, boolean useNullExtraLogging) { // given - SortedApiErrorSet errors = (useNullErrors) ? null : new SortedApiErrorSet(Arrays.asList( + SortedApiErrorSet errors = (useNullErrors) ? null : new SortedApiErrorSet(Arrays.asList( BarebonesCoreApiErrorForTesting.GENERIC_SERVICE_ERROR, BarebonesCoreApiErrorForTesting.MALFORMED_REQUEST )); List> extraDetailsForLogging = (useNullExtraLogging) ? null : Arrays.asList( @@ -142,7 +142,7 @@ public void handleResponse_three_args_should_work_as_expected( boolean useNullErrors, boolean useNullExtraLogging, boolean useNullResponseHeaders ) { // given - SortedApiErrorSet errors = (useNullErrors) ? null : new SortedApiErrorSet(Arrays.asList( + SortedApiErrorSet errors = (useNullErrors) ? null : new SortedApiErrorSet(Arrays.asList( BarebonesCoreApiErrorForTesting.GENERIC_SERVICE_ERROR, BarebonesCoreApiErrorForTesting.MALFORMED_REQUEST )); List> extraDetailsForLogging = (useNullExtraLogging) ? null : Arrays.asList( diff --git a/backstopper-core/src/test/java/com/nike/backstopper/handler/listener/impl/ClientDataValidationErrorHandlerListenerTest.java b/backstopper-core/src/test/java/com/nike/backstopper/handler/listener/impl/ClientDataValidationErrorHandlerListenerTest.java index 2488efa..dc637b8 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/handler/listener/impl/ClientDataValidationErrorHandlerListenerTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/handler/listener/impl/ClientDataValidationErrorHandlerListenerTest.java @@ -44,8 +44,8 @@ public class ClientDataValidationErrorHandlerListenerTest extends ListenerTestBa private static final ProjectApiErrors testProjectApiErrors = ProjectApiErrorsForTesting.withProjectSpecificData(null, null); - private ClientDataValidationErrorHandlerListener listener = new ClientDataValidationErrorHandlerListener(testProjectApiErrors, - ApiExceptionHandlerUtils.DEFAULT_IMPL); + private final ClientDataValidationErrorHandlerListener listener = new ClientDataValidationErrorHandlerListener(testProjectApiErrors, + ApiExceptionHandlerUtils.DEFAULT_IMPL); @Test public void constructor_sets_projectApiErrors_and_utils_to_passed_in_args() { @@ -98,14 +98,14 @@ public void shouldIgnoreExceptionThatItDoesNotWantToHandle() { public void shouldReturnGENERIC_SERVICE_ERRORForClientDataValidationErrorThatHasNullViolations() { ClientDataValidationError ex = new ClientDataValidationError(null, null, null); ApiExceptionHandlerListenerResult result = listener.shouldHandleException(ex); - validateResponse(result, true, Collections.singletonList(testProjectApiErrors.getGenericServiceError())); + validateResponse(result, true, Collections.singletonList(testProjectApiErrors.getGenericServiceError())); } @Test public void shouldReturnGENERIC_SERVICE_ERRORForClientDataValidationErrorThatHasEmptyViolations() { - ClientDataValidationError ex = new ClientDataValidationError(null, Collections.>emptyList(), null); + ClientDataValidationError ex = new ClientDataValidationError(null, Collections.emptyList(), null); ApiExceptionHandlerListenerResult result = listener.shouldHandleException(ex); - validateResponse(result, true, Collections.singletonList(testProjectApiErrors.getGenericServiceError())); + validateResponse(result, true, Collections.singletonList(testProjectApiErrors.getGenericServiceError())); } @Test @@ -168,11 +168,15 @@ private ConstraintViolation setupConstraintViolation(Class offendingObje @Test public void shouldReturnGENERIC_SERVICE_ERRORForViolationThatDoesNotMapToApiError() { ConstraintViolation violation = setupConstraintViolation(SomeValidatableObject.class, "path.to.violation", NotNull.class, "I_Am_Invalid"); - ClientDataValidationError ex = new ClientDataValidationError(Arrays.asList(new SomeValidatableObject("someArg1", "someArg2")), Collections.singletonList(violation), null); + ClientDataValidationError ex = new ClientDataValidationError( + List.of(new SomeValidatableObject("someArg1", "someArg2")), Collections.singletonList(violation), null); ApiExceptionHandlerListenerResult result = listener.shouldHandleException(ex); validateResponse(result, true, Collections.singletonList( // We expect it to be the generic error, with some metadata about the field that had an issue - new ApiErrorWithMetadata(testProjectApiErrors.getGenericServiceError(), Pair.of("field", (Object)"path.to.violation")) + new ApiErrorWithMetadata( + testProjectApiErrors.getGenericServiceError(), + Pair.of("field", "path.to.violation") + ) )); } @@ -181,12 +185,16 @@ public void shouldReturnExpectedErrorsForViolationsThatMapToApiErrors() { ConstraintViolation violation1 = setupConstraintViolation(SomeValidatableObject.class, "path.to.violation1", NotNull.class, "MISSING_EXPECTED_CONTENT"); ConstraintViolation violation2 = setupConstraintViolation(SomeValidatableObject.class, "path.to.violation2", NotEmpty.class, "TYPE_CONVERSION_ERROR"); ClientDataValidationError ex = new ClientDataValidationError( - Collections.singletonList(new SomeValidatableObject("someArg1", "someArg2")), Arrays.asList(violation1, violation2), null); + Collections.singletonList(new SomeValidatableObject("someArg1", "someArg2")), Arrays.asList(violation1, violation2), null); ApiExceptionHandlerListenerResult result = listener.shouldHandleException(ex); validateResponse(result, true, Arrays.asList( // We expect them to be the properly associated errors, with some metadata about the field that had an issue - new ApiErrorWithMetadata(BarebonesCoreApiErrorForTesting.MISSING_EXPECTED_CONTENT, Pair.of("field", (Object)"path.to.violation1")), - new ApiErrorWithMetadata(BarebonesCoreApiErrorForTesting.TYPE_CONVERSION_ERROR, Pair.of("field", (Object)"path.to.violation2")) + new ApiErrorWithMetadata(BarebonesCoreApiErrorForTesting.MISSING_EXPECTED_CONTENT, Pair.of("field", + "path.to.violation1" + )), + new ApiErrorWithMetadata(BarebonesCoreApiErrorForTesting.TYPE_CONVERSION_ERROR, Pair.of("field", + "path.to.violation2" + )) )); } @@ -211,14 +219,14 @@ public void shouldAddExtraLoggingDetailsForClientDataValidationError() { ); } - private static interface SomeValidationGroup {} + private interface SomeValidationGroup {} private static class SomeValidatableObject { @NotEmpty(message = "MISSING_EXPECTED_CONTENT") - private String arg1; + private final String arg1; @NotEmpty(message = "MISSING_EXPECTED_CONTENT") - private String arg2; + private final String arg2; public SomeValidatableObject(String arg1, String arg2) { this.arg1 = arg1; diff --git a/backstopper-core/src/test/java/com/nike/backstopper/handler/listener/impl/DownstreamNetworkExceptionHandlerListenerTest.java b/backstopper-core/src/test/java/com/nike/backstopper/handler/listener/impl/DownstreamNetworkExceptionHandlerListenerTest.java index 2c6bbab..69a1a2f 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/handler/listener/impl/DownstreamNetworkExceptionHandlerListenerTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/handler/listener/impl/DownstreamNetworkExceptionHandlerListenerTest.java @@ -36,7 +36,7 @@ public class DownstreamNetworkExceptionHandlerListenerTest extends ListenerTestBase { private static final ProjectApiErrors testProjectApiErrors = ProjectApiErrorsForTesting.withProjectSpecificData(null, null); - private DownstreamNetworkExceptionHandlerListener listener = new DownstreamNetworkExceptionHandlerListener(testProjectApiErrors); + private final DownstreamNetworkExceptionHandlerListener listener = new DownstreamNetworkExceptionHandlerListener(testProjectApiErrors); private static class HttpClientErrorExceptionForTests extends Exception { public final int statusCode; diff --git a/backstopper-core/src/test/java/com/nike/backstopper/handler/listener/impl/GenericApiExceptionHandlerListenerTest.java b/backstopper-core/src/test/java/com/nike/backstopper/handler/listener/impl/GenericApiExceptionHandlerListenerTest.java index 536f221..f966e86 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/handler/listener/impl/GenericApiExceptionHandlerListenerTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/handler/listener/impl/GenericApiExceptionHandlerListenerTest.java @@ -21,7 +21,7 @@ */ public class GenericApiExceptionHandlerListenerTest extends ListenerTestBase { - private GenericApiExceptionHandlerListener listener = new GenericApiExceptionHandlerListener(); + private final GenericApiExceptionHandlerListener listener = new GenericApiExceptionHandlerListener(); @Test public void shouldIgnoreExceptionThatItDoesNotWantToHandle() { diff --git a/backstopper-core/src/test/java/com/nike/backstopper/handler/listener/impl/ServersideValidationErrorHandlerListenerTest.java b/backstopper-core/src/test/java/com/nike/backstopper/handler/listener/impl/ServersideValidationErrorHandlerListenerTest.java index d8d712c..ce0b81f 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/handler/listener/impl/ServersideValidationErrorHandlerListenerTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/handler/listener/impl/ServersideValidationErrorHandlerListenerTest.java @@ -40,8 +40,8 @@ public class ServersideValidationErrorHandlerListenerTest extends ListenerTestBase { private static final ProjectApiErrors testProjectApiErrors = ProjectApiErrorsForTesting.withProjectSpecificData(null, null); - private ServersideValidationErrorHandlerListener listener = new ServersideValidationErrorHandlerListener(testProjectApiErrors, - ApiExceptionHandlerUtils.DEFAULT_IMPL); + private final ServersideValidationErrorHandlerListener listener = new ServersideValidationErrorHandlerListener(testProjectApiErrors, + ApiExceptionHandlerUtils.DEFAULT_IMPL); @Test public void constructor_sets_projectApiErrors_and_utils_to_passed_in_args() { @@ -149,9 +149,9 @@ public void shouldAddExtraLoggingDetailsForServersideValidationError() { private static class SomeValidatableObject { @NotEmpty(message = "INVALID_TRUSTED_HEADERS_ERROR") - private String arg1; + private final String arg1; @NotEmpty(message = "INVALID_TRUSTED_HEADERS_ERROR") - private String arg2; + private final String arg2; public SomeValidatableObject(String arg1, String arg2) { this.arg1 = arg1; diff --git a/backstopper-core/src/test/java/com/nike/backstopper/model/DefaultErrorContractDTOTest.java b/backstopper-core/src/test/java/com/nike/backstopper/model/DefaultErrorContractDTOTest.java index d8cfdd6..1eee9be 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/model/DefaultErrorContractDTOTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/model/DefaultErrorContractDTOTest.java @@ -47,9 +47,9 @@ public void shouldNotExplodeIfYouPassInNullErrorCollection() { @Test public void shouldCorrectlyTranslateApiErrorsToIndividualErrorViews() { - List apiErrors = Arrays.asList(BarebonesCoreApiErrorForTesting.NO_ACCEPTABLE_REPRESENTATION, - BarebonesCoreApiErrorForTesting.MALFORMED_REQUEST, - BarebonesCoreApiErrorForTesting.OUTSIDE_DEPENDENCY_RETURNED_AN_UNRECOVERABLE_ERROR); + List apiErrors = Arrays.asList(BarebonesCoreApiErrorForTesting.NO_ACCEPTABLE_REPRESENTATION, + BarebonesCoreApiErrorForTesting.MALFORMED_REQUEST, + BarebonesCoreApiErrorForTesting.OUTSIDE_DEPENDENCY_RETURNED_AN_UNRECOVERABLE_ERROR); DefaultErrorContractDTO erv = new DefaultErrorContractDTO(null, apiErrors); assertThat(erv.errors .size(), is(apiErrors.size())); diff --git a/backstopper-core/src/test/java/com/nike/backstopper/service/ClientDataValidationServiceTest.java b/backstopper-core/src/test/java/com/nike/backstopper/service/ClientDataValidationServiceTest.java index 29f09f2..9e81d26 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/service/ClientDataValidationServiceTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/service/ClientDataValidationServiceTest.java @@ -82,7 +82,7 @@ public void shouldDelegateValidateObjectsWithGroupsFailFastCollectionMethodWithN public void shouldDelegateValidateObjectsWithGroupsFailFastCollectionMethodWithEmptyGroups() { Object obj1 = new Object(); Object obj2 = new Object(); - validationServiceSpy.validateObjectsWithGroupsFailFast(Collections.>emptyList(), obj1, obj2); + validationServiceSpy.validateObjectsWithGroupsFailFast(Collections.emptyList(), obj1, obj2); verify(validationServiceSpy).validateObjectsWithGroupsFailFast((Class[])null, obj1, obj2); } @@ -94,13 +94,13 @@ public void validateObjectsWithGroupsFailFastShouldDoNothingIfObjectsArrayIsNull @Test public void validateObjectsWithGroupsFailFastShouldDoNothingIfObjectsArrayIsEmpty() { - validationServiceSpy.validateObjectsWithGroupsFailFast((Class[])null, new Object[0]); + validationServiceSpy.validateObjectsWithGroupsFailFast((Class[])null); verifyNoMoreInteractions(validatorMock); } @Test public void validateObjectsWithGroupsFailFastShouldValidatePassedInObjectsNoGroups() { - given(validatorMock.validate(any(), any(Class[].class))).willReturn(Collections.>emptySet()); + given(validatorMock.validate(any(), any(Class[].class))).willReturn(Collections.emptySet()); Object objToValidate1 = new Object(); Object objToValidate2 = new Object(); validationServiceSpy.validateObjectsWithGroupsFailFast((Class[])null, objToValidate1, objToValidate2); @@ -110,7 +110,7 @@ public void validateObjectsWithGroupsFailFastShouldValidatePassedInObjectsNoGrou @Test public void validateObjectsWithGroupsFailFastShouldValidatePassedInObjectsWithGroups() { - given(validatorMock.validate(any(), any(Class[].class))).willReturn(Collections.>emptySet()); + given(validatorMock.validate(any(), any(Class[].class))).willReturn(Collections.emptySet()); Object objToValidate1 = new Object(); Object objToValidate2 = new Object(); Class[] groups = new Class[]{Default.class, String.class}; @@ -121,7 +121,7 @@ public void validateObjectsWithGroupsFailFastShouldValidatePassedInObjectsWithGr @Test public void validateObjectsWithGroupsFailFastShouldNotThrowExceptionIfThereAreNoViolations() { - given(validatorMock.validate(any(), any(Class[].class))).willReturn(Collections.>emptySet()); + given(validatorMock.validate(any(), any(Class[].class))).willReturn(Collections.emptySet()); Object objToValidate = new Object(); validationServiceSpy.validateObjectsWithGroupsFailFast((Class[])null, objToValidate); verify(validatorMock).validate(objToValidate); @@ -129,9 +129,9 @@ public void validateObjectsWithGroupsFailFastShouldNotThrowExceptionIfThereAreNo @Test public void validateObjectsWithGroupsFailFastShouldNotValidateNullObjects() { - given(validatorMock.validate(any(), any(Class[].class))).willReturn(Collections.>emptySet()); + given(validatorMock.validate(any(), any(Class[].class))).willReturn(Collections.emptySet()); Object objToValidate = new Object(); - validationServiceSpy.validateObjectsWithGroupsFailFast((Class[])null, new Object[]{objToValidate, null}); + validationServiceSpy.validateObjectsWithGroupsFailFast((Class[])null, objToValidate, null); verify(validatorMock).validate(objToValidate); verifyNoMoreInteractions(validatorMock); } @@ -142,10 +142,11 @@ public void validateObjectsWithGroupsFailFastShouldThrowAppropriateExceptionWhen Object objToValidate2 = new Object(); Object objToValidate3 = new Object(); Class[] groups = new Class[]{Default.class, String.class}; - List> obj1Violations = Arrays.>asList(mock(ConstraintViolation.class)); - List> obj3Violations = Arrays.>asList(mock(ConstraintViolation.class), mock(ConstraintViolation.class)); + List> obj1Violations = + Collections.singletonList(mock(ConstraintViolation.class)); + List> obj3Violations = Arrays.asList(mock(ConstraintViolation.class), mock(ConstraintViolation.class)); given(validatorMock.validate(objToValidate1, groups)).willReturn(new HashSet<>(obj1Violations)); - given(validatorMock.validate(objToValidate2, groups)).willReturn(Collections.>emptySet()); + given(validatorMock.validate(objToValidate2, groups)).willReturn(Collections.emptySet()); given(validatorMock.validate(objToValidate3, groups)).willReturn(new HashSet<>(obj3Violations)); try { validationServiceSpy.validateObjectsWithGroupsFailFast(groups, objToValidate1, objToValidate2, objToValidate3); diff --git a/backstopper-core/src/test/java/com/nike/backstopper/service/FailFastServersideValidationServiceTest.java b/backstopper-core/src/test/java/com/nike/backstopper/service/FailFastServersideValidationServiceTest.java index b7fc288..a2dde67 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/service/FailFastServersideValidationServiceTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/service/FailFastServersideValidationServiceTest.java @@ -44,7 +44,7 @@ public void shouldNotThrowExceptionIfValidatorComesBackClean() { @Test(expected = ServersideValidationError.class) public void shouldThrowExceptionIfValidatorFindsConstraintViolations() { Object validateMe = new Object(); - when(validator.validate(validateMe)).thenReturn(Collections.>singleton(mock(ConstraintViolation.class))); + when(validator.validate(validateMe)).thenReturn(Collections.singleton(mock(ConstraintViolation.class))); validationService.validateObjectFailFast(validateMe); } } diff --git a/backstopper-core/src/test/java/com/nike/backstopper/service/NoOpJsr303ValidatorTest.java b/backstopper-core/src/test/java/com/nike/backstopper/service/NoOpJsr303ValidatorTest.java index c54cdf0..4cf7efb 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/service/NoOpJsr303ValidatorTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/service/NoOpJsr303ValidatorTest.java @@ -17,8 +17,8 @@ */ public class NoOpJsr303ValidatorTest { - private Validator noOpValidator = NoOpJsr303Validator.SINGLETON_IMPL; - private FooClass constraintAnnotatedClass = new FooClass(); + private final Validator noOpValidator = NoOpJsr303Validator.SINGLETON_IMPL; + private final FooClass constraintAnnotatedClass = new FooClass(); @Test public void validation_methods_return_empty_sets() { diff --git a/backstopper-custom-validators/src/main/java/com/nike/backstopper/validation/constraints/impl/StringConvertsToClassTypeValidator.java b/backstopper-custom-validators/src/main/java/com/nike/backstopper/validation/constraints/impl/StringConvertsToClassTypeValidator.java index 448f325..3d0a15c 100644 --- a/backstopper-custom-validators/src/main/java/com/nike/backstopper/validation/constraints/impl/StringConvertsToClassTypeValidator.java +++ b/backstopper-custom-validators/src/main/java/com/nike/backstopper/validation/constraints/impl/StringConvertsToClassTypeValidator.java @@ -171,11 +171,7 @@ protected boolean validateAsLong(String value) { protected boolean validateAsFloat(String value) { try { Float floatValue = Float.parseFloat(value); - if (floatValue.isInfinite() || floatValue.isNaN()) - return false; - - // No error, so it can be successfully parsed to this primitive type. - return true; + return !floatValue.isInfinite() && !floatValue.isNaN(); } catch (Exception ex) { // Couldn't parse the given string into this primitive type, so it's not valid. @@ -186,11 +182,7 @@ protected boolean validateAsFloat(String value) { protected boolean validateAsDouble(String value) { try { Double doubleValue = Double.parseDouble(value); - if (doubleValue.isInfinite() || doubleValue.isNaN()) - return false; - - // No error, so it can be successfully parsed to this primitive type. - return true; + return !doubleValue.isInfinite() && !doubleValue.isNaN(); } catch (Exception ex) { // Couldn't parse the given string into this primitive type, so it's not valid. diff --git a/backstopper-custom-validators/src/test/java/com/nike/backstopper/validation/constraints/impl/StringConvertsToClassTypeValidatorTest.java b/backstopper-custom-validators/src/test/java/com/nike/backstopper/validation/constraints/impl/StringConvertsToClassTypeValidatorTest.java index 276d824..4ab879f 100644 --- a/backstopper-custom-validators/src/test/java/com/nike/backstopper/validation/constraints/impl/StringConvertsToClassTypeValidatorTest.java +++ b/backstopper-custom-validators/src/test/java/com/nike/backstopper/validation/constraints/impl/StringConvertsToClassTypeValidatorTest.java @@ -27,7 +27,7 @@ @RunWith(DataProviderRunner.class) public class StringConvertsToClassTypeValidatorTest { - private Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); + private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); private StringConvertsToClassTypeValidator validatorImpl; @@ -308,7 +308,7 @@ public void shouldNotValidateInvalidLongBoxed() { @Test public void shouldNotValidateLongBoxedForValueTooBigForLong() { - doValidationTest(newObj().withFooLongBoxed("9" + String.valueOf(Long.MAX_VALUE)), "9" + String.valueOf(Long.MAX_VALUE), Long.class, false); + doValidationTest(newObj().withFooLongBoxed("9" + Long.MAX_VALUE), "9" + Long.MAX_VALUE, Long.class, false); } @Test @@ -323,7 +323,7 @@ public void shouldNotValidateInvalidLong() { @Test public void shouldNotValidateLongForValueTooBigForLong() { - doValidationTest(newObj().withFooLong("9" + String.valueOf(Long.MAX_VALUE)), "9" + String.valueOf(Long.MAX_VALUE), long.class, false); + doValidationTest(newObj().withFooLong("9" + Long.MAX_VALUE), "9" + Long.MAX_VALUE, long.class, false); } // Float (boxed) and float (primitive) =============================================================== @@ -385,7 +385,7 @@ public void shouldNotValidateInvalidNanDoubleBoxed() { @Test public void shouldNotValidateDoubleBoxedForValueTooBigForDouble() { - doValidationTest(newObj().withFooDoubleBoxed("1" + String.valueOf(Double.MAX_VALUE)), "1" + String.valueOf(Double.MAX_VALUE), Double.class, false); + doValidationTest(newObj().withFooDoubleBoxed("1" + Double.MAX_VALUE), "1" + Double.MAX_VALUE, Double.class, false); } @Test @@ -405,7 +405,7 @@ public void shouldNotValidateInvalidNaNDouble() { @Test public void shouldNotValidateDoubleForValueTooBigForDouble() { - doValidationTest(newObj().withFooDouble("1" + String.valueOf(Double.MAX_VALUE)), "1" + String.valueOf(Double.MAX_VALUE), double.class, false); + doValidationTest(newObj().withFooDouble("1" + Double.MAX_VALUE), "1" + Double.MAX_VALUE, double.class, false); } // Boolean (boxed) and boolean (primitive) =============================================================== diff --git a/backstopper-jackson/src/main/java/com/nike/backstopper/model/util/JsonUtilWithDefaultErrorContractDTOSupport.java b/backstopper-jackson/src/main/java/com/nike/backstopper/model/util/JsonUtilWithDefaultErrorContractDTOSupport.java index 70585a5..941ba5e 100644 --- a/backstopper-jackson/src/main/java/com/nike/backstopper/model/util/JsonUtilWithDefaultErrorContractDTOSupport.java +++ b/backstopper-jackson/src/main/java/com/nike/backstopper/model/util/JsonUtilWithDefaultErrorContractDTOSupport.java @@ -139,8 +139,7 @@ protected MetadataPropertyWriter(BeanPropertyWriter base) { @Override public void serializeAsField(Object bean, JsonGenerator jgen, SerializerProvider prov) throws Exception { - if (bean instanceof DefaultErrorDTO) { - DefaultErrorDTO error = (DefaultErrorDTO) bean; + if (bean instanceof DefaultErrorDTO error) { if (error.metadata == null || error.metadata.isEmpty()) { return; // empty metadata. Don't serialize } @@ -157,8 +156,7 @@ protected SmartErrorCodePropertyWriter(BeanPropertyWriter base) { @Override public void serializeAsField(Object bean, JsonGenerator jgen, SerializerProvider prov) throws Exception { - if (bean instanceof DefaultErrorDTO) { - DefaultErrorDTO error = (DefaultErrorDTO) bean; + if (bean instanceof DefaultErrorDTO error) { try { int codeAsInt = Integer.parseInt(error.code); jgen.writeFieldName(_name); diff --git a/backstopper-jackson/src/test/java/com/nike/backstopper/model/util/JsonUtilWithDefaultErrorContractDTOSupportTest.java b/backstopper-jackson/src/test/java/com/nike/backstopper/model/util/JsonUtilWithDefaultErrorContractDTOSupportTest.java index dc6a4a8..f0e9147 100644 --- a/backstopper-jackson/src/test/java/com/nike/backstopper/model/util/JsonUtilWithDefaultErrorContractDTOSupportTest.java +++ b/backstopper-jackson/src/test/java/com/nike/backstopper/model/util/JsonUtilWithDefaultErrorContractDTOSupportTest.java @@ -264,7 +264,7 @@ public void ErrorContractSerializationFactory_findPropWriter_returns_null_if_it_ ErrorContractSerializationFactory impl = new ErrorContractSerializationFactory(null, true, true); // when - BeanPropertyWriter result = impl.findPropWriter(Collections.emptyList(), UUID.randomUUID().toString()); + BeanPropertyWriter result = impl.findPropWriter(Collections.emptyList(), UUID.randomUUID().toString()); // then assertThat(result).isNull(); diff --git a/backstopper-servlet-api/src/main/java/com/nike/backstopper/handler/adapter/RequestInfoForLoggingServletApiAdapter.java b/backstopper-servlet-api/src/main/java/com/nike/backstopper/handler/adapter/RequestInfoForLoggingServletApiAdapter.java index 04966aa..0a420c5 100644 --- a/backstopper-servlet-api/src/main/java/com/nike/backstopper/handler/adapter/RequestInfoForLoggingServletApiAdapter.java +++ b/backstopper-servlet-api/src/main/java/com/nike/backstopper/handler/adapter/RequestInfoForLoggingServletApiAdapter.java @@ -126,7 +126,7 @@ protected void safeCloseCloseable(Closeable closeable) { closeable.close(); } catch (Throwable e) { logger.warn("An error occurred closing a Closeable resource. closeable_classname=\"" - + closeable.getClass().getName() + "\", exception_during_close=\"" + e.toString() + "\""); + + closeable.getClass().getName() + "\", exception_during_close=\"" + e + "\""); } } } diff --git a/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerServletApiBaseTest.java b/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerServletApiBaseTest.java index 80c030a..9135b05 100644 --- a/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerServletApiBaseTest.java +++ b/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerServletApiBaseTest.java @@ -40,7 +40,7 @@ public class ApiExceptionHandlerServletApiBaseTest { @Before public void beforeMethod() { instanceSpy = spy(new ApiExceptionHandlerServletApiBase(mock(ProjectApiErrors.class), - Collections.emptyList(), + Collections.emptyList(), ApiExceptionHandlerUtils.DEFAULT_IMPL) { @Override protected Object prepareFrameworkRepresentation(DefaultErrorContractDTO errorContractDTO, int httpStatusCode, Collection filteredClientErrors, @@ -62,7 +62,14 @@ public void maybeHandleExceptionReturnsSuperValue() throws UnexpectedMajorExcept @Test public void maybeHandleExceptionSetsHeadersAndStatusCodeOnServletResponse() throws UnexpectedMajorExceptionHandlingError { - ErrorResponseInfo expectedResponseInfo = new ErrorResponseInfo(42, null, MapBuilder.>builder().put("header1", Arrays.asList("h1val1")).put("header2", Arrays.asList("h2val1", "h2val2")).build()); + ErrorResponseInfo expectedResponseInfo = new ErrorResponseInfo( + 42, + null, + MapBuilder.>builder() + .put("header1", List.of("h1val1")) + .put("header2", Arrays.asList("h2val1", "h2val2")) + .build() + ); doReturn(expectedResponseInfo).when(instanceSpy).maybeHandleException(any(Throwable.class), any(RequestInfoForLogging.class)); instanceSpy.maybeHandleException(new Exception(), servletRequestMock, servletResponseMock); diff --git a/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/UnhandledExceptionHandlerServletApiBaseTest.java b/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/UnhandledExceptionHandlerServletApiBaseTest.java index 6966029..72b6605 100644 --- a/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/UnhandledExceptionHandlerServletApiBaseTest.java +++ b/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/UnhandledExceptionHandlerServletApiBaseTest.java @@ -65,7 +65,14 @@ public void handleExceptionReturnsSuperValue() throws UnexpectedMajorExceptionHa @Test public void handleExceptionSetsHeadersAndStatusCodeOnServletResponse() throws UnexpectedMajorExceptionHandlingError { - ErrorResponseInfo expectedResponseInfo = new ErrorResponseInfo(42, null, MapBuilder.>builder().put("header1", Arrays.asList("h1val1")).put("header2", Arrays.asList("h2val1", "h2val2")).build()); + ErrorResponseInfo expectedResponseInfo = new ErrorResponseInfo( + 42, + null, + MapBuilder.>builder() + .put("header1", List.of("h1val1")) + .put("header2", Arrays.asList("h2val1", "h2val2")) + .build() + ); doReturn(expectedResponseInfo).when(instanceSpy).handleException(any(Throwable.class), any(RequestInfoForLogging.class)); instanceSpy.handleException(new Exception(), servletRequestMock, servletResponseMock); diff --git a/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/adapter/RequestInfoForLoggingServletApiAdapterTest.java b/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/adapter/RequestInfoForLoggingServletApiAdapterTest.java index 63654c0..2b1fda9 100644 --- a/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/adapter/RequestInfoForLoggingServletApiAdapterTest.java +++ b/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/adapter/RequestInfoForLoggingServletApiAdapterTest.java @@ -79,7 +79,7 @@ public void getQueryStringDelegatesToServletRequest() { @Test public void getHeaderMapDelegatesToServletRequestAndCachesResult() { Map> expectedHeaderMap = new TreeMap<>(MapBuilder.>builder() - .put("header1", Arrays.asList("h1val1")) + .put("header1", List.of("h1val1")) .put("header2", Arrays.asList("h2val1", "h2val2")) .build()); doReturn(Collections.enumeration(expectedHeaderMap.keySet())).when(requestMock).getHeaderNames(); @@ -102,7 +102,7 @@ public void getHeaderMapReturnsEmptyMapIfServletRequestHeaderNamesReturnsNull() @Test public void getHeaderMapIgnoresHeadersWhereServletRequestGetHeadersMethodReturnsNull() { Map> expectedHeaderMap = new TreeMap<>(MapBuilder.>builder() - .put("header1", Arrays.asList("h1val1")) + .put("header1", List.of("h1val1")) .build()); doReturn(Collections.enumeration(Arrays.asList("header1", "header2"))).when(requestMock).getHeaderNames(); doReturn(Collections.enumeration(expectedHeaderMap.get("header1"))).when(requestMock).getHeaders("header1"); @@ -120,7 +120,7 @@ public void getHeaderDelegatesToServletRequest() { @Test public void getHeadersDelegatesToServletRequest() { - Pair> header1 = Pair.of("header1", Arrays.asList("h1val1")); + Pair> header1 = Pair.of("header1", List.of("h1val1")); Pair> header2 = Pair.of("header2", Arrays.asList("h2val1", "h2val2")); Map> expectedHeaderMap = new TreeMap<>(MapBuilder.>builder() .put(header1.getKey(), header1.getValue()) @@ -139,7 +139,7 @@ public void getAttributeDelegatesToServletRequest() { String attributeName = "someattribute"; UUID expectedValue = UUID.randomUUID(); doReturn(expectedValue).when(requestMock).getAttribute(attributeName); - assertThat(adapter.getAttribute(attributeName), Is.is(expectedValue)); + assertThat(adapter.getAttribute(attributeName), Is.is(expectedValue)); } @Test diff --git a/backstopper-spring-boot3-webmvc/src/test/java/com/nike/backstopper/handler/springboot/componenttest/SanityCheckComponentTest.java b/backstopper-spring-boot3-webmvc/src/test/java/com/nike/backstopper/handler/springboot/componenttest/SanityCheckComponentTest.java index c598ccc..5b52f1d 100644 --- a/backstopper-spring-boot3-webmvc/src/test/java/com/nike/backstopper/handler/springboot/componenttest/SanityCheckComponentTest.java +++ b/backstopper-spring-boot3-webmvc/src/test/java/com/nike/backstopper/handler/springboot/componenttest/SanityCheckComponentTest.java @@ -266,7 +266,7 @@ enum SanityCheckProjectApiError implements ApiError { SanityCheckProjectApiError(int errorCode, String message, int httpStatusCode) { this(new ApiErrorBase( - "delegated-to-enum-wrapper-" + UUID.randomUUID().toString(), errorCode, message, httpStatusCode + "delegated-to-enum-wrapper-" + UUID.randomUUID(), errorCode, message, httpStatusCode )); } diff --git a/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/componenttest/BackstopperSpringWebFluxComponentTest.java b/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/componenttest/BackstopperSpringWebFluxComponentTest.java index c5cdb1d..bd38657 100644 --- a/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/componenttest/BackstopperSpringWebFluxComponentTest.java +++ b/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/componenttest/BackstopperSpringWebFluxComponentTest.java @@ -1228,7 +1228,7 @@ enum ComponentTestProjectApiError implements ApiError { ComponentTestProjectApiError(int errorCode, String message, int httpStatusCode) { this(new ApiErrorBase( - "delegated-to-enum-wrapper-" + UUID.randomUUID().toString(), errorCode, message, httpStatusCode + "delegated-to-enum-wrapper-" + UUID.randomUUID(), errorCode, message, httpStatusCode )); } diff --git a/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/listener/impl/OneOffSpringWebFluxFrameworkExceptionHandlerListenerTest.java b/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/listener/impl/OneOffSpringWebFluxFrameworkExceptionHandlerListenerTest.java index a434159..8f6ab86 100644 --- a/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/listener/impl/OneOffSpringWebFluxFrameworkExceptionHandlerListenerTest.java +++ b/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/listener/impl/OneOffSpringWebFluxFrameworkExceptionHandlerListenerTest.java @@ -34,7 +34,7 @@ public class OneOffSpringWebFluxFrameworkExceptionHandlerListenerTest { private static final ProjectApiErrors testProjectApiErrors = ProjectApiErrorsForTesting.withProjectSpecificData(null, null); - private OneOffSpringWebFluxFrameworkExceptionHandlerListener listener = + private final OneOffSpringWebFluxFrameworkExceptionHandlerListener listener = new OneOffSpringWebFluxFrameworkExceptionHandlerListener( testProjectApiErrors, ApiExceptionHandlerUtils.DEFAULT_IMPL ); diff --git a/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringWebMvcFrameworkExceptionHandlerListener.java b/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringWebMvcFrameworkExceptionHandlerListener.java index 2e68ac0..74512cc 100644 --- a/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringWebMvcFrameworkExceptionHandlerListener.java +++ b/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringWebMvcFrameworkExceptionHandlerListener.java @@ -70,12 +70,11 @@ public OneOffSpringWebMvcFrameworkExceptionHandlerListener(ProjectApiErrors proj return handleError(projectApiErrors.getMethodNotAllowedApiError(), extraDetailsForLogging); } - if (ex instanceof MissingServletRequestPartException) { - MissingServletRequestPartException detailsEx = (MissingServletRequestPartException)ex; + if (ex instanceof MissingServletRequestPartException detailsEx) { return handleError( new ApiErrorWithMetadata( projectApiErrors.getMalformedRequestApiError(), - Pair.of("missing_required_part", (Object)detailsEx.getRequestPartName()) + Pair.of("missing_required_part", detailsEx.getRequestPartName()) ), extraDetailsForLogging ); @@ -95,13 +94,12 @@ protected ApiExceptionHandlerListenerResult handleServletRequestBindingException ApiError errorToUse = projectApiErrors.getMalformedRequestApiError(); // Add some extra context metadata if it's a MissingServletRequestParameterException. - if (ex instanceof MissingServletRequestParameterException) { - MissingServletRequestParameterException detailsEx = (MissingServletRequestParameterException)ex; - + if (ex instanceof MissingServletRequestParameterException detailsEx) { + errorToUse = new ApiErrorWithMetadata( errorToUse, - Pair.of("missing_param_name", (Object)detailsEx.getParameterName()), - Pair.of("missing_param_type", (Object)detailsEx.getParameterType()), + Pair.of("missing_param_name", detailsEx.getParameterName()), + Pair.of("missing_param_type", detailsEx.getParameterType()), Pair.of("required_location", "query_param") ); } diff --git a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/ClientfacingErrorITest.java b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/ClientfacingErrorITest.java index a2ad088..cae3588 100644 --- a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/ClientfacingErrorITest.java +++ b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/ClientfacingErrorITest.java @@ -336,10 +336,8 @@ public void throw4xxServerHttpStatusCodeException() { } private byte[] responseBodyForDownstreamServiceError() { - StringBuilder sb = new StringBuilder(); - sb.append("{\"result\":\"failure\",\"errorCode\":\"0x00000042\",\"errorMessage\":\"something bad happened\"}"); - return sb.toString().getBytes(); + return "{\"result\":\"failure\",\"errorCode\":\"0x00000042\",\"errorMessage\":\"something bad happened\"}".getBytes(); } @RequestMapping("/throwServerTimeoutException") diff --git a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerTest.java b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerTest.java index 4664fd8..94d78b1 100644 --- a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerTest.java +++ b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerTest.java @@ -37,7 +37,7 @@ public class SpringApiExceptionHandlerTest { @Before public void beforeMethod() { projectApiErrorsMock = mock(ProjectApiErrors.class); - listenerList = new ApiExceptionHandlerListenerList(Collections.emptyList()); + listenerList = new ApiExceptionHandlerListenerList(Collections.emptyList()); generalUtils = ApiExceptionHandlerUtils.DEFAULT_IMPL; springUtils = SpringApiExceptionHandlerUtils.DEFAULT_IMPL; handlerSpy = spy(new SpringApiExceptionHandler(projectApiErrorsMock, listenerList, generalUtils, springUtils)); diff --git a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerUtilsTest.java b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerUtilsTest.java index e236ceb..1fbde5e 100644 --- a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerUtilsTest.java +++ b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerUtilsTest.java @@ -38,8 +38,8 @@ public void setupMethod() { @Test public void generateModelAndViewForErrorResponseShouldGenerateModelAndViewWithErrorContractAsOnlyModelObject() throws JsonProcessingException { DefaultErrorContractDTO - erv = new DefaultErrorContractDTO("someRequestId", Arrays.asList(BarebonesCoreApiErrorForTesting.NO_ACCEPTABLE_REPRESENTATION, - BarebonesCoreApiErrorForTesting.UNSUPPORTED_MEDIA_TYPE)); + erv = new DefaultErrorContractDTO("someRequestId", Arrays.asList(BarebonesCoreApiErrorForTesting.NO_ACCEPTABLE_REPRESENTATION, + BarebonesCoreApiErrorForTesting.UNSUPPORTED_MEDIA_TYPE)); ModelAndView mav = new SpringApiExceptionHandlerUtils().generateModelAndViewForErrorResponse(erv, -1, null, null, null); assertThat(mav.getModel().size(), is(1)); diff --git a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringUnhandledExceptionHandlerTest.java b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringUnhandledExceptionHandlerTest.java index 350a17f..869784c 100644 --- a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringUnhandledExceptionHandlerTest.java +++ b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringUnhandledExceptionHandlerTest.java @@ -116,7 +116,7 @@ public void resolveException_delegates_to_handleException() { Exception originalEx = new RuntimeException("kaboom"); ModelAndView modelAndViewMock = mock(ModelAndView.class); ErrorResponseInfo handleExceptionResult = - new ErrorResponseInfo<>(424, modelAndViewMock, Collections.>emptyMap()); + new ErrorResponseInfo<>(424, modelAndViewMock, Collections.emptyMap()); doReturn(handleExceptionResult).when(handlerSpy).handleException(originalEx, reqMock, responseMock); // when diff --git a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringWebMvcFrameworkExceptionHandlerListenerTest.java b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringWebMvcFrameworkExceptionHandlerListenerTest.java index 4b8b111..1057b65 100644 --- a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringWebMvcFrameworkExceptionHandlerListenerTest.java +++ b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringWebMvcFrameworkExceptionHandlerListenerTest.java @@ -50,7 +50,7 @@ public class OneOffSpringWebMvcFrameworkExceptionHandlerListenerTest { private static final ProjectApiErrors testProjectApiErrors = ProjectApiErrorsForTesting.withProjectSpecificData(null, null); - private OneOffSpringWebMvcFrameworkExceptionHandlerListener listener = + private final OneOffSpringWebMvcFrameworkExceptionHandlerListener listener = new OneOffSpringWebMvcFrameworkExceptionHandlerListener( testProjectApiErrors, ApiExceptionHandlerUtils.DEFAULT_IMPL ); @@ -156,8 +156,8 @@ public void shouldHandleException_returns_MALFORMED_REQUEST_for_ServletRequestBi boolean isMissingRequestParamEx ) { // given - String missingParamName = "someParam-" + UUID.randomUUID().toString(); - String missingParamType = "someParamType-" + UUID.randomUUID().toString(); + String missingParamName = "someParam-" + UUID.randomUUID(); + String missingParamType = "someParamType-" + UUID.randomUUID(); ServletRequestBindingException ex = (isMissingRequestParamEx) ? new MissingServletRequestParameterException(missingParamName, missingParamType) diff --git a/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/ConventionBasedSpringValidationErrorToApiErrorHandlerListener.java b/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/ConventionBasedSpringValidationErrorToApiErrorHandlerListener.java index cb71071..5630a96 100644 --- a/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/ConventionBasedSpringValidationErrorToApiErrorHandlerListener.java +++ b/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/ConventionBasedSpringValidationErrorToApiErrorHandlerListener.java @@ -58,8 +58,7 @@ public ConventionBasedSpringValidationErrorToApiErrorHandlerListener( @Override public ApiExceptionHandlerListenerResult shouldHandleException(Throwable ex) { - if (ex instanceof Errors) { - Errors errEx = (Errors) ex; + if (ex instanceof Errors errEx) { List errList = errEx.getAllErrors(); //noinspection ConstantValue if (errList != null && !errList.isEmpty()) { diff --git a/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListener.java b/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListener.java index 90f3ab3..4afa3d8 100644 --- a/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListener.java +++ b/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListener.java @@ -303,8 +303,7 @@ protected ApiExceptionHandlerListenerResult handleTypeMismatchException( // ResponseEntityExceptionHandler for verification. if (ex instanceof ConversionNotSupportedException) { // We can add even more context log details if it's a MethodArgumentConversionNotSupportedException. - if (ex instanceof MethodArgumentConversionNotSupportedException) { - MethodArgumentConversionNotSupportedException macnsEx = (MethodArgumentConversionNotSupportedException)ex; + if (ex instanceof MethodArgumentConversionNotSupportedException macnsEx) { extraDetailsForLogging.add(Pair.of("method_arg_name", macnsEx.getName())); extraDetailsForLogging.add(Pair.of("method_arg_target_param", macnsEx.getParameter().toString())); } @@ -315,8 +314,7 @@ protected ApiExceptionHandlerListenerResult handleTypeMismatchException( // All other TypeMismatchExceptions should be treated as a 400, and we can/should include the metadata. // We can add even more context log details if it's a MethodArgumentTypeMismatchException. - if (ex instanceof MethodArgumentTypeMismatchException) { - MethodArgumentTypeMismatchException matmEx = (MethodArgumentTypeMismatchException)ex; + if (ex instanceof MethodArgumentTypeMismatchException matmEx) { List> hOrQMetadata = extractExtraMetadataForHeaderOrQueryParamException(matmEx); if (hOrQMetadata != null) { for (Pair pair : hOrQMetadata) { @@ -516,22 +514,19 @@ protected void addExtraDetailsForLoggingForResponseStatusException( @NotNull ResponseStatusException ex, @NotNull List> extraDetailsForLogging ) { - if (ex instanceof MethodNotAllowedException) { - MethodNotAllowedException detailsEx = (MethodNotAllowedException)ex; + if (ex instanceof MethodNotAllowedException detailsEx) { extraDetailsForLogging.add( Pair.of("supported_methods", concatenateCollectionToString(detailsEx.getSupportedMethods())) ); } - if (ex instanceof NotAcceptableStatusException) { - NotAcceptableStatusException detailsEx = (NotAcceptableStatusException)ex; + if (ex instanceof NotAcceptableStatusException detailsEx) { extraDetailsForLogging.add( Pair.of("supported_media_types", concatenateCollectionToString(detailsEx.getSupportedMediaTypes())) ); } - if (ex instanceof ServerErrorException) { - ServerErrorException detailsEx = (ServerErrorException)ex; + if (ex instanceof ServerErrorException detailsEx) { extraDetailsForLogging.add( Pair.of("method_parameter", String.valueOf(detailsEx.getMethodParameter())) ); @@ -540,15 +535,13 @@ protected void addExtraDetailsForLoggingForResponseStatusException( ); } - if (ex instanceof ServerWebInputException) { - ServerWebInputException detailsEx = (ServerWebInputException)ex; + if (ex instanceof ServerWebInputException detailsEx) { extraDetailsForLogging.add( Pair.of("method_parameter", String.valueOf(detailsEx.getMethodParameter())) ); } - if (ex instanceof UnsupportedMediaTypeStatusException) { - UnsupportedMediaTypeStatusException detailsEx = (UnsupportedMediaTypeStatusException)ex; + if (ex instanceof UnsupportedMediaTypeStatusException detailsEx) { extraDetailsForLogging.add( Pair.of("supported_media_types", concatenateCollectionToString(detailsEx.getSupportedMediaTypes())) ); @@ -602,8 +595,7 @@ protected void addExtraDetailsForLoggingForResponseStatusException( @NotNull ResponseStatusException ex, @NotNull String[] exReasonWords, @NotNull String exReason ) { // Check for an exception type where we can get the info without parsing strings. - if (ex instanceof MissingRequestValueException) { - MissingRequestValueException detailsEx = (MissingRequestValueException)ex; + if (ex instanceof MissingRequestValueException detailsEx) { return new RequiredParamData( detailsEx.getName(), extractRequiredTypeNoInfoLeak(detailsEx.getType()), diff --git a/backstopper-spring-web/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListenerTest.java b/backstopper-spring-web/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListenerTest.java index 079a0bc..9343a1e 100644 --- a/backstopper-spring-web/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListenerTest.java +++ b/backstopper-spring-web/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListenerTest.java @@ -90,7 +90,7 @@ public class OneOffSpringCommonFrameworkExceptionHandlerListenerTest extends ListenerTestBase { private static final ProjectApiErrors testProjectApiErrors = ProjectApiErrorsForTesting.withProjectSpecificData(null, null); - private OneOffSpringCommonFrameworkExceptionHandlerListener listener = new OneOffListenerBasicImpl( + private final OneOffSpringCommonFrameworkExceptionHandlerListener listener = new OneOffListenerBasicImpl( testProjectApiErrors, ApiExceptionHandlerUtils.DEFAULT_IMPL ); @@ -407,7 +407,7 @@ public void shouldHandleException_returns_MALFORMED_REQUEST_for_generic_HttpMess private enum HttpMessageNotReadableExceptionScenario { KNOWN_MESSAGE_FOR_MISSING_CONTENT( - new HttpMessageNotReadableException("Required request body is missing " + UUID.randomUUID().toString()), + new HttpMessageNotReadableException("Required request body is missing " + UUID.randomUUID()), ProjectApiErrors::getMissingExpectedContentApiError ), JSON_MAPPING_EXCEPTION_CAUSE_INDICATING_NO_CONTENT( @@ -1116,7 +1116,7 @@ public void shouldHandleException_handles_ServerErrorException_as_expected( ServerErrorException ex = (nullDetails) - ? new ServerErrorException("Some reason", (Throwable) null) + ? new ServerErrorException("Some reason", null) : new ServerErrorException("Some reason", details, null); List> expectedExtraDetailsForLogging = new ArrayList<>(); @@ -1207,7 +1207,7 @@ public void shouldHandleException_handles_MissingRequestValueException_as_expect paramIndex ); - String missingParamName = "some-param-" + UUID.randomUUID().toString(); + String missingParamName = "some-param-" + UUID.randomUUID(); MissingRequestValueException ex = new MissingRequestValueException( missingParamName, int.class, diff --git a/nike-internal-util/src/main/java/com/nike/internal/util/Pair.java b/nike-internal-util/src/main/java/com/nike/internal/util/Pair.java index e55ec34..872099f 100644 --- a/nike-internal-util/src/main/java/com/nike/internal/util/Pair.java +++ b/nike-internal-util/src/main/java/com/nike/internal/util/Pair.java @@ -140,8 +140,7 @@ public boolean equals(final Object obj) { if (obj == this) { return true; } - if (obj instanceof Map.Entry) { - final Map.Entry other = (Map.Entry) obj; + if (obj instanceof Map.Entry other) { return Objects.equals(getKey(), other.getKey()) && Objects.equals(getValue(), other.getValue()); } @@ -168,7 +167,7 @@ public int hashCode() { */ @Override public String toString() { - return new StringBuilder().append('(').append(getLeft()).append(',').append(getRight()).append(')').toString(); + return "(" + getLeft() + ',' + getRight() + ')'; } /** diff --git a/nike-internal-util/src/main/java/com/nike/internal/util/StringUtils.java b/nike-internal-util/src/main/java/com/nike/internal/util/StringUtils.java index 0287538..db271c4 100644 --- a/nike-internal-util/src/main/java/com/nike/internal/util/StringUtils.java +++ b/nike-internal-util/src/main/java/com/nike/internal/util/StringUtils.java @@ -64,7 +64,7 @@ public static String join(Collection iterable, String delimiter, String prefi for (Object obj : iterable) { if (!firstItem) sb.append(delimiter); - sb.append(String.valueOf(obj)); + sb.append(obj); firstItem = false; } @@ -140,7 +140,7 @@ public static boolean isBlank(final CharSequence cs) { return true; } for (int i = 0; i < strLen; i++) { - if (Character.isWhitespace(cs.charAt(i)) == false) { + if (!Character.isWhitespace(cs.charAt(i))) { return false; } } diff --git a/samples/sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample/error/SampleProjectApiError.java b/samples/sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample/error/SampleProjectApiError.java index 196b88b..6b543d9 100644 --- a/samples/sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample/error/SampleProjectApiError.java +++ b/samples/sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample/error/SampleProjectApiError.java @@ -70,14 +70,14 @@ public enum SampleProjectApiError implements ApiError { SampleProjectApiError(int errorCode, String message, int httpStatusCode) { this(new ApiErrorBase( - "delegated-to-enum-wrapper-" + UUID.randomUUID().toString(), errorCode, message, httpStatusCode + "delegated-to-enum-wrapper-" + UUID.randomUUID(), errorCode, message, httpStatusCode )); } @SuppressWarnings("unused") SampleProjectApiError(int errorCode, String message, int httpStatusCode, Map metadata) { this(new ApiErrorBase( - "delegated-to-enum-wrapper-" + UUID.randomUUID().toString(), errorCode, message, httpStatusCode, metadata + "delegated-to-enum-wrapper-" + UUID.randomUUID(), errorCode, message, httpStatusCode, metadata )); } diff --git a/samples/sample-spring-boot3-webmvc/src/main/java/com/nike/backstopper/springboot3webmvcsample/error/SampleProjectApiError.java b/samples/sample-spring-boot3-webmvc/src/main/java/com/nike/backstopper/springboot3webmvcsample/error/SampleProjectApiError.java index 45ffe0e..492af40 100644 --- a/samples/sample-spring-boot3-webmvc/src/main/java/com/nike/backstopper/springboot3webmvcsample/error/SampleProjectApiError.java +++ b/samples/sample-spring-boot3-webmvc/src/main/java/com/nike/backstopper/springboot3webmvcsample/error/SampleProjectApiError.java @@ -55,14 +55,14 @@ public enum SampleProjectApiError implements ApiError { SampleProjectApiError(int errorCode, String message, int httpStatusCode) { this(new ApiErrorBase( - "delegated-to-enum-wrapper-" + UUID.randomUUID().toString(), errorCode, message, httpStatusCode + "delegated-to-enum-wrapper-" + UUID.randomUUID(), errorCode, message, httpStatusCode )); } @SuppressWarnings("unused") SampleProjectApiError(int errorCode, String message, int httpStatusCode, Map metadata) { this(new ApiErrorBase( - "delegated-to-enum-wrapper-" + UUID.randomUUID().toString(), errorCode, message, httpStatusCode, metadata + "delegated-to-enum-wrapper-" + UUID.randomUUID(), errorCode, message, httpStatusCode, metadata )); } diff --git a/testonly/testonly-spring-webmvc-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/error/SampleProjectApiError.java b/testonly/testonly-spring-webmvc-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/error/SampleProjectApiError.java index 1181d61..32e4937 100644 --- a/testonly/testonly-spring-webmvc-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/error/SampleProjectApiError.java +++ b/testonly/testonly-spring-webmvc-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/error/SampleProjectApiError.java @@ -73,14 +73,14 @@ public enum SampleProjectApiError implements ApiError { SampleProjectApiError(int errorCode, String message, int httpStatusCode) { this(new ApiErrorBase( - "delegated-to-enum-wrapper-" + UUID.randomUUID().toString(), errorCode, message, httpStatusCode + "delegated-to-enum-wrapper-" + UUID.randomUUID(), errorCode, message, httpStatusCode )); } @SuppressWarnings("unused") SampleProjectApiError(int errorCode, String message, int httpStatusCode, Map metadata) { this(new ApiErrorBase( - "delegated-to-enum-wrapper-" + UUID.randomUUID().toString(), errorCode, message, httpStatusCode, metadata + "delegated-to-enum-wrapper-" + UUID.randomUUID(), errorCode, message, httpStatusCode, metadata )); } From c440f748e919e46fe3c4ebd766171e8a7b4a52dc Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Thu, 12 Sep 2024 17:29:14 -0700 Subject: [PATCH 36/42] Clean up some warnings, mostly backstopper-core --- backstopper-core/build.gradle | 1 - .../backstopper/apierror/ApiErrorBase.java | 4 +- .../apierror/ApiErrorWithMetadata.java | 1 + .../backstopper/exception/ApiException.java | 2 +- .../handler/ApiExceptionHandlerBase.java | 21 +++--- .../handler/ApiExceptionHandlerUtils.java | 4 +- .../UnhandledExceptionHandlerBase.java | 4 +- ...entDataValidationErrorHandlerListener.java | 8 +- ...streamNetworkExceptionHandlerListener.java | 6 +- .../GenericApiExceptionHandlerListener.java | 10 +-- ...versideValidationErrorHandlerListener.java | 2 +- .../model/DefaultErrorContractDTO.java | 15 +--- .../backstopper/model/DefaultErrorDTO.java | 13 +--- .../service/ClientDataValidationService.java | 2 +- .../nike/backstopper/util/ApiErrorUtil.java | 6 +- .../apierror/ApiErrorBaseTest.java | 4 + .../apierror/ApiErrorComparatorTest.java | 2 + .../apierror/ApiErrorWithMetadataTest.java | 5 ++ .../apierror/SortedApiErrorSetTest.java | 1 + .../ProjectApiErrorsTestBase.java | 2 +- .../range/IntegerRangeTest.java | 8 +- .../exception/ApiExceptionTest.java | 9 +++ .../handler/ApiExceptionHandlerBaseTest.java | 60 ++++++++------- .../handler/ApiExceptionHandlerUtilsTest.java | 5 +- .../handler/ErrorResponseInfoTest.java | 1 - .../UnhandledExceptionHandlerBaseTest.java | 74 ++++++------------- ...ataValidationErrorHandlerListenerTest.java | 53 +++++++------ ...amNetworkExceptionHandlerListenerTest.java | 13 +--- ...ideValidationErrorHandlerListenerTest.java | 52 ++++++------- .../model/DefaultErrorDTOTest.java | 51 +++++++------ .../ClientDataValidationServiceTest.java | 2 + ...ilFastServersideValidationServiceTest.java | 10 ++- .../service/NoOpJsr303ValidatorTest.java | 16 +--- .../backstopper/util/ApiErrorUtilTest.java | 9 --- .../StringConvertsToClassTypeValidator.java | 8 +- ...tringConvertsToClassTypeValidatorTest.java | 2 +- ...ithDefaultErrorContractDTOSupportTest.java | 21 ++---- ...ctionBasedJsr303AnnotationTrollerBase.java | 5 +- ...alidationMessagesPointToApiErrorsTest.java | 2 +- ...ApiExceptionHandlerServletApiBaseTest.java | 1 - ...ledExceptionHandlerServletApiBaseTest.java | 4 +- .../handler/ClientfacingErrorITest.java | 2 +- .../spring/SpringApiExceptionHandlerTest.java | 1 - .../SpringApiExceptionHandlerUtilsTest.java | 3 +- .../com/nike/internal/util/ImmutablePair.java | 2 +- .../java/com/nike/internal/util/Pair.java | 2 +- .../com/nike/internal/util/StringUtils.java | 2 +- ...xpectedErrorsAreReturnedComponentTest.java | 2 +- 48 files changed, 244 insertions(+), 289 deletions(-) diff --git a/backstopper-core/build.gradle b/backstopper-core/build.gradle index 59e25dc..2c46691 100644 --- a/backstopper-core/build.gradle +++ b/backstopper-core/build.gradle @@ -19,6 +19,5 @@ dependencies { "org.assertj:assertj-core:$assertJVersion", "com.tngtech.java:junit-dataprovider:$junitDataproviderVersion", "org.hamcrest:hamcrest-all:$hamcrestVersion", - "org.hibernate.validator:hibernate-validator:$hibernateValidatorVersion" ) } diff --git a/backstopper-core/src/main/java/com/nike/backstopper/apierror/ApiErrorBase.java b/backstopper-core/src/main/java/com/nike/backstopper/apierror/ApiErrorBase.java index dd8246b..8cd7a24 100644 --- a/backstopper-core/src/main/java/com/nike/backstopper/apierror/ApiErrorBase.java +++ b/backstopper-core/src/main/java/com/nike/backstopper/apierror/ApiErrorBase.java @@ -3,7 +3,6 @@ import com.nike.backstopper.util.ApiErrorUtil; import java.util.Collections; -import java.util.HashMap; import java.util.Map; /** @@ -42,7 +41,7 @@ public ApiErrorBase(String name, String errorCode, String message, int httpStatu this.metadata = (metadata.isEmpty()) ? Collections.emptyMap() - : Collections.unmodifiableMap(new HashMap<>(metadata)); + : Map.copyOf(metadata); } public ApiErrorBase(String name, int errorCode, String message, int httpStatusCode, Map metadata) { @@ -106,6 +105,7 @@ public Map getMetadata() { } @Override + @SuppressWarnings("EqualsWhichDoesntCheckParameterClass") public boolean equals(Object o) { return ApiErrorUtil.isApiErrorEqual(this, o); } diff --git a/backstopper-core/src/main/java/com/nike/backstopper/apierror/ApiErrorWithMetadata.java b/backstopper-core/src/main/java/com/nike/backstopper/apierror/ApiErrorWithMetadata.java index 06b9bcb..c87d527 100644 --- a/backstopper-core/src/main/java/com/nike/backstopper/apierror/ApiErrorWithMetadata.java +++ b/backstopper-core/src/main/java/com/nike/backstopper/apierror/ApiErrorWithMetadata.java @@ -83,6 +83,7 @@ public int getHttpStatusCode() { } @Override + @SuppressWarnings("EqualsWhichDoesntCheckParameterClass") public boolean equals(Object o) { return ApiErrorUtil.isApiErrorEqual(this, o); } diff --git a/backstopper-core/src/main/java/com/nike/backstopper/exception/ApiException.java b/backstopper-core/src/main/java/com/nike/backstopper/exception/ApiException.java index 51a2c43..4706ecb 100644 --- a/backstopper-core/src/main/java/com/nike/backstopper/exception/ApiException.java +++ b/backstopper-core/src/main/java/com/nike/backstopper/exception/ApiException.java @@ -216,7 +216,7 @@ protected static String extractMessage(ApiError error) { /** * Extracts and joins all messages from the input List<{@link ApiError}> if the desired message is null. - * + *

* Will return null if the input error List is null */ protected static String extractMessage(List apiErrors, String desiredMessage) { diff --git a/backstopper-core/src/main/java/com/nike/backstopper/handler/ApiExceptionHandlerBase.java b/backstopper-core/src/main/java/com/nike/backstopper/handler/ApiExceptionHandlerBase.java index 219ae8e..2dbdfbf 100644 --- a/backstopper-core/src/main/java/com/nike/backstopper/handler/ApiExceptionHandlerBase.java +++ b/backstopper-core/src/main/java/com/nike/backstopper/handler/ApiExceptionHandlerBase.java @@ -293,20 +293,20 @@ protected ErrorResponseInfo doHandleApiException( // Bulletproof against somehow getting a completely empty collection of client errors. This should never happen // but if it does we want a reasonable response. - if (filteredClientErrors == null || filteredClientErrors.size() == 0) { + if (filteredClientErrors == null || filteredClientErrors.isEmpty()) { ApiError genericServiceError = projectApiErrors.getGenericServiceError(); UUID trackingUuid = UUID.randomUUID(); String trackingLogKey = "bad_handler_logic_tracking_uuid"; extraDetailsForLogging.add(Pair.of(trackingLogKey, trackingUuid.toString())); - logger.error(String.format( + logger.error( "Found a situation where we ended up with 0 ApiErrors to return to the client. This should not happen " + "and likely indicates a logic error in ApiExceptionHandlerBase, or a ProjectApiErrors that isn't " - + "setup properly. Defaulting to " + genericServiceError.getName() + " for now, but this should be " - + "investigated and fixed. Search for %s=%s in the logs to find the log message that contains the " + + "setup properly. Defaulting to {} for now, but this should be " + + "investigated and fixed. Search for {}={} in the logs to find the log message that contains the " + "details of the request along with the full stack trace of the original exception. " - + "unfiltered_api_errors=%s", - trackingLogKey, trackingUuid, utils.concatenateErrorCollection(clientErrors) - )); + + "unfiltered_api_errors={}", + genericServiceError.getName(), trackingLogKey, trackingUuid, utils.concatenateErrorCollection(clientErrors) + ); filteredClientErrors = Collections.singletonList(genericServiceError); highestPriorityStatusCode = genericServiceError.getHttpStatusCode(); } @@ -375,8 +375,11 @@ protected ErrorResponseInfo doHandleApiException( */ @SuppressWarnings("UnusedParameters") protected boolean shouldLogStackTrace( - int statusCode, Collection filteredClientErrors, Throwable originalException, - Throwable coreException, RequestInfoForLogging request + int statusCode, + @SuppressWarnings("unused") Collection filteredClientErrors, + @SuppressWarnings("unused") Throwable originalException, + Throwable coreException, + @SuppressWarnings("unused") RequestInfoForLogging request ) { if (coreException instanceof ApiException) { // See if this ApiException is explicitly requesting stack trace logging to be forced on or off. diff --git a/backstopper-core/src/main/java/com/nike/backstopper/handler/ApiExceptionHandlerUtils.java b/backstopper-core/src/main/java/com/nike/backstopper/handler/ApiExceptionHandlerUtils.java index 15f8f96..c1dad04 100644 --- a/backstopper-core/src/main/java/com/nike/backstopper/handler/ApiExceptionHandlerUtils.java +++ b/backstopper-core/src/main/java/com/nike/backstopper/handler/ApiExceptionHandlerUtils.java @@ -299,8 +299,8 @@ public String parseSpecificHeaderToString(RequestInfoForLogging request, String /** - * @param sensitiveHeaders - * @param headerName + * @param sensitiveHeaders The set of sensitive headers names. + * @param headerName The header name in question. * @return Returns true if the header name is one of the sensitive headers in lower, upper or camel case. */ private static boolean containsCaseInSensitive(Set sensitiveHeaders, String headerName) { diff --git a/backstopper-core/src/main/java/com/nike/backstopper/handler/UnhandledExceptionHandlerBase.java b/backstopper-core/src/main/java/com/nike/backstopper/handler/UnhandledExceptionHandlerBase.java index a7e1e73..1137881 100644 --- a/backstopper-core/src/main/java/com/nike/backstopper/handler/UnhandledExceptionHandlerBase.java +++ b/backstopper-core/src/main/java/com/nike/backstopper/handler/UnhandledExceptionHandlerBase.java @@ -170,7 +170,7 @@ public ErrorResponseInfo handleException(Throwable ex, RequestInfoForLogging try { body = request.getBody(); } catch (RequestInfoForLogging.GetBodyException e) { - logger.warn("Failed to retrieve request_body while handling exception ex=" + ex, e); + logger.warn("Failed to retrieve request_body while handling exception ex={}", ex, e); body = "[ERROR_EXTRACING_BODY]"; } @@ -229,7 +229,7 @@ public ErrorResponseInfo handleException(Throwable ex, RequestInfoForLogging * Note that in many frameworks if the body has already been read once then it cannot be read again, * so you may have more work to do than just setting this method to return true. */ - @SuppressWarnings("UnusedParameters") + @SuppressWarnings("unused") protected boolean logRequestBodyOnUnhandledExceptions(Throwable ex, RequestInfoForLogging request) { return false; } diff --git a/backstopper-core/src/main/java/com/nike/backstopper/handler/listener/impl/ClientDataValidationErrorHandlerListener.java b/backstopper-core/src/main/java/com/nike/backstopper/handler/listener/impl/ClientDataValidationErrorHandlerListener.java index 43d060b..508c2e5 100644 --- a/backstopper-core/src/main/java/com/nike/backstopper/handler/listener/impl/ClientDataValidationErrorHandlerListener.java +++ b/backstopper-core/src/main/java/com/nike/backstopper/handler/listener/impl/ClientDataValidationErrorHandlerListener.java @@ -80,10 +80,10 @@ protected SortedApiErrorSet processClientDataValidationError(ClientDataValidatio List> extraDetailsForLogging) { // Add info about the objects that failed validation. - if (ex.getObjectsThatFailedValidation() != null && ex.getObjectsThatFailedValidation().size() > 0) { + if (ex.getObjectsThatFailedValidation() != null && !ex.getObjectsThatFailedValidation().isEmpty()) { StringBuilder sb = new StringBuilder(); for (Object obj : ex.getObjectsThatFailedValidation()) { - if (sb.length() > 0) + if (!sb.isEmpty()) sb.append(","); sb.append(obj.getClass().getName()); } @@ -94,7 +94,7 @@ protected SortedApiErrorSet processClientDataValidationError(ClientDataValidatio if (ex.getValidationGroups() != null && ex.getValidationGroups().length > 0) { StringBuilder sb = new StringBuilder(); for (Class group : ex.getValidationGroups()) { - if (sb.length() > 0) + if (!sb.isEmpty()) sb.append(","); sb.append(group.getName()); } @@ -108,7 +108,7 @@ protected SortedApiErrorSet processClientDataValidationError(ClientDataValidatio // Add full details about the violations. StringBuilder sb = new StringBuilder(); for (ConstraintViolation violation : ex.getViolations()) { - if (sb.length() > 0) + if (!sb.isEmpty()) sb.append(","); sb.append(violation.getRootBeanClass().getSimpleName()) diff --git a/backstopper-core/src/main/java/com/nike/backstopper/handler/listener/impl/DownstreamNetworkExceptionHandlerListener.java b/backstopper-core/src/main/java/com/nike/backstopper/handler/listener/impl/DownstreamNetworkExceptionHandlerListener.java index 3460dcb..cb55f8e 100644 --- a/backstopper-core/src/main/java/com/nike/backstopper/handler/listener/impl/DownstreamNetworkExceptionHandlerListener.java +++ b/backstopper-core/src/main/java/com/nike/backstopper/handler/listener/impl/DownstreamNetworkExceptionHandlerListener.java @@ -112,7 +112,7 @@ protected SortedApiErrorSet processServerHttpStatusCodeException(ServerHttpStatu * @return true if the given exception should propagate a 429 error to the original caller, false otherwise. * This method will not be called unless the given exception is a 429. */ - @SuppressWarnings("UnusedParameters") + @SuppressWarnings("unused") protected boolean shouldPropagate429Error(ServerHttpStatusCodeException ex) { return true; } @@ -120,7 +120,7 @@ protected boolean shouldPropagate429Error(ServerHttpStatusCodeException ex) { /** * @return true if we should log the raw response from the given exception, false otherwise. */ - @SuppressWarnings("UnusedParameters") + @SuppressWarnings("unused") protected boolean shouldLogRawResponse(ServerHttpStatusCodeException ex) { return true; } @@ -144,7 +144,7 @@ protected SortedApiErrorSet processServerUnknownHttpStatusCodeException( * add any relevant log data pairs to extraDetailsForLogging if it returns true. */ protected boolean isTemporaryProblem( - Throwable ex, @SuppressWarnings("UnusedParameters") List> extraDetailsForLogging + Throwable ex, @SuppressWarnings("unused") List> extraDetailsForLogging ) { if (ex instanceof TimeoutException) return true; diff --git a/backstopper-core/src/main/java/com/nike/backstopper/handler/listener/impl/GenericApiExceptionHandlerListener.java b/backstopper-core/src/main/java/com/nike/backstopper/handler/listener/impl/GenericApiExceptionHandlerListener.java index ba9a496..e1cbc2a 100644 --- a/backstopper-core/src/main/java/com/nike/backstopper/handler/listener/impl/GenericApiExceptionHandlerListener.java +++ b/backstopper-core/src/main/java/com/nike/backstopper/handler/listener/impl/GenericApiExceptionHandlerListener.java @@ -15,8 +15,8 @@ /** * Handles generic {@link ApiException} errors by simply setting {@link ApiExceptionHandlerListenerResult#errors} to - * {@link ApiException#apiErrors} and adding any {@link ApiException#extraDetailsForLogging} and/or - * {@link ApiException#extraResponseHeaders}. + * {@link ApiException#getApiErrors()} and adding any {@link ApiException#getExtraDetailsForLogging()} and/or + * {@link ApiException#getExtraResponseHeaders()}. */ @Named @Singleton @@ -32,12 +32,10 @@ public ApiExceptionHandlerListenerResult shouldHandleException(Throwable ex) { errors.addAll(apiException.getApiErrors()); // Add all the extra details for logging from the exception. - List> messages = new ArrayList<>(); - messages.addAll(apiException.getExtraDetailsForLogging()); + List> messages = new ArrayList<>(apiException.getExtraDetailsForLogging()); // Add all the extra response headers from the exception. - List>> headers = new ArrayList<>(); - headers.addAll(apiException.getExtraResponseHeaders()); + List>> headers = new ArrayList<>(apiException.getExtraResponseHeaders()); // Include the ApiException's message as a logged key/value pair. if (StringUtils.isNotBlank(ex.getMessage())) diff --git a/backstopper-core/src/main/java/com/nike/backstopper/handler/listener/impl/ServersideValidationErrorHandlerListener.java b/backstopper-core/src/main/java/com/nike/backstopper/handler/listener/impl/ServersideValidationErrorHandlerListener.java index 558fe6b..8c98968 100644 --- a/backstopper-core/src/main/java/com/nike/backstopper/handler/listener/impl/ServersideValidationErrorHandlerListener.java +++ b/backstopper-core/src/main/java/com/nike/backstopper/handler/listener/impl/ServersideValidationErrorHandlerListener.java @@ -96,7 +96,7 @@ protected SortedApiErrorSet processServersideValidationError(ServersideValidatio if (ex.getViolations() != null) { StringBuilder sb = new StringBuilder(); for (ConstraintViolation violation : ex.getViolations()) { - if (sb.length() > 0) + if (!sb.isEmpty()) sb.append(", "); sb.append(violation.getPropertyPath().toString()) diff --git a/backstopper-core/src/main/java/com/nike/backstopper/model/DefaultErrorContractDTO.java b/backstopper-core/src/main/java/com/nike/backstopper/model/DefaultErrorContractDTO.java index d71175c..ae25c32 100644 --- a/backstopper-core/src/main/java/com/nike/backstopper/model/DefaultErrorContractDTO.java +++ b/backstopper-core/src/main/java/com/nike/backstopper/model/DefaultErrorContractDTO.java @@ -1,9 +1,6 @@ package com.nike.backstopper.model; import com.nike.backstopper.apierror.ApiError; -import com.nike.backstopper.handler.ApiExceptionHandlerBase; -import com.nike.backstopper.handler.RequestInfoForLogging; -import com.nike.backstopper.handler.UnhandledExceptionHandlerBase; import java.io.Serializable; import java.util.ArrayList; @@ -20,10 +17,8 @@ * available in {@link ApiError}, making the errors your app throws and what shows up to the caller in the response * closely related and easy to reason about, but you are not required to use it - ultimately you control what gets * returned to the client based on what - * {@link ApiExceptionHandlerBase#prepareFrameworkRepresentation(DefaultErrorContractDTO, int, Collection, Throwable, - * RequestInfoForLogging)} or - * {@link UnhandledExceptionHandlerBase#prepareFrameworkRepresentation(DefaultErrorContractDTO, int, Collection, - * Throwable, RequestInfoForLogging)} returns. + * {@code ApiExceptionHandlerBase.prepareFrameworkRepresentation(...)} or + * {@code UnhandledExceptionHandlerBase.prepareFrameworkRepresentation(...)} returns. * * @author Nic Munroe */ @@ -76,13 +71,11 @@ public DefaultErrorContractDTO(String error_id, Collection apiErrors) * compile. */ public DefaultErrorContractDTO(String error_id, Collection errorsToCopy, - @SuppressWarnings("UnusedParameters") Void passInNullForThisArg) { + @SuppressWarnings("unused") Void passInNullForThisArg) { this.error_id = error_id; List errorsList = new ArrayList<>(); if (errorsToCopy != null) { - for (DefaultErrorDTO apiError : errorsToCopy) { - errorsList.add(apiError); - } + errorsList.addAll(errorsToCopy); } this.errors = Collections.unmodifiableList(errorsList); } diff --git a/backstopper-core/src/main/java/com/nike/backstopper/model/DefaultErrorDTO.java b/backstopper-core/src/main/java/com/nike/backstopper/model/DefaultErrorDTO.java index 9403a39..e48070c 100644 --- a/backstopper-core/src/main/java/com/nike/backstopper/model/DefaultErrorDTO.java +++ b/backstopper-core/src/main/java/com/nike/backstopper/model/DefaultErrorDTO.java @@ -2,14 +2,9 @@ import com.nike.backstopper.apierror.ApiError; import com.nike.backstopper.apierror.ApiErrorWithMetadata; -import com.nike.backstopper.handler.ApiExceptionHandlerBase; -import com.nike.backstopper.handler.RequestInfoForLogging; -import com.nike.backstopper.handler.UnhandledExceptionHandlerBase; import java.io.Serializable; -import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.Map; /** @@ -18,10 +13,8 @@ * in {@link ApiError} making the errors your app throws and what shows up to the client in the response closely * related and easy to reason about, but you are not required to use it - ultimately you control what gets returned * to the client based on what - * {@link ApiExceptionHandlerBase#prepareFrameworkRepresentation(DefaultErrorContractDTO, int, Collection, Throwable, - * RequestInfoForLogging)} or - * {@link UnhandledExceptionHandlerBase#prepareFrameworkRepresentation(DefaultErrorContractDTO, int, Collection, - * Throwable, RequestInfoForLogging)} returns. + * {@code ApiExceptionHandlerBase.prepareFrameworkRepresentation()} or + * {@code UnhandledExceptionHandlerBase.prepareFrameworkRepresentation()} returns. * * @author Nic Munroe */ @@ -81,6 +74,6 @@ public DefaultErrorDTO(String code, String message, Map metadata metadata = Collections.emptyMap(); this.metadata = (metadata.isEmpty()) ? Collections.emptyMap() - : Collections.unmodifiableMap(new HashMap<>(metadata)); + : Map.copyOf(metadata); } } diff --git a/backstopper-core/src/main/java/com/nike/backstopper/service/ClientDataValidationService.java b/backstopper-core/src/main/java/com/nike/backstopper/service/ClientDataValidationService.java index 96ab140..09a8a52 100644 --- a/backstopper-core/src/main/java/com/nike/backstopper/service/ClientDataValidationService.java +++ b/backstopper-core/src/main/java/com/nike/backstopper/service/ClientDataValidationService.java @@ -84,7 +84,7 @@ public void validateObjectsWithGroupFailFast(Class group, Object... validateT */ public void validateObjectsWithGroupsFailFast(Collection> groups, Object... validateTheseObjects) { Class[] groupsArray = - (groups == null || groups.size() == 0) ? null : groups.toArray(new Class[groups.size()]); + (groups == null || groups.isEmpty()) ? null : groups.toArray(new Class[0]); validateObjectsWithGroupsFailFast(groupsArray, validateTheseObjects); } diff --git a/backstopper-core/src/main/java/com/nike/backstopper/util/ApiErrorUtil.java b/backstopper-core/src/main/java/com/nike/backstopper/util/ApiErrorUtil.java index 4ab7d1e..c88e341 100644 --- a/backstopper-core/src/main/java/com/nike/backstopper/util/ApiErrorUtil.java +++ b/backstopper-core/src/main/java/com/nike/backstopper/util/ApiErrorUtil.java @@ -11,6 +11,10 @@ */ public class ApiErrorUtil { + private ApiErrorUtil() { + // Do nothing. + } + /** * Method for generating a hashcode for the given {@link ApiError} . This can be used in implementations of * {@link ApiError}. @@ -25,7 +29,7 @@ public static int generateApiErrorHashCode(ApiError apiError) { public static boolean isApiErrorEqual(ApiError apiError, Object o) { if (apiError == o) return true; if (apiError == null) return false; - if (o == null || !(o instanceof ApiError that)) return false; + if (!(o instanceof ApiError that)) return false; return apiError.getHttpStatusCode() == that.getHttpStatusCode() && Objects.equals(apiError.getName(), that.getName()) && Objects.equals(apiError.getErrorCode(), that.getErrorCode()) && diff --git a/backstopper-core/src/test/java/com/nike/backstopper/apierror/ApiErrorBaseTest.java b/backstopper-core/src/test/java/com/nike/backstopper/apierror/ApiErrorBaseTest.java index 1b4828b..2a5a224 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/apierror/ApiErrorBaseTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/apierror/ApiErrorBaseTest.java @@ -23,6 +23,7 @@ public class ApiErrorBaseTest { @Test public void constructor_should_throw_IllegalArgumentException_null_name() { // when + @SuppressWarnings("DataFlowIssue") Throwable ex = catchThrowable(() -> new ApiErrorBase(null, 42, "some error", 400)); // then @@ -34,6 +35,7 @@ public void constructor_should_throw_IllegalArgumentException_null_name() { @Test public void constructor_should_throw_IllegalArgumentException_null_error_code() { // when + @SuppressWarnings("DataFlowIssue") Throwable ex = catchThrowable(() -> new ApiErrorBase(UUID.randomUUID().toString(), null, "some error", 400)); // then @@ -106,6 +108,7 @@ public void equals_same_object_is_true() { ApiErrorBase error = new ApiErrorBase("name", 42, "errorMessage", 400); // then + //noinspection EqualsWithItself assertThat(error.equals(error)).isTrue(); } @@ -121,6 +124,7 @@ public void equals_null_or_other_class_is_false(boolean useNull) { String otherClass = useNull? null : ""; // then + //noinspection EqualsBetweenInconvertibleTypes assertThat(error.equals(otherClass)).isFalse(); } diff --git a/backstopper-core/src/test/java/com/nike/backstopper/apierror/ApiErrorComparatorTest.java b/backstopper-core/src/test/java/com/nike/backstopper/apierror/ApiErrorComparatorTest.java index 33b7468..26e5ac0 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/apierror/ApiErrorComparatorTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/apierror/ApiErrorComparatorTest.java @@ -24,11 +24,13 @@ public class ApiErrorComparatorTest { @Test public void should_return_0_for_reference_equality() { ApiError mockApiError = mock(ApiError.class); + //noinspection EqualsWithItself assertThat(comparator.compare(mockApiError, mockApiError)).isEqualTo(0); } @Test public void should_return_0_for_both_null() { + //noinspection EqualsWithItself assertThat(comparator.compare(null, null)).isEqualTo(0); } diff --git a/backstopper-core/src/test/java/com/nike/backstopper/apierror/ApiErrorWithMetadataTest.java b/backstopper-core/src/test/java/com/nike/backstopper/apierror/ApiErrorWithMetadataTest.java index 3117b57..d101b6c 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/apierror/ApiErrorWithMetadataTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/apierror/ApiErrorWithMetadataTest.java @@ -86,6 +86,7 @@ public void cconvenience_onstructor_sets_delegate_and_combo_metadata_with_extra_ @Test public void constructor_throws_IllegalArgumentException_if_delegate_is_null() { // when + @SuppressWarnings("DataFlowIssue") Throwable ex = catchThrowable(() -> new ApiErrorWithMetadata(null, extraMetadata)); // then @@ -97,6 +98,7 @@ public void constructor_throws_IllegalArgumentException_if_delegate_is_null() { @Test public void convenience_constructor_throws_IllegalArgumentException_if_delegate_is_null() { // when + @SuppressWarnings("DataFlowIssue") Throwable ex = catchThrowable(() -> new ApiErrorWithMetadata(null, Pair.of("foo", "bar"))); // then @@ -168,6 +170,7 @@ public void constructor_supports_null_or_empty_extra_metadata(boolean useNull) { @Test public void convenience_constructor_supports_null_or_empty_extra_metadata(boolean useNull) { // given + @SuppressWarnings("unchecked") Pair[] extraMetadataToUse = (useNull) ? null : new Pair[0]; // when @@ -224,6 +227,7 @@ public void equals_same_object_is_true() { ApiErrorWithMetadata awm = new ApiErrorWithMetadata(delegateWithMetadata, extraMetadata); // then + //noinspection EqualsWithItself assertThat(awm.equals(awm)).isTrue(); } @@ -239,6 +243,7 @@ public void equals_null_or_other_class_is_false(boolean useNull) { String otherClass = useNull? null : ""; // then + //noinspection EqualsBetweenInconvertibleTypes assertThat(awm.equals(otherClass)).isFalse(); } diff --git a/backstopper-core/src/test/java/com/nike/backstopper/apierror/SortedApiErrorSetTest.java b/backstopper-core/src/test/java/com/nike/backstopper/apierror/SortedApiErrorSetTest.java index eb556b4..c919799 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/apierror/SortedApiErrorSetTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/apierror/SortedApiErrorSetTest.java @@ -68,6 +68,7 @@ public void one_arg_constructor_with_values_uses_ApiErrorComparator_and_adds_val @Test public void one_arg_constructor_with_comparator_uses_supplied_comparator() { // given + @SuppressWarnings("unchecked") Comparator customComparator = mock(Comparator.class); // when diff --git a/backstopper-core/src/test/java/com/nike/backstopper/apierror/projectspecificinfo/ProjectApiErrorsTestBase.java b/backstopper-core/src/test/java/com/nike/backstopper/apierror/projectspecificinfo/ProjectApiErrorsTestBase.java index 1d462af..0401e88 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/apierror/projectspecificinfo/ProjectApiErrorsTestBase.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/apierror/projectspecificinfo/ProjectApiErrorsTestBase.java @@ -248,7 +248,7 @@ public void shouldNotContainDuplicateNamedApiErrors() { if (currentCount == null) currentCount = 0; - Integer newCount = currentCount + 1; + int newCount = currentCount + 1; nameToCountMap.put(apiError.getName(), newCount); if (newCount > 1) duplicateErrorNames.add(apiError.getName()); diff --git a/backstopper-core/src/test/java/com/nike/backstopper/apierror/projectspecificinfo/range/IntegerRangeTest.java b/backstopper-core/src/test/java/com/nike/backstopper/apierror/projectspecificinfo/range/IntegerRangeTest.java index 911e13f..25e7ddd 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/apierror/projectspecificinfo/range/IntegerRangeTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/apierror/projectspecificinfo/range/IntegerRangeTest.java @@ -3,7 +3,6 @@ import com.tngtech.java.junit.dataprovider.DataProvider; import com.tngtech.java.junit.dataprovider.DataProviderRunner; -import org.assertj.core.api.ThrowableAssert; import org.junit.Test; import org.junit.runner.RunWith; @@ -23,12 +22,7 @@ public class IntegerRangeTest { @Test public void constructor_throws_exception_if_upper_range_less_than_lower_range() { // when - Throwable ex = catchThrowable(new ThrowableAssert.ThrowingCallable() { - @Override - public void call() throws Throwable { - IntegerRange.of(5, 4); - } - }); + Throwable ex = catchThrowable(() -> IntegerRange.of(5, 4)); // then assertThat(ex).isInstanceOf(IllegalArgumentException.class); diff --git a/backstopper-core/src/test/java/com/nike/backstopper/exception/ApiExceptionTest.java b/backstopper-core/src/test/java/com/nike/backstopper/exception/ApiExceptionTest.java index 2857731..bdd286d 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/exception/ApiExceptionTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/exception/ApiExceptionTest.java @@ -148,6 +148,7 @@ public void constructor_with_builder_arg_throws_IllegalArgumentException_when_pa ApiException.Builder builder = ApiException.newBuilder(); // when + @SuppressWarnings("ThrowableNotThrown") Throwable ex = catchThrowable(() -> new ApiException(builder)); // then @@ -157,6 +158,7 @@ public void constructor_with_builder_arg_throws_IllegalArgumentException_when_pa } @Test(expected = IllegalArgumentException.class) + @SuppressWarnings("ThrowableNotThrown") public void single_error_constructor_fails_if_passed_null_arg() { // expect new ApiException((ApiError)null); @@ -182,6 +184,7 @@ public void single_error_constructor_sets_expected_values() { "false | false", }, splitBy = "\\|") @Test(expected = IllegalArgumentException.class) + @SuppressWarnings({"ThrowableNotThrown", "deprecation"}) public void no_cause_constructors_fail_when_passed_null_or_empty_apiErrors_list( boolean useNull, boolean useConstructorWithResponseHeaders ) { @@ -204,6 +207,7 @@ public void no_cause_constructors_fail_when_passed_null_or_empty_apiErrors_list( "false | false", }, splitBy = "\\|") @Test(expected = IllegalArgumentException.class) + @SuppressWarnings({"ThrowableNotThrown", "deprecation"}) public void with_cause_constructors_fail_when_passed_null_or_empty_apiErrors_list( boolean useNull, boolean useConstructorWithResponseHeaders ) { @@ -224,6 +228,7 @@ public void with_cause_constructors_fail_when_passed_null_or_empty_apiErrors_lis "false" }, splitBy = "\\|") @Test + @SuppressWarnings("deprecation") public void no_cause_constructors_should_translate_null_logging_details_to_empty_list( boolean useConstructorWithResponseHeaders ) { @@ -249,6 +254,7 @@ public void no_cause_constructor_should_translate_null_response_headers_to_empty List apiErrors = Arrays.asList(apiError1, apiError2); // when + @SuppressWarnings("deprecation") ApiException apiException = new ApiException(apiErrors, loggingDetails, null, exceptionMessage); // then @@ -262,6 +268,7 @@ public void no_cause_constructor_should_translate_null_response_headers_to_empty "false" }, splitBy = "\\|") @Test + @SuppressWarnings("deprecation") public void with_cause_constructors_should_translate_null_logging_details_to_empty_list( boolean useConstructorWithResponseHeaders ) { @@ -287,6 +294,7 @@ public void with_cause_constructor_should_translate_null_response_headers_to_emp List apiErrors = Arrays.asList(apiError1, apiError2); // when + @SuppressWarnings("deprecation") ApiException apiException = new ApiException(apiErrors, loggingDetails, null, exceptionMessage, cause); // then @@ -308,6 +316,7 @@ public void extractMessage_listOfErrors_with_message_returns_message() { } @Test + @SuppressWarnings("ConstantValue") public void extractMessage_nullListOfErrors_without_message_returns_null() { // when String extractedMessage = ApiException.extractMessage(null, null); diff --git a/backstopper-core/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerBaseTest.java b/backstopper-core/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerBaseTest.java index f625682..bd9cbfb 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerBaseTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerBaseTest.java @@ -74,7 +74,8 @@ public class ApiExceptionHandlerBaseTest { @Before public void setupMethod() { handler = new TestApiExceptionHandler(); - MockitoAnnotations.initMocks(this); + //noinspection resource + MockitoAnnotations.openMocks(this); } private boolean containsApiError(Collection errorViews, ApiError error) { @@ -118,11 +119,14 @@ private List findAllApiErrorsWithHttpStatusCode(int httpStatusCode) { @Test(expected = IllegalArgumentException.class) public void constructor_throws_IllegalArgumentException_if_passed_null_projectApiErrors() { // expect - new ApiExceptionHandlerBase(null, singletonList(new GenericApiExceptionHandlerListener()), ApiExceptionHandlerUtils.DEFAULT_IMPL) { + new ApiExceptionHandlerBase<>( + null, singletonList(new GenericApiExceptionHandlerListener()), ApiExceptionHandlerUtils.DEFAULT_IMPL + ) { @Override protected Object prepareFrameworkRepresentation( - DefaultErrorContractDTO errorContractDTO, int httpStatusCode, Collection rawFilteredApiErrors, - Throwable originalException, RequestInfoForLogging request) { + DefaultErrorContractDTO errorContractDTO, int httpStatusCode, Collection rawFilteredApiErrors, + Throwable originalException, RequestInfoForLogging request + ) { return null; } }; @@ -131,10 +135,12 @@ protected Object prepareFrameworkRepresentation( @Test(expected = IllegalArgumentException.class) public void constructor_throws_IllegalArgumentException_if_passed_null_listener_list() { // expect - new ApiExceptionHandlerBase(mock(ProjectApiErrors.class), null, ApiExceptionHandlerUtils.DEFAULT_IMPL) { + new ApiExceptionHandlerBase<>( + mock(ProjectApiErrors.class), null, ApiExceptionHandlerUtils.DEFAULT_IMPL + ) { @Override protected Object prepareFrameworkRepresentation( - DefaultErrorContractDTO errorContractDTO, int httpStatusCode, Collection rawFilteredApiErrors, + DefaultErrorContractDTO errorContractDTO, int httpStatusCode, Collection rawFilteredApiErrors, Throwable originalException, RequestInfoForLogging request) { return null; } @@ -144,10 +150,12 @@ protected Object prepareFrameworkRepresentation( @Test(expected = IllegalArgumentException.class) public void constructor_throws_IllegalArgumentException_if_passed_null_apiExceptionHandlerUtils() { // expect - new ApiExceptionHandlerBase(mock(ProjectApiErrors.class), singletonList(new GenericApiExceptionHandlerListener()), null) { + new ApiExceptionHandlerBase<>( + mock(ProjectApiErrors.class), singletonList(new GenericApiExceptionHandlerListener()), null + ) { @Override protected Object prepareFrameworkRepresentation( - DefaultErrorContractDTO errorContractDTO, int httpStatusCode, Collection rawFilteredApiErrors, + DefaultErrorContractDTO errorContractDTO, int httpStatusCode, Collection rawFilteredApiErrors, Throwable originalException, RequestInfoForLogging request) { return null; } @@ -164,7 +172,7 @@ public void maybeHandleException_should_call_doHandleApiException_with_results_o ) throws UnexpectedMajorExceptionHandlingError { // given Throwable ex = new RuntimeException("kaboom"); - ApiExceptionHandlerBase handlerSpy = spy(new TestApiExceptionHandler()); + ApiExceptionHandlerBase handlerSpy = spy(new TestApiExceptionHandler()); ApiExceptionHandlerListenerResult listenerResultMock = (shouldHandle) @@ -176,14 +184,14 @@ public void maybeHandleException_should_call_doHandleApiException_with_results_o : ApiExceptionHandlerListenerResult.ignoreResponse(); doReturn(listenerResultMock).when(handlerSpy).shouldHandleApiException(ex); - ErrorResponseInfo errorResponseInfoMock = mock(ErrorResponseInfo.class); + ErrorResponseInfo errorResponseInfoMock = mock(ErrorResponseInfo.class); doReturn(errorResponseInfoMock).when(handlerSpy).doHandleApiException( - any(SortedApiErrorSet.class), any(List.class), any(List.class), any(Throwable.class), + any(SortedApiErrorSet.class), any(), any(), any(Throwable.class), any(RequestInfoForLogging.class) ); // when - ErrorResponseInfo result = handlerSpy.maybeHandleException(ex, reqMock); + ErrorResponseInfo result = handlerSpy.maybeHandleException(ex, reqMock); // then if (shouldHandle) { @@ -229,7 +237,7 @@ public void shouldReturnAllErrorsWhenAllErrorsAreSameHttpStatusCode() throws Une for (Integer httpStatusCode : testProjectApiErrors.getStatusCodePriorityOrder()) { List allErrorsForThisHttpStatusCode = findAllApiErrorsWithHttpStatusCode(httpStatusCode); // Skip if we have no errors for a status code in the default order list of status codes: - if (allErrorsForThisHttpStatusCode.size() > 0) { + if (!allErrorsForThisHttpStatusCode.isEmpty()) { ErrorResponseInfo result = handler.maybeHandleException(ApiException.newBuilder().withApiErrors(allErrorsForThisHttpStatusCode).build(), reqMock); validateResponse(result, allErrorsForThisHttpStatusCode); } @@ -275,7 +283,7 @@ public void shouldIgnoreExceptionsNotCoveredByAnyHandlerListeners() throws Unexp @Test(expected = UnexpectedMajorExceptionHandlingError.class) public void shouldThrowUnexpectedMajorExceptionHandlingErrorIfBizarroInnerExceptionOccurs() throws UnexpectedMajorExceptionHandlingError { - ErrorResponseInfo result = handler.maybeHandleException(new ApiException(testProjectApiErrors.getGenericServiceError()) { + handler.maybeHandleException(new ApiException(testProjectApiErrors.getGenericServiceError()) { @Override public List getApiErrors() { throw new RuntimeException("Bizarro inner exception"); @@ -335,7 +343,8 @@ public void handleExceptionShouldUseGenericServiceErrorIfProjectApiErrorsDotGetS @Test public void handleExceptionShouldAddErrorIdToResponseHeader() { ApiExceptionHandlerBase handler = new TestApiExceptionHandler(); - ErrorResponseInfo result = handler.doHandleApiException(singletonSortedSetOf(CUSTOM_API_ERROR), new ArrayList>(), + ErrorResponseInfo result = handler.doHandleApiException(singletonSortedSetOf(CUSTOM_API_ERROR), + new ArrayList<>(), null, new Exception(), reqMock); assertThat(result.headersToAddToResponse.get("error_uid"), is( singletonList(result.frameworkRepresentationObj.erv.error_id))); @@ -359,7 +368,8 @@ protected Map> extraHeadersForResponse(TestDTO frameworkRep }; // when - ErrorResponseInfo result = handler.doHandleApiException(singletonSortedSetOf(CUSTOM_API_ERROR), new ArrayList>(), + ErrorResponseInfo result = handler.doHandleApiException(singletonSortedSetOf(CUSTOM_API_ERROR), + new ArrayList<>(), null, new Exception(), reqMock); // then @@ -379,7 +389,7 @@ public void doHandleApiException_should_add_headers_from_passed_in_extra_headers // when ErrorResponseInfo result = handler.doHandleApiException( - singletonSortedSetOf(CUSTOM_API_ERROR), new ArrayList>(), extraHeadersArg, + singletonSortedSetOf(CUSTOM_API_ERROR), new ArrayList<>(), extraHeadersArg, new Exception(), reqMock ); @@ -418,7 +428,7 @@ protected Map> extraHeadersForResponse(TestDTO frameworkRep // when ErrorResponseInfo result = handler.doHandleApiException( - singletonSortedSetOf(CUSTOM_API_ERROR), new ArrayList>(), extraHeadersArg, + singletonSortedSetOf(CUSTOM_API_ERROR), new ArrayList<>(), extraHeadersArg, new Exception(), reqMock ); @@ -448,7 +458,8 @@ protected Map> extraHeadersForResponse(TestDTO frameworkRep }; // when - ErrorResponseInfo result = handler.doHandleApiException(singletonSortedSetOf(CUSTOM_API_ERROR), new ArrayList>(), + ErrorResponseInfo result = handler.doHandleApiException(singletonSortedSetOf(CUSTOM_API_ERROR), + new ArrayList<>(), null, new Exception(), reqMock); // then @@ -467,7 +478,7 @@ public void doHandleApiException_should_not_allow_error_uid_from_passed_in_extra // when ErrorResponseInfo result = handler.doHandleApiException( - singletonSortedSetOf(CUSTOM_API_ERROR), new ArrayList>(), extraHeadersArg, + singletonSortedSetOf(CUSTOM_API_ERROR), new ArrayList<>(), extraHeadersArg, new Exception(), reqMock ); @@ -487,6 +498,7 @@ public void doHandleApiException_should_not_allow_error_uid_from_passed_in_extra @Test public void shouldLogStackTrace_has_expected_default_behavior(int statusCode, boolean expectedResult) { // given + @SuppressWarnings("unchecked") Collection errorsCollectionMock = mock(Collection.class); Throwable originalExceptionMock = mock(Throwable.class); Throwable coreExceptionMock = mock(Throwable.class); @@ -528,6 +540,7 @@ public void shouldLogStackTrace_honors_ApiException_with_StackTraceLoggingBehavi int statusCode, StackTraceLoggingBehavior stackTraceLoggingBehavior, boolean expectedResult ) { // given + @SuppressWarnings("unchecked") Collection errorsCollectionMock = mock(Collection.class); Throwable originalExceptionMock = mock(Throwable.class); ApiException coreException = ApiException @@ -684,12 +697,7 @@ protected TestDTO prepareFrameworkRepresentation(DefaultErrorContractDTO errorCo ProjectSpecificErrorCodeRange.ALLOW_ALL_ERROR_CODES ); - private static class TestDTO { - public final DefaultErrorContractDTO erv; - - private TestDTO(DefaultErrorContractDTO erv) { - this.erv = erv; - } + private record TestDTO(DefaultErrorContractDTO erv) { } } diff --git a/backstopper-core/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerUtilsTest.java b/backstopper-core/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerUtilsTest.java index 83868f6..6e1bc32 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerUtilsTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerUtilsTest.java @@ -55,7 +55,8 @@ public class ApiExceptionHandlerUtilsTest { @Before public void setupMethod() { - MockitoAnnotations.initMocks(this); + //noinspection resource + MockitoAnnotations.openMocks(this); } @Test @@ -440,7 +441,7 @@ public void concatenateErrorCollectionShouldReturnBlankStringWhenPassedNull() { @Test public void concatenateErrorCollectionShouldReturnBlankStringWhenPassedEmptyCollection() { - String result = impl.concatenateErrorCollection(new ArrayList()); + String result = impl.concatenateErrorCollection(new ArrayList<>()); assertThat(result, is("")); } diff --git a/backstopper-core/src/test/java/com/nike/backstopper/handler/ErrorResponseInfoTest.java b/backstopper-core/src/test/java/com/nike/backstopper/handler/ErrorResponseInfoTest.java index f504502..21a2025 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/handler/ErrorResponseInfoTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/handler/ErrorResponseInfoTest.java @@ -4,7 +4,6 @@ import org.junit.Test; -import java.util.Arrays; import java.util.List; import java.util.Map; diff --git a/backstopper-core/src/test/java/com/nike/backstopper/handler/UnhandledExceptionHandlerBaseTest.java b/backstopper-core/src/test/java/com/nike/backstopper/handler/UnhandledExceptionHandlerBaseTest.java index b7163e7..72e8ae3 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/handler/UnhandledExceptionHandlerBaseTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/handler/UnhandledExceptionHandlerBaseTest.java @@ -12,14 +12,10 @@ import com.tngtech.java.junit.dataprovider.DataProvider; import com.tngtech.java.junit.dataprovider.DataProviderRunner; -import org.hamcrest.CustomTypeSafeMatcher; -import org.hamcrest.Matcher; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; import org.slf4j.Logger; import java.util.ArrayList; @@ -34,7 +30,10 @@ import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyCollection; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyMap; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; @@ -69,23 +68,20 @@ public void setupTest() { } @Test - public void handleException_should_delegate_to_ApiExceptionHandlerUtils_for_building_log_message_and_should_log_the_result() throws Exception { + public void handleException_should_delegate_to_ApiExceptionHandlerUtils_for_building_log_message_and_should_log_the_result() { // given Exception exceptionToThrow = new Exception("kaboom"); Logger loggerMock = mock(Logger.class); Glassbox.setInternalState(exHandlerSpy, "logger", loggerMock); final List sbHolder = new ArrayList<>(); - doAnswer(new Answer() { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable { - StringBuilder sb = (StringBuilder) invocation.getArguments()[0]; - sb.append(UUID.randomUUID()); - sbHolder.add(sb); - return UUID.randomUUID().toString(); - } + doAnswer(invocation -> { + StringBuilder sb = (StringBuilder) invocation.getArguments()[0]; + sb.append(UUID.randomUUID()); + sbHolder.add(sb); + return UUID.randomUUID().toString(); }).when(utilsSpy).buildErrorMessageForLogs( any(StringBuilder.class), eq(reqMock), eq(errorsExpectedToBeUsed), eq(httpStatusCodeExpectedToBeUsed), - eq(exceptionToThrow), any(List.class) + eq(exceptionToThrow), anyList() ); // when @@ -94,19 +90,19 @@ public Object answer(InvocationOnMock invocation) throws Throwable { // then verify(utilsSpy).buildErrorMessageForLogs( any(StringBuilder.class), eq(reqMock), eq(errorsExpectedToBeUsed), eq(httpStatusCodeExpectedToBeUsed), - eq(exceptionToThrow), any(List.class) + eq(exceptionToThrow), anyList() ); assertThat(sbHolder).hasSize(1); verify(loggerMock).error(sbHolder.get(0).toString(), exceptionToThrow); } @Test - public void handleException_should_delegate_to_prepareFrameworkRepresentation_for_response() throws Exception { + public void handleException_should_delegate_to_prepareFrameworkRepresentation_for_response() { // given Exception exceptionToThrow = new Exception("kaboom"); TestDTO frameworkRepresentationObj = mock(TestDTO.class); doReturn(frameworkRepresentationObj).when(exHandlerSpy).prepareFrameworkRepresentation( - any(DefaultErrorContractDTO.class), anyInt(), any(Collection.class), any(Throwable.class), any(RequestInfoForLogging.class) + any(DefaultErrorContractDTO.class), anyInt(), anyCollection(), any(Throwable.class), any(RequestInfoForLogging.class) ); // when @@ -210,21 +206,22 @@ public void handleException_delegates_to_generateLastDitchFallbackErrorResponseI if (explodeBeforeUtilsErrorIdIsGenerated) { doThrow(ohWowThisIsBadException).when(utilsSpy).buildErrorMessageForLogs( any(StringBuilder.class), eq(reqMock), eq(errorsExpectedToBeUsed), eq(httpStatusCodeExpectedToBeUsed), - eq(origEx), any(List.class) + eq(origEx), anyList() ); } else { doReturn(utilsErrorIdToReturn).when(utilsSpy).buildErrorMessageForLogs( any(StringBuilder.class), eq(reqMock), eq(errorsExpectedToBeUsed), eq(httpStatusCodeExpectedToBeUsed), - eq(origEx), any(List.class) + eq(origEx), anyList() ); doThrow(ohWowThisIsBadException).when(exHandlerSpy).prepareFrameworkRepresentation( - any(DefaultErrorContractDTO.class), anyInt(), any(Collection.class), any(Throwable.class), any(RequestInfoForLogging.class) + any(DefaultErrorContractDTO.class), anyInt(), anyCollection(), any(Throwable.class), any(RequestInfoForLogging.class) ); } + @SuppressWarnings("unchecked") ErrorResponseInfo lastDitchResponse = mock(ErrorResponseInfo.class); doReturn(lastDitchResponse).when(exHandlerSpy).generateLastDitchFallbackErrorResponseInfo( - eq(origEx), eq(reqMock), anyString(), any(Map.class) + eq(origEx), eq(reqMock), anyString(), anyMap() ); // when @@ -234,17 +231,18 @@ public void handleException_delegates_to_generateLastDitchFallbackErrorResponseI if (explodeBeforeUtilsErrorIdIsGenerated) { verify(utilsSpy).buildErrorMessageForLogs( any(StringBuilder.class), eq(reqMock), eq(errorsExpectedToBeUsed), eq(httpStatusCodeExpectedToBeUsed), - eq(origEx), any(List.class) + eq(origEx), anyList() ); } else { verify(exHandlerSpy).prepareFrameworkRepresentation( - any(DefaultErrorContractDTO.class), anyInt(), any(Collection.class), any(Throwable.class), any(RequestInfoForLogging.class) + any(DefaultErrorContractDTO.class), anyInt(), anyCollection(), any(Throwable.class), any(RequestInfoForLogging.class) ); } assertThat(result).isEqualTo(lastDitchResponse); ArgumentCaptor lastDitchErrorIdCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor lastDitchHeadersCaptor = ArgumentCaptor.forClass(Map.class); + @SuppressWarnings("unchecked") + ArgumentCaptor>> lastDitchHeadersCaptor = ArgumentCaptor.forClass(Map.class); verify(exHandlerSpy).generateLastDitchFallbackErrorResponseInfo(eq(origEx), eq(reqMock), lastDitchErrorIdCaptor.capture(), lastDitchHeadersCaptor.capture()); String lastDitchErrorId = lastDitchErrorIdCaptor.getValue(); Map> lastDitchHeaders = lastDitchHeadersCaptor.getValue(); @@ -253,27 +251,6 @@ public void handleException_delegates_to_generateLastDitchFallbackErrorResponseI assertThat(lastDitchHeaders).isEqualTo(MapBuilder.builder("error_uid", singletonList(lastDitchErrorId)).build()); } - private Matcher errorResponseViewMatches(final DefaultErrorContractDTO expectedErrorContract) { - return new CustomTypeSafeMatcher("a matching ErrorResponseView"){ - @Override - protected boolean matchesSafely(DefaultErrorContractDTO item) { - if (!(item.errors.size() == expectedErrorContract.errors.size())) - return false; - - for (int i = 0; i < item.errors.size(); i++) { - DefaultErrorDTO itemError = item.errors.get(i); - DefaultErrorDTO expectedError = item.errors.get(i); - if (!itemError.code.equals(expectedError.code)) - return false; - if (!itemError.message.equals(expectedError.message)) - return false; - } - - return item.error_id.equals(expectedErrorContract.error_id); - } - }; - } - private static class TestUnhandledExceptionHandler extends UnhandledExceptionHandlerBase { public TestUnhandledExceptionHandler(ProjectApiErrors projectApiErrors, ApiExceptionHandlerUtils utils) { @@ -299,11 +276,6 @@ protected ErrorResponseInfo generateLastDitchFallbackErrorResponseInfo( } } - private static class TestDTO { - public final DefaultErrorContractDTO erv; - - private TestDTO(DefaultErrorContractDTO erv) { - this.erv = erv; - } + private record TestDTO(DefaultErrorContractDTO erv) { } } diff --git a/backstopper-core/src/test/java/com/nike/backstopper/handler/listener/impl/ClientDataValidationErrorHandlerListenerTest.java b/backstopper-core/src/test/java/com/nike/backstopper/handler/listener/impl/ClientDataValidationErrorHandlerListenerTest.java index dc637b8..df300cc 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/handler/listener/impl/ClientDataValidationErrorHandlerListenerTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/handler/listener/impl/ClientDataValidationErrorHandlerListenerTest.java @@ -12,8 +12,6 @@ import com.nike.internal.util.Pair; import org.assertj.core.api.Assertions; -import org.assertj.core.api.ThrowableAssert; -import org.hibernate.validator.constraints.NotEmpty; import org.junit.Test; import java.lang.annotation.Annotation; @@ -24,6 +22,7 @@ import jakarta.validation.ConstraintViolation; import jakarta.validation.Path; +import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import jakarta.validation.groups.Default; import jakarta.validation.metadata.ConstraintDescriptor; @@ -64,12 +63,9 @@ public void constructor_sets_projectApiErrors_and_utils_to_passed_in_args() { @Test public void constructor_throws_IllegalArgumentException_if_passed_null_projectApiErrors() { // when - Throwable ex = Assertions.catchThrowable(new ThrowableAssert.ThrowingCallable() { - @Override - public void call() throws Throwable { - new ClientDataValidationErrorHandlerListener(null, ApiExceptionHandlerUtils.DEFAULT_IMPL); - } - }); + @SuppressWarnings("DataFlowIssue") + Throwable ex = Assertions.catchThrowable( + () -> new ClientDataValidationErrorHandlerListener(null, ApiExceptionHandlerUtils.DEFAULT_IMPL)); // then Assertions.assertThat(ex).isInstanceOf(IllegalArgumentException.class); @@ -78,12 +74,9 @@ public void call() throws Throwable { @Test public void constructor_throws_IllegalArgumentException_if_passed_null_utils() { // when - Throwable ex = Assertions.catchThrowable(new ThrowableAssert.ThrowingCallable() { - @Override - public void call() throws Throwable { - new ClientDataValidationErrorHandlerListener(mock(ProjectApiErrors.class), null); - } - }); + @SuppressWarnings("DataFlowIssue") + Throwable ex = Assertions.catchThrowable( + () -> new ClientDataValidationErrorHandlerListener(mock(ProjectApiErrors.class), null)); // then Assertions.assertThat(ex).isInstanceOf(IllegalArgumentException.class); @@ -144,7 +137,13 @@ public void shouldNotAddExtraLoggingDetailsForValidationGroupsWhenGroupsArrayIsE assertThat(extraLoggingDetails.isEmpty(), is(true)); } - private ConstraintViolation setupConstraintViolation(Class offendingObjectClass, String path, Class annotationClass, String message) { + private ConstraintViolation setupConstraintViolation( + Class offendingObjectClass, + String path, + Class annotationClass, + String message + ) { + @SuppressWarnings("unchecked") ConstraintViolation mockConstraintViolation = mock(ConstraintViolation.class); Path mockPath = mock(Path.class); @@ -199,6 +198,7 @@ public void shouldReturnExpectedErrorsForViolationsThatMapToApiErrors() { } @Test + @SuppressWarnings("unchecked") public void shouldAddExtraLoggingDetailsForClientDataValidationError() { ConstraintViolation violation1 = setupConstraintViolation(SomeValidatableObject.class, "path.to.violation1", NotNull.class, "MISSING_EXPECTED_CONTENT"); ConstraintViolation violation2 = setupConstraintViolation(Object.class, "path.to.violation2", NotEmpty.class, "TYPE_CONVERSION_ERROR"); @@ -214,24 +214,21 @@ public void shouldAddExtraLoggingDetailsForClientDataValidationError() { Pair.of("client_data_validation_failed_objects", SomeValidatableObject.class.getName() + "," + Object.class.getName()), Pair.of("validation_groups_considered", Default.class.getName() + "," + SomeValidationGroup.class.getName()), Pair.of("constraint_violation_details", - "SomeValidatableObject.path.to.violation1|jakarta.validation.constraints.NotNull|MISSING_EXPECTED_CONTENT,Object.path.to.violation2|org.hibernate.validator.constraints" + - ".NotEmpty|TYPE_CONVERSION_ERROR")) + "SomeValidatableObject.path.to.violation1|jakarta.validation.constraints.NotNull|MISSING_EXPECTED_CONTENT," + + "Object.path.to.violation2|jakarta.validation.constraints.NotEmpty|TYPE_CONVERSION_ERROR")) ); } private interface SomeValidationGroup {} - private static class SomeValidatableObject { - - @NotEmpty(message = "MISSING_EXPECTED_CONTENT") - private final String arg1; - @NotEmpty(message = "MISSING_EXPECTED_CONTENT") - private final String arg2; - - public SomeValidatableObject(String arg1, String arg2) { - this.arg1 = arg1; - this.arg2 = arg2; + private record SomeValidatableObject( + @NotEmpty(message = "MISSING_EXPECTED_CONTENT") String arg1, + @NotEmpty(message = "MISSING_EXPECTED_CONTENT") String arg2 + ) { + private SomeValidatableObject(String arg1, String arg2) { + this.arg1 = arg1; + this.arg2 = arg2; + } } - } } \ No newline at end of file diff --git a/backstopper-core/src/test/java/com/nike/backstopper/handler/listener/impl/DownstreamNetworkExceptionHandlerListenerTest.java b/backstopper-core/src/test/java/com/nike/backstopper/handler/listener/impl/DownstreamNetworkExceptionHandlerListenerTest.java index 69a1a2f..ff1a64b 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/handler/listener/impl/DownstreamNetworkExceptionHandlerListenerTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/handler/listener/impl/DownstreamNetworkExceptionHandlerListenerTest.java @@ -13,7 +13,6 @@ import com.nike.internal.util.Pair; import org.assertj.core.api.Assertions; -import org.assertj.core.api.ThrowableAssert; import org.junit.Test; import java.net.ConnectException; @@ -66,12 +65,8 @@ public void constructor_sets_projectApiErrors_to_passed_in_arg() { @Test public void constructor_throws_IllegalArgumentException_if_passed_null() { // when - Throwable ex = Assertions.catchThrowable(new ThrowableAssert.ThrowingCallable() { - @Override - public void call() throws Throwable { - new DownstreamNetworkExceptionHandlerListener(null); - } - }); + @SuppressWarnings("DataFlowIssue") + Throwable ex = Assertions.catchThrowable(() -> new DownstreamNetworkExceptionHandlerListener(null)); // then Assertions.assertThat(ex).isInstanceOf(IllegalArgumentException.class); @@ -174,7 +169,7 @@ public void processServerHttpStatusCodeExceptionShouldIncludeStatusCodeAndRawRes } @Test - public void processServerHttpStatusCodeExceptionShouldReturnOUTSIDE_DEPENDENCY_RETURNED_A_TEMPORARY_ERRORWhenItSeesTheCorrectStatusCode() throws Exception { + public void processServerHttpStatusCodeExceptionShouldReturnOUTSIDE_DEPENDENCY_RETURNED_A_TEMPORARY_ERRORWhenItSeesTheCorrectStatusCode() { HttpClientErrorExceptionForTests details = new HttpClientErrorExceptionForTests(ApiErrorConstants.HTTP_STATUS_CODE_SERVICE_UNAVAILABLE, null, null); ServerHttpStatusCodeException ex = new ServerHttpStatusCodeException(new Exception(), "FOO", details, details.statusCode, details.headers, details.rawResponseBody); @@ -186,7 +181,7 @@ public void processServerHttpStatusCodeExceptionShouldReturnOUTSIDE_DEPENDENCY_R } @Test - public void processServerHttpStatusCodeExceptionShouldReturnTOO_MANY_REQUESTSWhenItSeesTheCorrectStatusCode() throws Exception { + public void processServerHttpStatusCodeExceptionShouldReturnTOO_MANY_REQUESTSWhenItSeesTheCorrectStatusCode() { HttpClientErrorExceptionForTests details = new HttpClientErrorExceptionForTests(ApiErrorConstants.HTTP_STATUS_CODE_TOO_MANY_REQUESTS, null, null); ServerHttpStatusCodeException ex = new ServerHttpStatusCodeException(new Exception(), "FOO", details, details.statusCode, details.headers, details.rawResponseBody); diff --git a/backstopper-core/src/test/java/com/nike/backstopper/handler/listener/impl/ServersideValidationErrorHandlerListenerTest.java b/backstopper-core/src/test/java/com/nike/backstopper/handler/listener/impl/ServersideValidationErrorHandlerListenerTest.java index ce0b81f..2b37fd9 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/handler/listener/impl/ServersideValidationErrorHandlerListenerTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/handler/listener/impl/ServersideValidationErrorHandlerListenerTest.java @@ -10,8 +10,6 @@ import com.nike.internal.util.Pair; import org.assertj.core.api.Assertions; -import org.assertj.core.api.ThrowableAssert; -import org.hibernate.validator.constraints.NotEmpty; import org.junit.Test; import java.lang.annotation.Annotation; @@ -23,6 +21,7 @@ import jakarta.validation.ConstraintViolation; import jakarta.validation.Path; +import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import jakarta.validation.metadata.ConstraintDescriptor; @@ -60,12 +59,9 @@ public void constructor_sets_projectApiErrors_and_utils_to_passed_in_args() { @Test public void constructor_throws_IllegalArgumentException_if_passed_null_projectApiErrors() { // when - Throwable ex = Assertions.catchThrowable(new ThrowableAssert.ThrowingCallable() { - @Override - public void call() throws Throwable { - new ServersideValidationErrorHandlerListener(null, ApiExceptionHandlerUtils.DEFAULT_IMPL); - } - }); + @SuppressWarnings("DataFlowIssue") + Throwable ex = Assertions.catchThrowable( + () -> new ServersideValidationErrorHandlerListener(null, ApiExceptionHandlerUtils.DEFAULT_IMPL)); // then Assertions.assertThat(ex).isInstanceOf(IllegalArgumentException.class); @@ -74,12 +70,9 @@ public void call() throws Throwable { @Test public void constructor_throws_IllegalArgumentException_if_passed_null_utils() { // when - Throwable ex = Assertions.catchThrowable(new ThrowableAssert.ThrowingCallable() { - @Override - public void call() throws Throwable { - new ServersideValidationErrorHandlerListener(mock(ProjectApiErrors.class), null); - } - }); + @SuppressWarnings("DataFlowIssue") + Throwable ex = Assertions.catchThrowable( + () -> new ServersideValidationErrorHandlerListener(mock(ProjectApiErrors.class), null)); // then Assertions.assertThat(ex).isInstanceOf(IllegalArgumentException.class); @@ -113,6 +106,7 @@ public void shouldIgnoreDownstreamRequestOrResponseBodyFailedValidationException } private ConstraintViolation setupConstraintViolation(String path, Class annotationClass, String message) { + @SuppressWarnings("unchecked") ConstraintViolation mockConstraintViolation = mock(ConstraintViolation.class); Path mockPath = mock(Path.class); @@ -132,6 +126,7 @@ private ConstraintViolation setupConstraintViolation(String path, Class< } @Test + @SuppressWarnings("unchecked") public void shouldAddExtraLoggingDetailsForServersideValidationError() { ConstraintViolation violation1 = setupConstraintViolation("path.to.violation1", NotNull.class, "Violation_1_Message"); ConstraintViolation violation2 = setupConstraintViolation("path.to.violation2", NotEmpty.class, "Violation_2_Message"); @@ -139,24 +134,21 @@ public void shouldAddExtraLoggingDetailsForServersideValidationError() { List> extraLoggingDetails = new ArrayList<>(); listener.processServersideValidationError(ex, extraLoggingDetails); - extraLoggingDetails.toString(); - assertThat(extraLoggingDetails, containsInAnyOrder(Pair.of("serverside_validation_object", SomeValidatableObject.class.getName()), - Pair.of("serverside_validation_errors", - "path.to.violation1|jakarta.validation.constraints.NotNull|Violation_1_Message, path.to.violation2|org.hibernate.validator.constraints" + - ".NotEmpty|Violation_2_Message"))); + assertThat(extraLoggingDetails, containsInAnyOrder( + Pair.of("serverside_validation_object", SomeValidatableObject.class.getName()), + Pair.of("serverside_validation_errors", + "path.to.violation1|jakarta.validation.constraints.NotNull|Violation_1_Message, " + + "path.to.violation2|jakarta.validation.constraints.NotEmpty|Violation_2_Message"))); } - private static class SomeValidatableObject { - - @NotEmpty(message = "INVALID_TRUSTED_HEADERS_ERROR") - private final String arg1; - @NotEmpty(message = "INVALID_TRUSTED_HEADERS_ERROR") - private final String arg2; - - public SomeValidatableObject(String arg1, String arg2) { - this.arg1 = arg1; - this.arg2 = arg2; + private record SomeValidatableObject( + @NotEmpty(message = "INVALID_TRUSTED_HEADERS_ERROR") String arg1, + @NotEmpty(message = "INVALID_TRUSTED_HEADERS_ERROR") String arg2 + ) { + private SomeValidatableObject(String arg1, String arg2) { + this.arg1 = arg1; + this.arg2 = arg2; + } } - } } diff --git a/backstopper-core/src/test/java/com/nike/backstopper/model/DefaultErrorDTOTest.java b/backstopper-core/src/test/java/com/nike/backstopper/model/DefaultErrorDTOTest.java index fd54d64..e22cc03 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/model/DefaultErrorDTOTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/model/DefaultErrorDTOTest.java @@ -1,15 +1,14 @@ package com.nike.backstopper.model; import com.nike.backstopper.apierror.ApiError; -import com.nike.internal.util.MapBuilder; import com.tngtech.java.junit.dataprovider.DataProvider; import com.tngtech.java.junit.dataprovider.DataProviderRunner; -import org.assertj.core.api.ThrowableAssert; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.UUID; @@ -26,6 +25,7 @@ * @author Nic Munroe */ @RunWith(DataProviderRunner.class) +@SuppressWarnings("ClassEscapesDefinedScope") public class DefaultErrorDTOTest { @Test @@ -44,16 +44,18 @@ private enum MetadataArgOption { } private Map generateMetadata(MetadataArgOption metadataArgOption) { - switch(metadataArgOption) { - case NULL: - return null; - case EMPTY: - return new HashMap<>(); - case NOT_EMPTY: - return MapBuilder.builder().put("foo", UUID.randomUUID().toString()).put("bar", 42).build(); - default: - throw new IllegalArgumentException("Unhandled case: " + metadataArgOption); - } + // We need to return a modifiable map, because Map.copyOf() (which DefaultErrorDTO uses to copy metadata passed + // in) is smart enough to return the original map if it's already unmodifiable, and our tests want to prove + // that it will do a deep copy if necessary. + Map modifiableMetadataMap = new HashMap<>(); + modifiableMetadataMap.put("foo", UUID.randomUUID().toString()); + modifiableMetadataMap.put("bar", 42); + + return switch (metadataArgOption) { + case NULL -> null; + case EMPTY -> new HashMap<>(); + case NOT_EMPTY -> modifiableMetadataMap; + }; } private void verifyMetadata(final DefaultErrorDTO error, MetadataArgOption metadataArgOption, Map expectedMetadata) { @@ -66,15 +68,18 @@ private void verifyMetadata(final DefaultErrorDTO error, MetadataArgOption metad break; case NOT_EMPTY: - assertThat(error.metadata) - .isNotSameAs(expectedMetadata) - .isEqualTo(expectedMetadata); - Throwable ex = catchThrowable(new ThrowableAssert.ThrowingCallable() { - @Override - public void call() throws Throwable { - error.metadata.put("can't modify", "me"); - } - }); + assertThat(error.metadata).isEqualTo(expectedMetadata); + + if (isUnmodifiableMap(expectedMetadata)) { + // DefaultErrorDTO uses Map.copyOf() to copy the metadata, which is smart enough to return the + // original when the original is unmodifiable. + assertThat(error.metadata).isSameAs(expectedMetadata); + } else { + assertThat(error.metadata).isNotSameAs(expectedMetadata); + } + + @SuppressWarnings("DataFlowIssue") + Throwable ex = catchThrowable(() -> error.metadata.put("can't modify", "me")); assertThat(ex).isInstanceOf(UnsupportedOperationException.class); break; @@ -83,6 +88,10 @@ public void call() throws Throwable { } } + private boolean isUnmodifiableMap(Map map) { + return Collections.unmodifiableMap(map).getClass().isInstance(map) || Map.copyOf(map) == map; + } + @DataProvider(value = { "NULL", "EMPTY", diff --git a/backstopper-core/src/test/java/com/nike/backstopper/service/ClientDataValidationServiceTest.java b/backstopper-core/src/test/java/com/nike/backstopper/service/ClientDataValidationServiceTest.java index 9e81d26..b7f4109 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/service/ClientDataValidationServiceTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/service/ClientDataValidationServiceTest.java @@ -142,8 +142,10 @@ public void validateObjectsWithGroupsFailFastShouldThrowAppropriateExceptionWhen Object objToValidate2 = new Object(); Object objToValidate3 = new Object(); Class[] groups = new Class[]{Default.class, String.class}; + @SuppressWarnings("unchecked") List> obj1Violations = Collections.singletonList(mock(ConstraintViolation.class)); + @SuppressWarnings("unchecked") List> obj3Violations = Arrays.asList(mock(ConstraintViolation.class), mock(ConstraintViolation.class)); given(validatorMock.validate(objToValidate1, groups)).willReturn(new HashSet<>(obj1Violations)); given(validatorMock.validate(objToValidate2, groups)).willReturn(Collections.emptySet()); diff --git a/backstopper-core/src/test/java/com/nike/backstopper/service/FailFastServersideValidationServiceTest.java b/backstopper-core/src/test/java/com/nike/backstopper/service/FailFastServersideValidationServiceTest.java index a2dde67..e3cc941 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/service/FailFastServersideValidationServiceTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/service/FailFastServersideValidationServiceTest.java @@ -10,6 +10,7 @@ import java.util.Collections; import java.util.HashSet; +import java.util.Set; import jakarta.validation.ConstraintViolation; import jakarta.validation.Validator; @@ -31,20 +32,23 @@ public class FailFastServersideValidationServiceTest { @Before public void beforeMethod() { - MockitoAnnotations.initMocks(this); + //noinspection resource + MockitoAnnotations.openMocks(this); } @Test public void shouldNotThrowExceptionIfValidatorComesBackClean() { Object validateMe = new Object(); - when(validator.validate(validateMe)).thenReturn(new HashSet>()); + when(validator.validate(validateMe)).thenReturn(new HashSet<>()); validationService.validateObjectFailFast(validateMe); } @Test(expected = ServersideValidationError.class) public void shouldThrowExceptionIfValidatorFindsConstraintViolations() { Object validateMe = new Object(); - when(validator.validate(validateMe)).thenReturn(Collections.singleton(mock(ConstraintViolation.class))); + @SuppressWarnings("unchecked") + Set> mockReturnVal = Collections.singleton(mock(ConstraintViolation.class)); + when(validator.validate(validateMe)).thenReturn(mockReturnVal); validationService.validateObjectFailFast(validateMe); } } diff --git a/backstopper-core/src/test/java/com/nike/backstopper/service/NoOpJsr303ValidatorTest.java b/backstopper-core/src/test/java/com/nike/backstopper/service/NoOpJsr303ValidatorTest.java index 4cf7efb..b7bffad 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/service/NoOpJsr303ValidatorTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/service/NoOpJsr303ValidatorTest.java @@ -1,6 +1,5 @@ package com.nike.backstopper.service; -import org.assertj.core.api.ThrowableAssert; import org.junit.Test; import jakarta.validation.ValidationException; @@ -31,12 +30,7 @@ public void validation_methods_return_empty_sets() { @Test public void getConstraintsForClass_throws_ValidationException() { // when - Throwable ex = catchThrowable(new ThrowableAssert.ThrowingCallable() { - @Override - public void call() throws Throwable { - noOpValidator.getConstraintsForClass(FooClass.class); - } - }); + Throwable ex = catchThrowable(() -> noOpValidator.getConstraintsForClass(FooClass.class)); // then assertThat(ex).isInstanceOf(ValidationException.class); @@ -45,12 +39,7 @@ public void call() throws Throwable { @Test public void unwrap_throws_ValidationException() { // when - Throwable ex = catchThrowable(new ThrowableAssert.ThrowingCallable() { - @Override - public void call() throws Throwable { - noOpValidator.unwrap(FooClass.class); - } - }); + Throwable ex = catchThrowable(() -> noOpValidator.unwrap(FooClass.class)); // then assertThat(ex).isInstanceOf(ValidationException.class); @@ -58,6 +47,7 @@ public void call() throws Throwable { private static class FooClass { @NotNull + @SuppressWarnings("unused") public String notNullString = null; } } \ No newline at end of file diff --git a/backstopper-core/src/test/java/com/nike/backstopper/util/ApiErrorUtilTest.java b/backstopper-core/src/test/java/com/nike/backstopper/util/ApiErrorUtilTest.java index 1f7f3a5..b0dc654 100644 --- a/backstopper-core/src/test/java/com/nike/backstopper/util/ApiErrorUtilTest.java +++ b/backstopper-core/src/test/java/com/nike/backstopper/util/ApiErrorUtilTest.java @@ -19,15 +19,6 @@ @RunWith(DataProviderRunner.class) public class ApiErrorUtilTest { - @Test - public void constructor_code_coverage() { - // given - ApiErrorUtil util = new ApiErrorUtil(); - - // then - assertThat(util).isNotNull(); - } - @Test public void generate_api_error_hashcode_generates_expected_hashcodes() { // given diff --git a/backstopper-custom-validators/src/main/java/com/nike/backstopper/validation/constraints/impl/StringConvertsToClassTypeValidator.java b/backstopper-custom-validators/src/main/java/com/nike/backstopper/validation/constraints/impl/StringConvertsToClassTypeValidator.java index 3d0a15c..99d10fa 100644 --- a/backstopper-custom-validators/src/main/java/com/nike/backstopper/validation/constraints/impl/StringConvertsToClassTypeValidator.java +++ b/backstopper-custom-validators/src/main/java/com/nike/backstopper/validation/constraints/impl/StringConvertsToClassTypeValidator.java @@ -170,8 +170,8 @@ protected boolean validateAsLong(String value) { protected boolean validateAsFloat(String value) { try { - Float floatValue = Float.parseFloat(value); - return !floatValue.isInfinite() && !floatValue.isNaN(); + float floatValue = Float.parseFloat(value); + return !Float.isInfinite(floatValue) && !Float.isNaN(floatValue); } catch (Exception ex) { // Couldn't parse the given string into this primitive type, so it's not valid. @@ -181,8 +181,8 @@ protected boolean validateAsFloat(String value) { protected boolean validateAsDouble(String value) { try { - Double doubleValue = Double.parseDouble(value); - return !doubleValue.isInfinite() && !doubleValue.isNaN(); + double doubleValue = Double.parseDouble(value); + return !Double.isInfinite(doubleValue) && !Double.isNaN(doubleValue); } catch (Exception ex) { // Couldn't parse the given string into this primitive type, so it's not valid. diff --git a/backstopper-custom-validators/src/test/java/com/nike/backstopper/validation/constraints/impl/StringConvertsToClassTypeValidatorTest.java b/backstopper-custom-validators/src/test/java/com/nike/backstopper/validation/constraints/impl/StringConvertsToClassTypeValidatorTest.java index 4ab879f..9349a15 100644 --- a/backstopper-custom-validators/src/test/java/com/nike/backstopper/validation/constraints/impl/StringConvertsToClassTypeValidatorTest.java +++ b/backstopper-custom-validators/src/test/java/com/nike/backstopper/validation/constraints/impl/StringConvertsToClassTypeValidatorTest.java @@ -179,7 +179,7 @@ protected void doValidationTest(CorrectAnnotationPlacement testMe, String value, Set> validatorResult = validator.validate(testMe); assertThat(directValidationResult, is(expectedResult)); - assertThat(validatorResult.size() == 0, is(expectedResult)); + assertThat(validatorResult.isEmpty(), is(expectedResult)); } @DataProvider(value = { diff --git a/backstopper-jackson/src/test/java/com/nike/backstopper/model/util/JsonUtilWithDefaultErrorContractDTOSupportTest.java b/backstopper-jackson/src/test/java/com/nike/backstopper/model/util/JsonUtilWithDefaultErrorContractDTOSupportTest.java index f0e9147..e3ac14e 100644 --- a/backstopper-jackson/src/test/java/com/nike/backstopper/model/util/JsonUtilWithDefaultErrorContractDTOSupportTest.java +++ b/backstopper-jackson/src/test/java/com/nike/backstopper/model/util/JsonUtilWithDefaultErrorContractDTOSupportTest.java @@ -15,7 +15,6 @@ import com.tngtech.java.junit.dataprovider.DataProvider; import com.tngtech.java.junit.dataprovider.DataProviderRunner; -import org.assertj.core.api.ThrowableAssert; import org.junit.Test; import org.junit.runner.RunWith; @@ -296,17 +295,13 @@ public void writeValueAsString_does_not_blow_up_on_null_metadata() { } @Test - public void MetadataPropertyWriter_serializeAsField_still_works_for_non_Error_objects() throws Exception { + public void MetadataPropertyWriter_serializeAsField_still_works_for_non_Error_objects() { // given final MetadataPropertyWriter mpw = new MetadataPropertyWriter(mock(BeanPropertyWriter.class)); // when - Throwable ex = catchThrowable(new ThrowableAssert.ThrowingCallable() { - @Override - public void call() throws Throwable { - mpw.serializeAsField(new Object(), mock(JsonGenerator.class), mock(SerializerProvider.class)); - } - }); + Throwable ex = catchThrowable( + () -> mpw.serializeAsField(new Object(), mock(JsonGenerator.class), mock(SerializerProvider.class))); // then // We expect a NPE because mocking a base BeanPropertyWriter is incredibly difficult and not worth the effort. @@ -314,17 +309,13 @@ public void call() throws Throwable { } @Test - public void SmartErrorCodePropertyWriter_serializeAsField_still_works_for_non_Error_objects() throws Exception { + public void SmartErrorCodePropertyWriter_serializeAsField_still_works_for_non_Error_objects() { // given final SmartErrorCodePropertyWriter secpw = new SmartErrorCodePropertyWriter(mock(BeanPropertyWriter.class)); // when - Throwable ex = catchThrowable(new ThrowableAssert.ThrowingCallable() { - @Override - public void call() throws Throwable { - secpw.serializeAsField(new Object(), mock(JsonGenerator.class), mock(SerializerProvider.class)); - } - }); + Throwable ex = catchThrowable( + () -> secpw.serializeAsField(new Object(), mock(JsonGenerator.class), mock(SerializerProvider.class))); // then // We expect a NPE because mocking a base BeanPropertyWriter is incredibly difficult and not worth the effort. diff --git a/backstopper-reusable-tests-junit5/src/main/java/com/nike/backstopper/apierror/contract/jsr303convention/ReflectionBasedJsr303AnnotationTrollerBase.java b/backstopper-reusable-tests-junit5/src/main/java/com/nike/backstopper/apierror/contract/jsr303convention/ReflectionBasedJsr303AnnotationTrollerBase.java index 35498c2..5c671e4 100644 --- a/backstopper-reusable-tests-junit5/src/main/java/com/nike/backstopper/apierror/contract/jsr303convention/ReflectionBasedJsr303AnnotationTrollerBase.java +++ b/backstopper-reusable-tests-junit5/src/main/java/com/nike/backstopper/apierror/contract/jsr303convention/ReflectionBasedJsr303AnnotationTrollerBase.java @@ -313,8 +313,9 @@ public ReflectionBasedJsr303AnnotationTrollerBase(Set extraPackagesForCo constraintAnnotationClasses = new ArrayList<>(); for (Class constraintAnnotatedType : reflections.getTypesAnnotatedWith(Constraint.class, true)) { if (constraintAnnotatedType.isAnnotation()) { - //noinspection unchecked - constraintAnnotationClasses.add((Class) constraintAnnotatedType); + @SuppressWarnings("unchecked") + Class castClass = (Class) constraintAnnotatedType; + constraintAnnotationClasses.add(castClass); } } diff --git a/backstopper-reusable-tests-junit5/src/main/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyJsr303ValidationMessagesPointToApiErrorsTest.java b/backstopper-reusable-tests-junit5/src/main/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyJsr303ValidationMessagesPointToApiErrorsTest.java index ad22e0a..6c1e683 100644 --- a/backstopper-reusable-tests-junit5/src/main/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyJsr303ValidationMessagesPointToApiErrorsTest.java +++ b/backstopper-reusable-tests-junit5/src/main/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyJsr303ValidationMessagesPointToApiErrorsTest.java @@ -66,7 +66,7 @@ public void verifyThatAllValidationAnnotationsReferToApiErrors() { } } - if (invalidAnnotations.size() > 0) { + if (!invalidAnnotations.isEmpty()) { // We have at least one invalid annotation, so this unit test will need to fail. // Sort our invalid-annotations list to make it easier to fix errors for the developer looking at the error output. invalidAnnotations.sort( diff --git a/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerServletApiBaseTest.java b/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerServletApiBaseTest.java index 9135b05..0ca94cd 100644 --- a/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerServletApiBaseTest.java +++ b/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerServletApiBaseTest.java @@ -2,7 +2,6 @@ import com.nike.backstopper.apierror.ApiError; import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; -import com.nike.backstopper.handler.listener.ApiExceptionHandlerListener; import com.nike.backstopper.model.DefaultErrorContractDTO; import com.nike.internal.util.MapBuilder; diff --git a/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/UnhandledExceptionHandlerServletApiBaseTest.java b/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/UnhandledExceptionHandlerServletApiBaseTest.java index 72b6605..b71c618 100644 --- a/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/UnhandledExceptionHandlerServletApiBaseTest.java +++ b/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/UnhandledExceptionHandlerServletApiBaseTest.java @@ -56,7 +56,7 @@ protected ErrorResponseInfo generateLastDitchFallbackErrorResponseInfo(T } @Test - public void handleExceptionReturnsSuperValue() throws UnexpectedMajorExceptionHandlingError { + public void handleExceptionReturnsSuperValue() { ErrorResponseInfo expectedResponseInfo = new ErrorResponseInfo(42, null, null); doReturn(expectedResponseInfo).when(instanceSpy).handleException(any(Throwable.class), any(RequestInfoForLogging.class)); ErrorResponseInfo actualResponseInfo = instanceSpy.handleException(new Exception(), servletRequestMock, servletResponseMock); @@ -64,7 +64,7 @@ public void handleExceptionReturnsSuperValue() throws UnexpectedMajorExceptionHa } @Test - public void handleExceptionSetsHeadersAndStatusCodeOnServletResponse() throws UnexpectedMajorExceptionHandlingError { + public void handleExceptionSetsHeadersAndStatusCodeOnServletResponse() { ErrorResponseInfo expectedResponseInfo = new ErrorResponseInfo( 42, null, diff --git a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/ClientfacingErrorITest.java b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/ClientfacingErrorITest.java index cae3588..768c601 100644 --- a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/ClientfacingErrorITest.java +++ b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/ClientfacingErrorITest.java @@ -359,7 +359,7 @@ public void throwServerUnreachableException() { @RequestMapping("/throwSpecificValidationExceptions") public void throwSpecificValidationExceptions(@RequestBody List errorsToThrow) { throw ApiException.newBuilder() - .withApiErrors(new ArrayList(errorsToThrow)) + .withApiErrors(new ArrayList<>(errorsToThrow)) .withExtraResponseHeaders(Pair.of("foo1", singletonList("bar")), Pair.of("foo2", Arrays.asList("bar2.1", "bar2.2")) ) diff --git a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerTest.java b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerTest.java index 94d78b1..2f125a7 100644 --- a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerTest.java +++ b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerTest.java @@ -3,7 +3,6 @@ import com.nike.backstopper.apierror.projectspecificinfo.ProjectApiErrors; import com.nike.backstopper.handler.ApiExceptionHandlerUtils; import com.nike.backstopper.handler.UnexpectedMajorExceptionHandlingError; -import com.nike.backstopper.handler.listener.ApiExceptionHandlerListener; import com.nike.backstopper.handler.spring.listener.ApiExceptionHandlerListenerList; import org.junit.Before; diff --git a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerUtilsTest.java b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerUtilsTest.java index 1fbde5e..b2e921f 100644 --- a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerUtilsTest.java +++ b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerUtilsTest.java @@ -1,6 +1,5 @@ package com.nike.backstopper.handler.spring; -import com.nike.backstopper.apierror.ApiError; import com.nike.backstopper.apierror.testing.base.BaseSpringEnabledValidationTestCase; import com.nike.backstopper.apierror.testutil.BarebonesCoreApiErrorForTesting; import com.nike.backstopper.model.DefaultErrorContractDTO; @@ -36,7 +35,7 @@ public void setupMethod() { } @Test - public void generateModelAndViewForErrorResponseShouldGenerateModelAndViewWithErrorContractAsOnlyModelObject() throws JsonProcessingException { + public void generateModelAndViewForErrorResponseShouldGenerateModelAndViewWithErrorContractAsOnlyModelObject() { DefaultErrorContractDTO erv = new DefaultErrorContractDTO("someRequestId", Arrays.asList(BarebonesCoreApiErrorForTesting.NO_ACCEPTABLE_REPRESENTATION, BarebonesCoreApiErrorForTesting.UNSUPPORTED_MEDIA_TYPE)); diff --git a/nike-internal-util/src/main/java/com/nike/internal/util/ImmutablePair.java b/nike-internal-util/src/main/java/com/nike/internal/util/ImmutablePair.java index 739eee0..db9839a 100644 --- a/nike-internal-util/src/main/java/com/nike/internal/util/ImmutablePair.java +++ b/nike-internal-util/src/main/java/com/nike/internal/util/ImmutablePair.java @@ -42,7 +42,7 @@ public final class ImmutablePair extends Pair { * @return a pair formed from the two parameters, not null */ public static ImmutablePair of(final L left, final R right) { - return new ImmutablePair(left, right); + return new ImmutablePair<>(left, right); } /** diff --git a/nike-internal-util/src/main/java/com/nike/internal/util/Pair.java b/nike-internal-util/src/main/java/com/nike/internal/util/Pair.java index 872099f..0314fab 100644 --- a/nike-internal-util/src/main/java/com/nike/internal/util/Pair.java +++ b/nike-internal-util/src/main/java/com/nike/internal/util/Pair.java @@ -43,7 +43,7 @@ public abstract class Pair implements Map.Entry, Comparable Pair of(final L left, final R right) { - return new ImmutablePair(left, right); + return new ImmutablePair<>(left, right); } //----------------------------------------------------------------------- diff --git a/nike-internal-util/src/main/java/com/nike/internal/util/StringUtils.java b/nike-internal-util/src/main/java/com/nike/internal/util/StringUtils.java index db271c4..ba308b8 100644 --- a/nike-internal-util/src/main/java/com/nike/internal/util/StringUtils.java +++ b/nike-internal-util/src/main/java/com/nike/internal/util/StringUtils.java @@ -96,7 +96,7 @@ public static String join(Collection iterable, String delimiter, String prefi * @since 3.0 Changed signature from isEmpty(String) to isEmpty(CharSequence) */ public static boolean isEmpty(final CharSequence cs) { - return cs == null || cs.length() == 0; + return cs == null || cs.isEmpty(); } /** diff --git a/samples/sample-spring-boot3-webflux/src/test/java/com/nike/backstopper/springbootsample/componenttest/VerifyExpectedErrorsAreReturnedComponentTest.java b/samples/sample-spring-boot3-webflux/src/test/java/com/nike/backstopper/springbootsample/componenttest/VerifyExpectedErrorsAreReturnedComponentTest.java index 3a63e8f..6c8b23e 100644 --- a/samples/sample-spring-boot3-webflux/src/test/java/com/nike/backstopper/springbootsample/componenttest/VerifyExpectedErrorsAreReturnedComponentTest.java +++ b/samples/sample-spring-boot3-webflux/src/test/java/com/nike/backstopper/springbootsample/componenttest/VerifyExpectedErrorsAreReturnedComponentTest.java @@ -216,7 +216,7 @@ public void verify_flux_sample_get() throws IOException { assertThat(response.statusCode()).isEqualTo(200); List responseBody = objectMapper.readValue( - response.asString(), new TypeReference>(){} + response.asString(), new TypeReference<>() {} ); assertThat(responseBody).hasSizeGreaterThan(1); From c5c85553f64a51366bba27598cd4d802c4fb414e Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Thu, 12 Sep 2024 18:03:57 -0700 Subject: [PATCH 37/42] More warning cleanups --- .../StringConvertsToClassType.java | 3 +- .../StringConvertsToClassTypeValidator.java | 6 +- ...tringConvertsToClassTypeValidatorTest.java | 5 +- ...tilWithDefaultErrorContractDTOSupport.java | 4 +- ...ithDefaultErrorContractDTOSupportTest.java | 6 +- ...otationsAreJacksonCaseInsensitiveTest.java | 1 + ...alidationMessagesPointToApiErrorsTest.java | 18 ++---- .../ReflectionMagicWorksTest.java | 32 ---------- .../ProjectApiErrorsTestBaseTest.java | 1 + ...equestInfoForLoggingServletApiAdapter.java | 6 +- ...ApiExceptionHandlerServletApiBaseTest.java | 8 +-- ...ledExceptionHandlerServletApiBaseTest.java | 8 +-- ...stInfoForLoggingServletApiAdapterTest.java | 1 + ...andledServletContainerErrorHelperTest.java | 1 + backstopper-spring-boot3-webmvc/build.gradle | 1 + .../BackstopperSpringboot3WebMvcConfig.java | 2 +- .../SanityCheckComponentTest.java | 22 ++++--- ...ringboot3ContainerErrorControllerTest.java | 5 ++ ...BackstopperSpringWebFluxComponentTest.java | 52 ++++++++-------- ...idationErrorToApiErrorHandlerListener.java | 5 -- ...mmonFrameworkExceptionHandlerListener.java | 34 +++++------ ...ionErrorToApiErrorHandlerListenerTest.java | 5 +- ...FrameworkExceptionHandlerListenerTest.java | 60 +++++++++---------- .../spring/reusable/testutil/TestUtils.java | 4 +- 24 files changed, 128 insertions(+), 162 deletions(-) diff --git a/backstopper-custom-validators/src/main/java/com/nike/backstopper/validation/constraints/StringConvertsToClassType.java b/backstopper-custom-validators/src/main/java/com/nike/backstopper/validation/constraints/StringConvertsToClassType.java index 5efbcdb..725e615 100644 --- a/backstopper-custom-validators/src/main/java/com/nike/backstopper/validation/constraints/StringConvertsToClassType.java +++ b/backstopper-custom-validators/src/main/java/com/nike/backstopper/validation/constraints/StringConvertsToClassType.java @@ -72,10 +72,11 @@ * * @author Nic Munroe */ -@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER }) +@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER }) @Retention(RUNTIME) @Documented @Constraint(validatedBy = StringConvertsToClassTypeValidator.class) +@SuppressWarnings("unused") public @interface StringConvertsToClassType { String message() default "{StringConvertsToClassType.message}"; Class[] groups() default { }; diff --git a/backstopper-custom-validators/src/main/java/com/nike/backstopper/validation/constraints/impl/StringConvertsToClassTypeValidator.java b/backstopper-custom-validators/src/main/java/com/nike/backstopper/validation/constraints/impl/StringConvertsToClassTypeValidator.java index 99d10fa..4bddaa8 100644 --- a/backstopper-custom-validators/src/main/java/com/nike/backstopper/validation/constraints/impl/StringConvertsToClassTypeValidator.java +++ b/backstopper-custom-validators/src/main/java/com/nike/backstopper/validation/constraints/impl/StringConvertsToClassTypeValidator.java @@ -93,7 +93,9 @@ protected boolean isDesiredClassAssignableToOneOf(Class... allowedClasses) { @SuppressWarnings("unchecked") protected boolean validateAsEnum(String value) { try { - Enum.valueOf((Class) desiredClass, value); + @SuppressWarnings("rawtypes") + Class castClass = (Class) desiredClass; + Enum.valueOf(castClass, value); // No error, so it can be successfully parsed to this enum type as-is. return true; } @@ -112,7 +114,7 @@ protected boolean validateAsEnum(String value) { return false; for (Object enumValue : enumValues) { - if (enumValue instanceof Enum && ((Enum) enumValue).name().equalsIgnoreCase(value)) + if (enumValue instanceof Enum && ((Enum) enumValue).name().equalsIgnoreCase(value)) return true; } diff --git a/backstopper-custom-validators/src/test/java/com/nike/backstopper/validation/constraints/impl/StringConvertsToClassTypeValidatorTest.java b/backstopper-custom-validators/src/test/java/com/nike/backstopper/validation/constraints/impl/StringConvertsToClassTypeValidatorTest.java index 9349a15..29dc956 100644 --- a/backstopper-custom-validators/src/test/java/com/nike/backstopper/validation/constraints/impl/StringConvertsToClassTypeValidatorTest.java +++ b/backstopper-custom-validators/src/test/java/com/nike/backstopper/validation/constraints/impl/StringConvertsToClassTypeValidatorTest.java @@ -25,12 +25,15 @@ * @author Nic Munroe */ @RunWith(DataProviderRunner.class) +@SuppressWarnings("ClassEscapesDefinedScope") public class StringConvertsToClassTypeValidatorTest { + @SuppressWarnings("resource") private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); private StringConvertsToClassTypeValidator validatorImpl; + @SuppressWarnings("unused") private enum RgbColors { RED, Green, blue } @@ -153,7 +156,7 @@ private CorrectAnnotationPlacement withFooString(String fooString) { this.fooString = fooString; return this; } - private CorrectAnnotationPlacement withFooObject(String fooObject) { + private CorrectAnnotationPlacement withFooObject(@SuppressWarnings("SameParameterValue") String fooObject) { this.fooObject = fooObject; return this; } diff --git a/backstopper-jackson/src/main/java/com/nike/backstopper/model/util/JsonUtilWithDefaultErrorContractDTOSupport.java b/backstopper-jackson/src/main/java/com/nike/backstopper/model/util/JsonUtilWithDefaultErrorContractDTOSupport.java index 941ba5e..819fa18 100644 --- a/backstopper-jackson/src/main/java/com/nike/backstopper/model/util/JsonUtilWithDefaultErrorContractDTOSupport.java +++ b/backstopper-jackson/src/main/java/com/nike/backstopper/model/util/JsonUtilWithDefaultErrorContractDTOSupport.java @@ -31,13 +31,13 @@ *

You can further define a default generic error response that will be returned if there's a problem during * serialization by calling {@link #writeValueAsString(Object, ObjectMapper, String)}. The other methods use * {@link #DEFAULT_ERROR_RESPONSE_STRING} as a default. - * + *

* Created by dsand7 on 9/25/14. */ @SuppressWarnings("WeakerAccess") public class JsonUtilWithDefaultErrorContractDTOSupport { - protected JsonUtilWithDefaultErrorContractDTOSupport() { + private JsonUtilWithDefaultErrorContractDTOSupport() { // Do nothing } diff --git a/backstopper-jackson/src/test/java/com/nike/backstopper/model/util/JsonUtilWithDefaultErrorContractDTOSupportTest.java b/backstopper-jackson/src/test/java/com/nike/backstopper/model/util/JsonUtilWithDefaultErrorContractDTOSupportTest.java index e3ac14e..dec58ac 100644 --- a/backstopper-jackson/src/test/java/com/nike/backstopper/model/util/JsonUtilWithDefaultErrorContractDTOSupportTest.java +++ b/backstopper-jackson/src/test/java/com/nike/backstopper/model/util/JsonUtilWithDefaultErrorContractDTOSupportTest.java @@ -252,11 +252,6 @@ public void writeValueAsString_uses_DEFAULT_ERROR_RESPONSE_STRING_if_defaultResp verifyResultIsDefaultErrorContract(result); } - @Test - public void code_coverage_hoops() { - new JsonUtilWithDefaultErrorContractDTOSupport(); - } - @Test public void ErrorContractSerializationFactory_findPropWriter_returns_null_if_it_cannot_find_() { // given @@ -322,6 +317,7 @@ public void SmartErrorCodePropertyWriter_serializeAsField_still_works_for_non_Er assertThat(ex).isInstanceOf(NullPointerException.class); } + @SuppressWarnings("unused") public static class FooClass { public String metadata = "foo"; public String code = "42"; diff --git a/backstopper-reusable-tests-junit5/src/main/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyEnumsReferencedByStringConvertsToClassTypeJsr303AnnotationsAreJacksonCaseInsensitiveTest.java b/backstopper-reusable-tests-junit5/src/main/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyEnumsReferencedByStringConvertsToClassTypeJsr303AnnotationsAreJacksonCaseInsensitiveTest.java index 1388159..5840814 100644 --- a/backstopper-reusable-tests-junit5/src/main/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyEnumsReferencedByStringConvertsToClassTypeJsr303AnnotationsAreJacksonCaseInsensitiveTest.java +++ b/backstopper-reusable-tests-junit5/src/main/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyEnumsReferencedByStringConvertsToClassTypeJsr303AnnotationsAreJacksonCaseInsensitiveTest.java @@ -59,6 +59,7 @@ public abstract class VerifyEnumsReferencedByStringConvertsToClassTypeJsr303Anno * {@link StringConvertsToClassType} for more info on why this is required and how to do it. */ @Test + @SuppressWarnings("ExtractMethodRecommender") public void verifyEnumsReferencedByStringConvertsToClassTypeJsr303AnnotationsAreCaseInsensitive() throws IOException { ReflectionBasedJsr303AnnotationTrollerBase troller = getAnnotationTroller(); diff --git a/backstopper-reusable-tests-junit5/src/main/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyJsr303ValidationMessagesPointToApiErrorsTest.java b/backstopper-reusable-tests-junit5/src/main/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyJsr303ValidationMessagesPointToApiErrorsTest.java index 6c1e683..f9f35fd 100644 --- a/backstopper-reusable-tests-junit5/src/main/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyJsr303ValidationMessagesPointToApiErrorsTest.java +++ b/backstopper-reusable-tests-junit5/src/main/java/com/nike/backstopper/apierror/contract/jsr303convention/VerifyJsr303ValidationMessagesPointToApiErrorsTest.java @@ -108,17 +108,11 @@ public void verifyThatAllValidationAnnotationsReferToApiErrors() { /** * DTO class describing the context of an invalid annotation. */ - @SuppressWarnings("WeakerAccess") - private static class InvalidAnnotationDescription { - - public final Annotation annotation; - public final AnnotatedElement annotatedElement; - public final String message; - - private InvalidAnnotationDescription(Annotation annotation, AnnotatedElement annotatedElement, String message) { - this.annotation = annotation; - this.annotatedElement = annotatedElement; - this.message = message; - } + private record InvalidAnnotationDescription( + Annotation annotation, + AnnotatedElement annotatedElement, + String message + ) { + // Nothing here. } } diff --git a/backstopper-reusable-tests-junit5/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/ReflectionMagicWorksTest.java b/backstopper-reusable-tests-junit5/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/ReflectionMagicWorksTest.java index 26c909d..b456a06 100644 --- a/backstopper-reusable-tests-junit5/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/ReflectionMagicWorksTest.java +++ b/backstopper-reusable-tests-junit5/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/ReflectionMagicWorksTest.java @@ -34,7 +34,6 @@ import static java.lang.annotation.RetentionPolicy.RUNTIME; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertTrue; /** * Verifies the logic in {@link ReflectionBasedJsr303AnnotationTrollerBase} to make sure that its reflection magic works the way we expect it to. @@ -111,27 +110,6 @@ public void verifyThatExclusionFilterMethodIsExcludingSpecifiedMembers() { .hasSize(strictMemberCheckClassAnnotations.size() - 4); } - /** - * Another test for {@link ReflectionBasedJsr303AnnotationTrollerBase#getSubAnnotationListUsingExclusionFilters(java.util.List, java.util.List, java.util.List)} - */ - @Test - public void verifyThatExclusionFilterMethodIsExcludingBoth() { - List> annotationOptionsClassAnnotations = getSubAnnotationListForElementsOfOwnerClass(TROLLER.allConstraintAnnotationsMasterList, - DifferentValidationAnnotationOptions.class); - List> strictMemberCheckClassAnnotations = getSubAnnotationListForElementsOfOwnerClass(TROLLER.allConstraintAnnotationsMasterList, - StrictMemberCheck.class); - - List> combinedAnnotations = new ArrayList<>(annotationOptionsClassAnnotations); - combinedAnnotations.addAll(strictMemberCheckClassAnnotations); - - assertTrue(strictMemberCheckClassAnnotations.size() > 4); - assertThat( - getSubAnnotationListUsingExclusionFilters( - strictMemberCheckClassAnnotations, singletonList(DifferentValidationAnnotationOptions.class), STRICT_MEMBER_CHECK_EXCLUSIONS - ) - ).hasSize(strictMemberCheckClassAnnotations.size() - 4); - } - @SomeClassLevelJsr303Annotation.List( { @SomeClassLevelJsr303Annotation(message = "I am a class annotated with a constraint in a list 1"), @@ -234,11 +212,6 @@ public static List>> getStrictMembe } public static class SomeClassLevelJsr303AnnotationValidator implements ConstraintValidator { - @Override - public void initialize(SomeClassLevelJsr303Annotation constraintAnnotation) { - - } - @Override public boolean isValid(Object value, ConstraintValidatorContext context) { return true; @@ -264,11 +237,6 @@ public boolean isValid(Object value, ConstraintValidatorContext context) { } public static class OtherClassLevelJsr303AnnotationValidator implements ConstraintValidator { - @Override - public void initialize(OtherClassLevelJsr303Annotation constraintAnnotation) { - - } - @Override public boolean isValid(Object value, ConstraintValidatorContext context) { return true; diff --git a/backstopper-reusable-tests-junit5/src/test/java/com/nike/backstopper/apierror/projectspecificinfo/ProjectApiErrorsTestBaseTest.java b/backstopper-reusable-tests-junit5/src/test/java/com/nike/backstopper/apierror/projectspecificinfo/ProjectApiErrorsTestBaseTest.java index d8b821d..31de8d8 100644 --- a/backstopper-reusable-tests-junit5/src/test/java/com/nike/backstopper/apierror/projectspecificinfo/ProjectApiErrorsTestBaseTest.java +++ b/backstopper-reusable-tests-junit5/src/test/java/com/nike/backstopper/apierror/projectspecificinfo/ProjectApiErrorsTestBaseTest.java @@ -96,6 +96,7 @@ protected ProjectApiErrors getProjectApiErrors() { } @Test + @SuppressWarnings("ExtractMethodRecommender") public void allErrorsShouldBeCoreApiErrorsOrCoreApiErrorWrappersOrFallInProjectSpecificErrorRange_works_for_valid_cases() { // given final ApiError coreError = BarebonesCoreApiErrorForTesting.TYPE_CONVERSION_ERROR; diff --git a/backstopper-servlet-api/src/main/java/com/nike/backstopper/handler/adapter/RequestInfoForLoggingServletApiAdapter.java b/backstopper-servlet-api/src/main/java/com/nike/backstopper/handler/adapter/RequestInfoForLoggingServletApiAdapter.java index 0a420c5..02ff36e 100644 --- a/backstopper-servlet-api/src/main/java/com/nike/backstopper/handler/adapter/RequestInfoForLoggingServletApiAdapter.java +++ b/backstopper-servlet-api/src/main/java/com/nike/backstopper/handler/adapter/RequestInfoForLoggingServletApiAdapter.java @@ -125,8 +125,10 @@ protected void safeCloseCloseable(Closeable closeable) { try { closeable.close(); } catch (Throwable e) { - logger.warn("An error occurred closing a Closeable resource. closeable_classname=\"" - + closeable.getClass().getName() + "\", exception_during_close=\"" + e + "\""); + logger.warn( + "An error occurred closing a Closeable resource. closeable_classname=\"{}\", exception_during_close=\"{}\"", + closeable.getClass().getName(), e.toString() + ); } } } diff --git a/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerServletApiBaseTest.java b/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerServletApiBaseTest.java index 0ca94cd..62fe21c 100644 --- a/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerServletApiBaseTest.java +++ b/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/ApiExceptionHandlerServletApiBaseTest.java @@ -32,7 +32,7 @@ */ public class ApiExceptionHandlerServletApiBaseTest { - private ApiExceptionHandlerServletApiBase instanceSpy; + private ApiExceptionHandlerServletApiBase instanceSpy; private HttpServletRequest servletRequestMock; private HttpServletResponse servletResponseMock; @@ -53,15 +53,15 @@ protected Object prepareFrameworkRepresentation(DefaultErrorContractDTO errorCon @Test public void maybeHandleExceptionReturnsSuperValue() throws UnexpectedMajorExceptionHandlingError { - ErrorResponseInfo expectedResponseInfo = new ErrorResponseInfo(42, null, null); + ErrorResponseInfo expectedResponseInfo = new ErrorResponseInfo<>(42, null, null); doReturn(expectedResponseInfo).when(instanceSpy).maybeHandleException(any(Throwable.class), any(RequestInfoForLogging.class)); - ErrorResponseInfo actualResponseInfo = instanceSpy.maybeHandleException(new Exception(), servletRequestMock, servletResponseMock); + ErrorResponseInfo actualResponseInfo = instanceSpy.maybeHandleException(new Exception(), servletRequestMock, servletResponseMock); assertThat(actualResponseInfo, sameInstance(expectedResponseInfo)); } @Test public void maybeHandleExceptionSetsHeadersAndStatusCodeOnServletResponse() throws UnexpectedMajorExceptionHandlingError { - ErrorResponseInfo expectedResponseInfo = new ErrorResponseInfo( + ErrorResponseInfo expectedResponseInfo = new ErrorResponseInfo<>( 42, null, MapBuilder.>builder() diff --git a/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/UnhandledExceptionHandlerServletApiBaseTest.java b/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/UnhandledExceptionHandlerServletApiBaseTest.java index b71c618..777a884 100644 --- a/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/UnhandledExceptionHandlerServletApiBaseTest.java +++ b/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/UnhandledExceptionHandlerServletApiBaseTest.java @@ -30,7 +30,7 @@ */ public class UnhandledExceptionHandlerServletApiBaseTest { - private UnhandledExceptionHandlerServletApiBase instanceSpy; + private UnhandledExceptionHandlerServletApiBase instanceSpy; private HttpServletRequest servletRequestMock; private HttpServletResponse servletResponseMock; @@ -57,15 +57,15 @@ protected ErrorResponseInfo generateLastDitchFallbackErrorResponseInfo(T @Test public void handleExceptionReturnsSuperValue() { - ErrorResponseInfo expectedResponseInfo = new ErrorResponseInfo(42, null, null); + ErrorResponseInfo expectedResponseInfo = new ErrorResponseInfo<>(42, null, null); doReturn(expectedResponseInfo).when(instanceSpy).handleException(any(Throwable.class), any(RequestInfoForLogging.class)); - ErrorResponseInfo actualResponseInfo = instanceSpy.handleException(new Exception(), servletRequestMock, servletResponseMock); + ErrorResponseInfo actualResponseInfo = instanceSpy.handleException(new Exception(), servletRequestMock, servletResponseMock); assertThat(actualResponseInfo, sameInstance(expectedResponseInfo)); } @Test public void handleExceptionSetsHeadersAndStatusCodeOnServletResponse() { - ErrorResponseInfo expectedResponseInfo = new ErrorResponseInfo( + ErrorResponseInfo expectedResponseInfo = new ErrorResponseInfo<>( 42, null, MapBuilder.>builder() diff --git a/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/adapter/RequestInfoForLoggingServletApiAdapterTest.java b/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/adapter/RequestInfoForLoggingServletApiAdapterTest.java index 2b1fda9..26ccb37 100644 --- a/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/adapter/RequestInfoForLoggingServletApiAdapterTest.java +++ b/backstopper-servlet-api/src/test/java/com/nike/backstopper/handler/adapter/RequestInfoForLoggingServletApiAdapterTest.java @@ -207,6 +207,7 @@ public DelegatingServletInputStream(InputStream sourceStream) { /** * Return the underlying source stream (never null). */ + @SuppressWarnings("unused") public final InputStream getSourceStream() { return this.sourceStream; } diff --git a/backstopper-servlet-api/src/test/java/com/nike/backstopper/servletapi/UnhandledServletContainerErrorHelperTest.java b/backstopper-servlet-api/src/test/java/com/nike/backstopper/servletapi/UnhandledServletContainerErrorHelperTest.java index 0bcd538..7c1b074 100644 --- a/backstopper-servlet-api/src/test/java/com/nike/backstopper/servletapi/UnhandledServletContainerErrorHelperTest.java +++ b/backstopper-servlet-api/src/test/java/com/nike/backstopper/servletapi/UnhandledServletContainerErrorHelperTest.java @@ -36,6 +36,7 @@ * @author Nic Munroe */ @RunWith(DataProviderRunner.class) +@SuppressWarnings("ClassEscapesDefinedScope") public class UnhandledServletContainerErrorHelperTest { private UnhandledServletContainerErrorHelper helper; diff --git a/backstopper-spring-boot3-webmvc/build.gradle b/backstopper-spring-boot3-webmvc/build.gradle index 71ada5e..9f00463 100644 --- a/backstopper-spring-boot3-webmvc/build.gradle +++ b/backstopper-spring-boot3-webmvc/build.gradle @@ -11,6 +11,7 @@ dependencies { "jakarta.servlet:jakarta.servlet-api:$servletApiVersion", ) testImplementation( + "org.jetbrains:annotations:$jetbrainsAnnotationsVersion", "junit:junit:$junitVersion", "org.mockito:mockito-core:$mockitoVersion", "ch.qos.logback:logback-classic:$logbackVersion", diff --git a/backstopper-spring-boot3-webmvc/src/main/java/com/nike/backstopper/handler/springboot/config/BackstopperSpringboot3WebMvcConfig.java b/backstopper-spring-boot3-webmvc/src/main/java/com/nike/backstopper/handler/springboot/config/BackstopperSpringboot3WebMvcConfig.java index 760aac2..d1d9e61 100644 --- a/backstopper-spring-boot3-webmvc/src/main/java/com/nike/backstopper/handler/springboot/config/BackstopperSpringboot3WebMvcConfig.java +++ b/backstopper-spring-boot3-webmvc/src/main/java/com/nike/backstopper/handler/springboot/config/BackstopperSpringboot3WebMvcConfig.java @@ -24,7 +24,7 @@ * SpringApiExceptionHandler} and {@link SpringUnhandledExceptionHandler} in your application. These two exception * handlers will supersede the built-in spring exception handler chain and will translate ALL errors heading to * the caller so that they conform to the API error contract. - * + *

* This also pulls in {@link BackstopperSpringboot3ContainerErrorController} to handle exceptions that originate in the * Servlet container outside Spring proper so they can also be handled by Backstopper. See the * {@link SpringApiExceptionHandler}, {@link SpringUnhandledExceptionHandler}, and diff --git a/backstopper-spring-boot3-webmvc/src/test/java/com/nike/backstopper/handler/springboot/componenttest/SanityCheckComponentTest.java b/backstopper-spring-boot3-webmvc/src/test/java/com/nike/backstopper/handler/springboot/componenttest/SanityCheckComponentTest.java index 5b52f1d..4ec0afe 100644 --- a/backstopper-spring-boot3-webmvc/src/test/java/com/nike/backstopper/handler/springboot/componenttest/SanityCheckComponentTest.java +++ b/backstopper-spring-boot3-webmvc/src/test/java/com/nike/backstopper/handler/springboot/componenttest/SanityCheckComponentTest.java @@ -14,6 +14,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import org.jetbrains.annotations.NotNull; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; @@ -90,7 +91,7 @@ public void beforeMethod() { public void afterMethod() { } - private void verifyErrorReceived(ExtractableResponse response, ApiError expectedError) { + private void verifyErrorReceived(ExtractableResponse response, ApiError expectedError) { verifyErrorReceived(response, singleton(expectedError), expectedError.getHttpStatusCode()); } @@ -103,7 +104,7 @@ private DefaultErrorDTO findErrorMatching(DefaultErrorContractDTO errorContract, return null; } - private void verifyErrorReceived(ExtractableResponse response, Collection expectedErrors, int expectedHttpStatusCode) { + private void verifyErrorReceived(ExtractableResponse response, Collection expectedErrors, int expectedHttpStatusCode) { assertThat(response.statusCode()).isEqualTo(expectedHttpStatusCode); try { DefaultErrorContractDTO errorContract = objectMapper.readValue(response.asString(), DefaultErrorContractDTO.class); @@ -124,7 +125,7 @@ private void verifyErrorReceived(ExtractableResponse response, Collection response = given() .baseUri("http://localhost") .port(SERVER_PORT) @@ -142,7 +143,7 @@ public void verify_non_error_endpoint_responds_without_error() { @Test public void verify_ENDPOINT_ERROR_returned_if_error_endpoint_is_called() { - ExtractableResponse response = + ExtractableResponse response = given() .baseUri("http://localhost") .port(SERVER_PORT) @@ -159,7 +160,7 @@ public void verify_ENDPOINT_ERROR_returned_if_error_endpoint_is_called() { @Test public void verify_NOT_FOUND_returned_if_unknown_path_is_requested() { - ExtractableResponse response = + ExtractableResponse response = given() .baseUri("http://localhost") .port(SERVER_PORT) @@ -176,7 +177,7 @@ public void verify_NOT_FOUND_returned_if_unknown_path_is_requested() { @Test public void verify_ERROR_THROWN_IN_SERVLET_FILTER_OUTSIDE_SPRING_returned_if_servlet_filter_trigger_occurs() { - ExtractableResponse response = + ExtractableResponse response = given() .baseUri("http://localhost") .port(SERVER_PORT) @@ -195,6 +196,7 @@ public void verify_ERROR_THROWN_IN_SERVLET_FILTER_OUTSIDE_SPRING_returned_if_ser @SpringBootApplication @Configuration @Import({BackstopperSpringboot3WebMvcConfig.class, SanityCheckController.class }) + @SuppressWarnings("unused") static class SanitcyCheckComponentTestApp { @Bean public ProjectApiErrors getProjectApiErrors() { @@ -203,12 +205,13 @@ public ProjectApiErrors getProjectApiErrors() { @Bean public Validator getJsr303Validator() { + //noinspection resource return Validation.buildDefaultValidatorFactory().getValidator(); } @Bean - public FilterRegistrationBean explodingServletFilter() { - FilterRegistrationBean frb = new FilterRegistrationBean(new ExplodingFilter()); + public FilterRegistrationBean explodingServletFilter() { + FilterRegistrationBean frb = new FilterRegistrationBean<>(new ExplodingFilter()); frb.setOrder(Ordered.HIGHEST_PRECEDENCE); return frb; } @@ -217,7 +220,7 @@ public static class ExplodingFilter extends OncePerRequestFilter { @Override protected void doFilterInternal( - HttpServletRequest request, HttpServletResponse response, FilterChain filterChain + HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain ) throws ServletException, IOException { if ("true".equals(request.getHeader("throw-servlet-filter-exception"))) { throw ApiException @@ -233,6 +236,7 @@ protected void doFilterInternal( } @Controller + @SuppressWarnings("unused") static class SanityCheckController { public static final String NON_ERROR_ENDPOINT_PATH = "/nonErrorEndpoint"; public static final String ERROR_THROWING_ENDPOINT_PATH = "/throwErrorEndpoint"; diff --git a/backstopper-spring-boot3-webmvc/src/test/java/com/nike/backstopper/handler/springboot/controller/BackstopperSpringboot3ContainerErrorControllerTest.java b/backstopper-spring-boot3-webmvc/src/test/java/com/nike/backstopper/handler/springboot/controller/BackstopperSpringboot3ContainerErrorControllerTest.java index 16deff4..ab32be2 100644 --- a/backstopper-spring-boot3-webmvc/src/test/java/com/nike/backstopper/handler/springboot/controller/BackstopperSpringboot3ContainerErrorControllerTest.java +++ b/backstopper-spring-boot3-webmvc/src/test/java/com/nike/backstopper/handler/springboot/controller/BackstopperSpringboot3ContainerErrorControllerTest.java @@ -29,6 +29,7 @@ public class BackstopperSpringboot3ContainerErrorControllerTest { private UnhandledServletContainerErrorHelper unhandledContainerErrorHelperMock; private ServletRequest servletRequestMock; private ServerProperties serverPropertiesMock; + @SuppressWarnings("FieldCanBeLocal") private ErrorProperties errorPropertiesMock; private String errorPath; @@ -62,6 +63,7 @@ public void constructor_sets_fields_as_expected() { @Test public void constructor_throws_NPE_if_passed_null_ProjectApiErrors() { // when + @SuppressWarnings("DataFlowIssue") Throwable ex = catchThrowable( () -> new BackstopperSpringboot3ContainerErrorController( null, unhandledContainerErrorHelperMock, serverPropertiesMock @@ -77,6 +79,7 @@ public void constructor_throws_NPE_if_passed_null_ProjectApiErrors() { @Test public void constructor_throws_NPE_if_passed_null_UnhandledServletContainerErrorHelper() { // when + @SuppressWarnings("DataFlowIssue") Throwable ex = catchThrowable( () -> new BackstopperSpringboot3ContainerErrorController( projectApiErrorsMock, null, serverPropertiesMock @@ -92,6 +95,7 @@ public void constructor_throws_NPE_if_passed_null_UnhandledServletContainerError @Test public void constructor_throws_NPE_if_passed_null_ServerProperties() { // when + @SuppressWarnings("DataFlowIssue") Throwable ex = catchThrowable( () -> new BackstopperSpringboot3ContainerErrorController( projectApiErrorsMock, unhandledContainerErrorHelperMock, null @@ -105,6 +109,7 @@ public void constructor_throws_NPE_if_passed_null_ServerProperties() { } @Test + @SuppressWarnings("ThrowableNotThrown") public void error_method_throws_result_of_calling_UnhandledServletContainerErrorHelper() { // given BackstopperSpringboot3ContainerErrorController impl = new BackstopperSpringboot3ContainerErrorController( diff --git a/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/componenttest/BackstopperSpringWebFluxComponentTest.java b/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/componenttest/BackstopperSpringWebFluxComponentTest.java index bd38657..c5e6f67 100644 --- a/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/componenttest/BackstopperSpringWebFluxComponentTest.java +++ b/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/componenttest/BackstopperSpringWebFluxComponentTest.java @@ -181,7 +181,7 @@ public void afterMethod() { @Test public void verify_non_error_endpoint_responds_without_error() { - ExtractableResponse response = + ExtractableResponse response = given() .baseUri("http://localhost") .port(SERVER_PORT) @@ -198,7 +198,7 @@ public void verify_non_error_endpoint_responds_without_error() { @Test public void verify_ENDPOINT_ERROR_returned_if_error_endpoint_is_called() { - ExtractableResponse response = + ExtractableResponse response = given() .baseUri("http://localhost") .port(SERVER_PORT) @@ -214,7 +214,7 @@ public void verify_ENDPOINT_ERROR_returned_if_error_endpoint_is_called() { @Test public void verify_NOT_FOUND_returned_if_unknown_path_is_requested() { - ExtractableResponse response = + ExtractableResponse response = given() .baseUri("http://localhost") .port(SERVER_PORT) @@ -238,7 +238,7 @@ public void verify_NOT_FOUND_returned_if_unknown_path_is_requested() { public void verify_mono_endpoint( String specialHeader, ComponentTestProjectApiError expectedError ) { - ExtractableResponse response = + ExtractableResponse response = given() .baseUri("http://localhost") .port(SERVER_PORT) @@ -268,7 +268,7 @@ public void verify_mono_endpoint( public void verify_flux_endpoint( String specialHeader, ComponentTestProjectApiError expectedError ) { - ExtractableResponse response = + ExtractableResponse response = given() .baseUri("http://localhost") .port(SERVER_PORT) @@ -298,7 +298,7 @@ public void verify_flux_endpoint( public void verify_router_function_endpoint( String specialHeader, ComponentTestProjectApiError expectedError ) { - ExtractableResponse response = + ExtractableResponse response = given() .baseUri("http://localhost") .port(SERVER_PORT) @@ -329,7 +329,7 @@ public void verify_router_function_endpoint( public void verify_exploding_filter_behavior( String specialHeader, ComponentTestProjectApiError expectedError ) { - ExtractableResponse response = + ExtractableResponse response = given() .baseUri("http://localhost") .port(SERVER_PORT) @@ -346,7 +346,7 @@ public void verify_exploding_filter_behavior( @Test public void verify_METHOD_NOT_ALLOWED_returned_if_known_path_is_requested_with_invalid_http_method() { - ExtractableResponse response = + ExtractableResponse response = given() .baseUri("http://localhost") .port(SERVER_PORT) @@ -362,7 +362,7 @@ public void verify_METHOD_NOT_ALLOWED_returned_if_known_path_is_requested_with_i @Test public void verify_sample_get_fails_with_NO_ACCEPTABLE_REPRESENTATION_if_passed_invalid_accept_header() { - ExtractableResponse response = + ExtractableResponse response = given() .baseUri("http://localhost") .port(SERVER_PORT) @@ -382,7 +382,7 @@ public void verify_sample_post_fails_with_UNSUPPORTED_MEDIA_TYPE_if_passed_inval SampleModel requestPayload = randomizedSampleModel(); String requestPayloadAsString = objectMapper.writeValueAsString(requestPayload); - ExtractableResponse response = + ExtractableResponse response = given() .baseUri("http://localhost") .port(SERVER_PORT) @@ -400,7 +400,7 @@ public void verify_sample_post_fails_with_UNSUPPORTED_MEDIA_TYPE_if_passed_inval @Test public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert_type_for_query_param() { - ExtractableResponse response = + ExtractableResponse response = given() .baseUri("http://localhost") .port(SERVER_PORT) @@ -440,7 +440,7 @@ public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert @Test public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert_type_for_header() { - ExtractableResponse response = + ExtractableResponse response = given() .baseUri("http://localhost") .port(SERVER_PORT) @@ -480,7 +480,7 @@ public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert @Test public void verify_ResponseStatusException_with_TypeMismatchException_is_handled_generically_when_status_code_is_unexpected() { - ExtractableResponse response = + ExtractableResponse response = given() .baseUri("http://localhost") .port(SERVER_PORT) @@ -507,7 +507,7 @@ public void verify_ResponseStatusException_with_TypeMismatchException_is_handled @Test public void verify_MALFORMED_REQUEST_is_thrown_when_required_query_param_is_missing_and_error_metadata_must_be_extracted_from_ex_reason() { - ExtractableResponse response = + ExtractableResponse response = given() .baseUri("http://localhost") .port(SERVER_PORT) @@ -541,7 +541,7 @@ public void verify_MALFORMED_REQUEST_is_thrown_when_required_query_param_is_miss @Test public void verify_MALFORMED_REQUEST_is_thrown_when_required_header_is_missing_and_error_metadata_must_be_extracted_from_ex_reason() { - ExtractableResponse response = + ExtractableResponse response = given() .baseUri("http://localhost") .port(SERVER_PORT) @@ -575,7 +575,7 @@ public void verify_MALFORMED_REQUEST_is_thrown_when_required_header_is_missing_a @Test public void verify_GENERIC_SERVICE_ERROR_returned_if_ServerErrorException_is_thrown() { - ExtractableResponse response = + ExtractableResponse response = given() .baseUri("http://localhost") .port(SERVER_PORT) @@ -598,7 +598,7 @@ public void verify_GENERIC_SERVICE_ERROR_returned_if_ServerErrorException_is_thr @Test public void verify_GENERIC_SERVICE_ERROR_returned_if_ResponseStatusException_with_ConversionNotSupportedException_cause_is_thrown() { - ExtractableResponse response = + ExtractableResponse response = given() .baseUri("http://localhost") .port(SERVER_PORT) @@ -623,7 +623,7 @@ public void verify_GENERIC_SERVICE_ERROR_returned_if_ResponseStatusException_wit @Test public void verify_sample_post_fails_with_MISSING_EXPECTED_CONTENT_if_passed_empty_body() { - ExtractableResponse response = + ExtractableResponse response = given() .baseUri("http://localhost") .port(SERVER_PORT) @@ -659,7 +659,7 @@ public void verify_MALFORMED_REQUEST_is_returned_if_passed_bad_json_body_which_r badRequestPayloadAsMap.put("throw_manual_error", "not-a-boolean"); String badJsonPayloadAsString = objectMapper.writeValueAsString(badRequestPayloadAsMap); - ExtractableResponse response = + ExtractableResponse response = given() .baseUri("http://localhost") .port(SERVER_PORT) @@ -714,7 +714,7 @@ public void verify_jsr303_validation_errors( SampleModel requestPayload = new SampleModel(fooString, rangeString, rgbColorString, false); String requestPayloadAsString = objectMapper.writeValueAsString(requestPayload); - ExtractableResponse response = + ExtractableResponse response = given() .baseUri("http://localhost") .port(SERVER_PORT) @@ -756,7 +756,7 @@ else if (RGB_COLOR_CANNOT_BE_NULL.equals(apiError) || NOT_RGB_COLOR_ENUM.equals( @Test public void verify_GENERIC_SERVICE_ERROR_returned_if_unhandled_exception_is_thrown() { - ExtractableResponse response = + ExtractableResponse response = given() .baseUri("http://localhost") .port(SERVER_PORT) @@ -791,7 +791,7 @@ public void verify_GENERIC_SERVICE_ERROR_returned_if_unhandled_exception_is_thro public void verify_generic_ResponseStatusCode_exceptions_result_in_ApiError_from_project_if_status_code_is_known( int desiredStatusCode, SampleCoreApiError expectedError ) { - ExtractableResponse response = + ExtractableResponse response = given() .baseUri("http://localhost") .port(SERVER_PORT) @@ -821,7 +821,7 @@ public void verify_generic_ResponseStatusCode_exceptions_result_in_ApiError_from public void verify_generic_ResponseStatusCode_exception_with_unknown_status_code_results_in_synthetic_ApiError( int unknownStatusCode ) { - ExtractableResponse response = + ExtractableResponse response = given() .baseUri("http://localhost") .port(SERVER_PORT) @@ -854,7 +854,7 @@ public void verify_generic_ResponseStatusCode_exception_with_unknown_status_code ); } - private void verifyErrorReceived(ExtractableResponse response, ApiError expectedError) { + private void verifyErrorReceived(ExtractableResponse response, ApiError expectedError) { verifyErrorReceived(response, singleton(expectedError), expectedError.getHttpStatusCode()); } @@ -867,7 +867,7 @@ private DefaultErrorDTO findErrorMatching(DefaultErrorContractDTO errorContract, return null; } - private void verifyErrorReceived(ExtractableResponse response, Collection expectedErrors, int expectedHttpStatusCode) { + private void verifyErrorReceived(ExtractableResponse response, Collection expectedErrors, int expectedHttpStatusCode) { assertThat(response.statusCode()).isEqualTo(expectedHttpStatusCode); try { DefaultErrorContractDTO errorContract = objectMapper.readValue(response.asString(), DefaultErrorContractDTO.class); @@ -1173,7 +1173,7 @@ public String responseStatusExForSpecificStatusCodeEndpoint(ServerHttpRequest re request.getHeaders().getFirst("desired-status-code") ); throw new ResponseStatusException( - HttpStatus.resolve(desiredStatusCode), + HttpStatus.valueOf(desiredStatusCode), "Synthetic ResponseStatusException with specific desired status code: " + desiredStatusCode ); } diff --git a/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/ConventionBasedSpringValidationErrorToApiErrorHandlerListener.java b/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/ConventionBasedSpringValidationErrorToApiErrorHandlerListener.java index 5630a96..f407599 100644 --- a/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/ConventionBasedSpringValidationErrorToApiErrorHandlerListener.java +++ b/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/ConventionBasedSpringValidationErrorToApiErrorHandlerListener.java @@ -8,8 +8,6 @@ import com.nike.backstopper.handler.listener.ApiExceptionHandlerListenerResult; import org.jetbrains.annotations.NotNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.validation.Errors; import org.springframework.validation.FieldError; import org.springframework.validation.ObjectError; @@ -34,9 +32,6 @@ @SuppressWarnings("WeakerAccess") public class ConventionBasedSpringValidationErrorToApiErrorHandlerListener implements ApiExceptionHandlerListener { - private static final Logger logger = - LoggerFactory.getLogger(ConventionBasedSpringValidationErrorToApiErrorHandlerListener.class); - protected final ProjectApiErrors projectApiErrors; /** diff --git a/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListener.java b/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListener.java index 4afa3d8..e0a2d09 100644 --- a/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListener.java +++ b/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListener.java @@ -617,29 +617,23 @@ protected void addExtraDetailsForLoggingForResponseStatusException( return null; } - protected static class RequiredParamData { - public final String paramName; - public final String paramType; - public final List> extraMetadata; - - public RequiredParamData(String paramName, String paramType, List> extraMetadata) { - this.paramName = paramName; - this.paramType = paramType; - this.extraMetadata = extraMetadata; - } - + protected record RequiredParamData( + String paramName, + String paramType, + List> extraMetadata + ) { public Map getAsApiErrorMetadata() { - Map metadata = new LinkedHashMap<>(); - if (extraMetadata != null) { - for (Pair pair : extraMetadata) { - if (pair != null) { - metadata.put(pair.getKey(), pair.getValue()); + Map metadata = new LinkedHashMap<>(); + if (extraMetadata != null) { + for (Pair pair : extraMetadata) { + if (pair != null) { + metadata.put(pair.getKey(), pair.getValue()); + } } } + metadata.put("missing_param_name", paramName); + metadata.put("missing_param_type", paramType); + return metadata; } - metadata.put("missing_param_name", paramName); - metadata.put("missing_param_type", paramType); - return metadata; } - } } diff --git a/backstopper-spring-web/src/test/java/com/nike/backstopper/handler/spring/listener/impl/ConventionBasedSpringValidationErrorToApiErrorHandlerListenerTest.java b/backstopper-spring-web/src/test/java/com/nike/backstopper/handler/spring/listener/impl/ConventionBasedSpringValidationErrorToApiErrorHandlerListenerTest.java index 4ce16a5..9ac4c0c 100644 --- a/backstopper-spring-web/src/test/java/com/nike/backstopper/handler/spring/listener/impl/ConventionBasedSpringValidationErrorToApiErrorHandlerListenerTest.java +++ b/backstopper-spring-web/src/test/java/com/nike/backstopper/handler/spring/listener/impl/ConventionBasedSpringValidationErrorToApiErrorHandlerListenerTest.java @@ -67,6 +67,7 @@ public void constructor_sets_projectApiErrors_to_passed_in_arg() { @Test public void constructor_throws_IllegalArgumentException_if_passed_null() { // when + @SuppressWarnings("DataFlowIssue") Throwable ex = Assertions.catchThrowable( () -> new ConventionBasedSpringValidationErrorToApiErrorHandlerListener(null) ); @@ -149,7 +150,7 @@ public void shouldHandleException_handles_WebExchangeBindException_as_expected() ); when(bindingResult.getAllErrors()).thenReturn(errorsList); - WebExchangeBindException ex = new WebExchangeBindException(null, bindingResult); + WebExchangeBindException ex = new WebExchangeBindException(mock(MethodParameter.class), bindingResult); // when ApiExceptionHandlerListenerResult result = listener.shouldHandleException(ex); @@ -177,7 +178,7 @@ public void shouldHandleException_ignores_WebExchangeBindException_that_has_null List errorsList = (objectErrorsListIsNull) ? null : Collections.emptyList(); when(bindingResult.getAllErrors()).thenReturn(errorsList); - WebExchangeBindException ex = new WebExchangeBindException(null, bindingResult); + WebExchangeBindException ex = new WebExchangeBindException(mock(MethodParameter.class), bindingResult); // when ApiExceptionHandlerListenerResult result = listener.shouldHandleException(ex); diff --git a/backstopper-spring-web/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListenerTest.java b/backstopper-spring-web/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListenerTest.java index 9343a1e..5871a13 100644 --- a/backstopper-spring-web/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListenerTest.java +++ b/backstopper-spring-web/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListenerTest.java @@ -29,6 +29,7 @@ import org.springframework.core.ResolvableType; import org.springframework.core.codec.DecodingException; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; @@ -87,6 +88,7 @@ * @author Nic Munroe */ @RunWith(DataProviderRunner.class) +@SuppressWarnings("ClassEscapesDefinedScope") public class OneOffSpringCommonFrameworkExceptionHandlerListenerTest extends ListenerTestBase { private static final ProjectApiErrors testProjectApiErrors = ProjectApiErrorsForTesting.withProjectSpecificData(null, null); @@ -129,6 +131,7 @@ public void constructor_sets_projectApiErrors_and_utils_to_passed_in_args() { @Test public void constructor_throws_IllegalArgumentException_if_passed_null_projectApiErrors() { // when + @SuppressWarnings("DataFlowIssue") Throwable ex = Assertions.catchThrowable( () -> new OneOffListenerBasicImpl(null, ApiExceptionHandlerUtils.DEFAULT_IMPL) ); @@ -140,6 +143,7 @@ public void constructor_throws_IllegalArgumentException_if_passed_null_projectAp @Test public void constructor_throws_IllegalArgumentException_if_passed_null_utils() { // when + @SuppressWarnings("DataFlowIssue") Throwable ex = Assertions.catchThrowable( () -> new OneOffListenerBasicImpl(mock(ProjectApiErrors.class), null) ); @@ -407,23 +411,27 @@ public void shouldHandleException_returns_MALFORMED_REQUEST_for_generic_HttpMess private enum HttpMessageNotReadableExceptionScenario { KNOWN_MESSAGE_FOR_MISSING_CONTENT( - new HttpMessageNotReadableException("Required request body is missing " + UUID.randomUUID()), + new HttpMessageNotReadableException("Required request body is missing " + UUID.randomUUID(), + mock(HttpInputMessage.class)), ProjectApiErrors::getMissingExpectedContentApiError ), JSON_MAPPING_EXCEPTION_CAUSE_INDICATING_NO_CONTENT( - new HttpMessageNotReadableException("foobar", new JsonMappingException("No content to map due to end-of-input")), + new HttpMessageNotReadableException("foobar", new JsonMappingException(null, "No content to map due to end-of-input"), + mock(HttpInputMessage.class)), ProjectApiErrors::getMissingExpectedContentApiError ), CAUSE_IS_NULL( - new HttpMessageNotReadableException("foobar", null, null), + new HttpMessageNotReadableException("foobar", null, mock(HttpInputMessage.class)), ProjectApiErrors::getMalformedRequestApiError ), CAUSE_IS_NOT_JSON_MAPPING_EXCEPTION( - new HttpMessageNotReadableException("foobar", new Exception("No content to map due to end-of-input")), + new HttpMessageNotReadableException("foobar", new Exception("No content to map due to end-of-input"), + mock(HttpInputMessage.class)), ProjectApiErrors::getMalformedRequestApiError ), CAUSE_IS_JSON_MAPPING_EXCEPTION_BUT_JME_MESSAGE_IS_NOT_THE_NO_CONTENT_MESSAGE( - new HttpMessageNotReadableException("foobar", new JsonMappingException("garbagio")), + new HttpMessageNotReadableException("foobar", new JsonMappingException(null, "garbagio"), + mock(HttpInputMessage.class)), ProjectApiErrors::getMalformedRequestApiError ); @@ -643,21 +651,7 @@ public void nullSafeStringContains_works_as_expected( private void validateResponse( ApiExceptionHandlerListenerResult result, - boolean expectedShouldHandle, - Collection expectedErrors, - Pair ... expectedExtraDetailsForLogging - ) { - List> loggingDetailsList = (expectedExtraDetailsForLogging == null) - ? Collections.emptyList() - : Arrays.asList(expectedExtraDetailsForLogging); - validateResponse( - result, expectedShouldHandle, expectedErrors, loggingDetailsList - ); - } - - private void validateResponse( - ApiExceptionHandlerListenerResult result, - boolean expectedShouldHandle, + @SuppressWarnings("SameParameterValue") boolean expectedShouldHandle, Collection expectedErrors, List> expectedExtraDetailsForLogging ) { @@ -672,7 +666,7 @@ private void validateResponse( private enum TypeMismatchExceptionScenario { CONVERSION_NOT_SUPPORTED_500( - HttpStatus.resolve(500), + HttpStatus.valueOf(500), new ConversionNotSupportedException( new PropertyChangeEvent("doesNotMatter", "somePropertyName", "oldValue", "newValue"), Integer.class, @@ -686,7 +680,7 @@ private enum TypeMismatchExceptionScenario { ) ), GENERIC_TYPE_MISMATCH_EXCEPTION_400( - HttpStatus.resolve(400), + HttpStatus.valueOf(400), new TypeMismatchException( new PropertyChangeEvent("doesNotMatter", "somePropertyName", "oldValue", "newValue"), Integer.class @@ -704,19 +698,19 @@ private enum TypeMismatchExceptionScenario { ) ), UNEXPECTED_4XX_STATUS_CODE( - HttpStatus.resolve(403), + HttpStatus.valueOf(403), new TypeMismatchException("doesNotMatter", Integer.class), testProjectApiErrors.getForbiddenApiError(), Collections.emptyList() ), UNEXPECTED_5XX_STATUS_CODE( - HttpStatus.resolve(503), + HttpStatus.valueOf(503), new TypeMismatchException("doesNotMatter", Integer.class), testProjectApiErrors.getTemporaryServiceProblemApiError(), Collections.emptyList() ), UNKNOWN_4XX_STATUS_CODE( - HttpStatus.resolve(418), + HttpStatus.valueOf(418), new TypeMismatchException("doesNotMatter", Integer.class), new ApiErrorBase( "GENERIC_API_ERROR_FOR_RESPONSE_STATUS_CODE_418", @@ -727,7 +721,7 @@ private enum TypeMismatchExceptionScenario { Collections.emptyList() ), UNKNOWN_5XX_STATUS_CODE( - HttpStatus.resolve(509), + HttpStatus.valueOf(509), new TypeMismatchException("doesNotMatter", Integer.class), new ApiErrorBase( "GENERIC_API_ERROR_FOR_RESPONSE_STATUS_CODE_509", @@ -798,7 +792,7 @@ public void shouldHandleException_returns_MALFORMED_REQUEST_for_ResponseStatusEx ) { // given ResponseStatusException ex = new ResponseStatusException( - HttpStatus.resolve(statusCode), + HttpStatus.valueOf(statusCode), "Some ResponseStatusException reason", new DecodingException("Some decoding ex") ); @@ -869,7 +863,7 @@ public void shouldHandleException_returns_MALFORMED_REQUEST_for_ResponseStatusEx BarebonesCoreApiErrorForTesting expectedBaseError ) { // given - ResponseStatusException ex = new ResponseStatusException(HttpStatus.resolve(statusCode), exReasonString); + ResponseStatusException ex = new ResponseStatusException(HttpStatus.valueOf(statusCode), exReasonString); List> expectedExtraDetailsForLogging = new ArrayList<>(); ApiExceptionHandlerUtils.DEFAULT_IMPL.addBaseExceptionMessageToExtraDetailsForLogging( ex, expectedExtraDetailsForLogging @@ -908,7 +902,7 @@ public void shouldHandleException_returns_MISSING_EXPECTED_CONTENT_for_ResponseS int statusCode, String exReasonString, BarebonesCoreApiErrorForTesting expectedError ) { // given - ResponseStatusException ex = new ResponseStatusException(HttpStatus.resolve(statusCode), exReasonString); + ResponseStatusException ex = new ResponseStatusException(HttpStatus.valueOf(statusCode), exReasonString); List> expectedExtraDetailsForLogging = new ArrayList<>(); ApiExceptionHandlerUtils.DEFAULT_IMPL.addBaseExceptionMessageToExtraDetailsForLogging( ex, expectedExtraDetailsForLogging @@ -944,7 +938,7 @@ public void shouldHandleException_handles_generic_ResponseStatusException_by_ret ) { // given ResponseStatusException ex = new ResponseStatusException( - HttpStatus.resolve(desiredStatusCode), "Some ResponseStatusException reason" + HttpStatus.valueOf(desiredStatusCode), "Some ResponseStatusException reason" ); List> expectedExtraDetailsForLogging = new ArrayList<>(); ApiExceptionHandlerUtils.DEFAULT_IMPL.addBaseExceptionMessageToExtraDetailsForLogging( @@ -968,12 +962,13 @@ public void shouldHandleException_handles_generic_ResponseStatusException_by_ret "509" }) @Test + @SuppressWarnings("ExtractMethodRecommender") public void shouldHandleException_handles_generic_ResponseStatusException_by_returning_synthetic_ApiError_if_status_code_is_unknown( int desiredStatusCode ) { // given ResponseStatusException ex = new ResponseStatusException( - HttpStatus.resolve(desiredStatusCode), "Some ResponseStatusException reason" + HttpStatus.valueOf(desiredStatusCode), "Some ResponseStatusException reason" ); List> expectedExtraDetailsForLogging = new ArrayList<>(); ApiExceptionHandlerUtils.DEFAULT_IMPL.addBaseExceptionMessageToExtraDetailsForLogging( @@ -1042,7 +1037,7 @@ public void shouldHandleException_handles_MethodNotAllowedException_as_expected( assertThat(supportedMethodsLoggingDetailsValue).isPresent(); List actualLoggingDetailsMethods = supportedMethodsLoggingDetailsValue .map(s -> { - if (s.equals("")) { + if (s.isEmpty()) { return Collections.emptyList(); } return Arrays.stream(s.split(",")).map(HttpMethod::valueOf).collect(Collectors.toList()); @@ -1180,6 +1175,7 @@ public void shouldHandleException_handles_ServerWebInputException_as_expected( ); } + @SuppressWarnings("unused") public void methodWithAnnotatedParams( @RequestHeader int headerParam, @RequestParam int queryParam, diff --git a/testonly/testonly-spring-webmvc-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/testutil/TestUtils.java b/testonly/testonly-spring-webmvc-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/testutil/TestUtils.java index 2f5a2a9..0bbb2b6 100644 --- a/testonly/testonly-spring-webmvc-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/testutil/TestUtils.java +++ b/testonly/testonly-spring-webmvc-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/testutil/TestUtils.java @@ -26,7 +26,7 @@ public class TestUtils { private static final ObjectMapper objectMapper = new ObjectMapper(); - public static void verifyErrorReceived(ExtractableResponse response, ApiError expectedError) { + public static void verifyErrorReceived(ExtractableResponse response, ApiError expectedError) { verifyErrorReceived(response, singleton(expectedError), expectedError.getHttpStatusCode()); } @@ -41,7 +41,7 @@ public static DefaultErrorDTO findErrorMatching(DefaultErrorContractDTO errorCon } public static void verifyErrorReceived( - ExtractableResponse response, + ExtractableResponse response, Collection expectedErrors, int expectedHttpStatusCode ) { From e42cd2c33c068a0c6779d6c65d6e1d66675d3592 Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Fri, 13 Sep 2024 11:50:28 -0700 Subject: [PATCH 38/42] Clean up more warnings --- ...ithDefaultErrorContractDTOSupportTest.java | 18 ++--- ...nBasedJsr303AnnotationTrollerBaseTest.java | 2 +- backstopper-spring-boot3-webmvc/build.gradle | 2 + backstopper-spring-web-flux/build.gradle | 2 + .../RequestInfoForLoggingWebFluxAdapter.java | 10 ++- .../SpringWebfluxApiExceptionHandler.java | 7 +- ...SpringWebfluxApiExceptionHandlerUtils.java | 7 +- ...pringWebfluxUnhandledExceptionHandler.java | 2 +- ...questInfoForLoggingWebFluxAdapterTest.java | 20 +++++- .../SpringWebfluxApiExceptionHandlerTest.java | 17 ++++- ...ngWebfluxApiExceptionHandlerUtilsTest.java | 7 +- ...gWebfluxUnhandledExceptionHandlerTest.java | 23 +++++-- ...BackstopperSpringWebFluxComponentTest.java | 66 +++++++++++-------- ...FrameworkExceptionHandlerListenerTest.java | 3 + .../spring/SpringApiExceptionHandler.java | 15 +++-- .../SpringApiExceptionHandlerUtils.java | 2 +- .../SpringContainerErrorController.java | 4 +- .../SpringUnhandledExceptionHandler.java | 9 ++- .../config/BackstopperSpringWebMvcConfig.java | 2 +- .../BaseSpringEnabledValidationTestCase.java | 13 ++-- .../base/TestCaseValidationSpringConfig.java | 1 + .../handler/ClientfacingErrorITest.java | 19 +++++- .../spring/SpringApiExceptionHandlerTest.java | 2 + .../SpringApiExceptionHandlerUtilsTest.java | 15 ----- .../SpringContainerErrorControllerTest.java | 3 + .../SpringUnhandledExceptionHandlerTest.java | 5 +- ...FrameworkExceptionHandlerListenerTest.java | 5 +- ...lFastServersideValidationServiceITest.java | 8 +-- backstopper-spring-web/build.gradle | 1 + ...mmonFrameworkExceptionHandlerListener.java | 9 +-- build.gradle | 4 ++ nike-internal-util/build.gradle | 3 + .../com/nike/internal/util/ImmutablePair.java | 3 + .../com/nike/internal/util/MapBuilder.java | 1 + .../java/com/nike/internal/util/Pair.java | 7 +- .../com/nike/internal/util/StringUtils.java | 4 +- .../nike/internal/util/testing/Glassbox.java | 2 +- .../nike/internal/util/testing/TestUtils.java | 2 +- .../java/com/nike/internal/util/PairTest.java | 1 + .../nike/internal/util/StringUtilsTest.java | 5 -- .../internal/util/testing/GlassboxTest.java | 5 +- .../sample-spring-boot3-webflux/build.gradle | 6 +- .../SampleSpringboot3WebFluxSpringConfig.java | 11 ++-- .../controller/SampleController.java | 2 +- .../sample-spring-boot3-webmvc/build.gradle | 6 +- .../SampleSpringboot3WebMvcSpringConfig.java | 7 +- .../error/SampleProjectApiError.java | 2 +- samples/sample-spring-web-mvc/build.gradle | 8 ++- .../testonly-spring-6_0-webmvc/build.gradle | 2 + .../testonly-spring-6_1-webmvc/build.gradle | 2 + .../build.gradle | 2 + .../build.gradle | 2 + .../testutil/ExplodingServletFilter.java | 3 +- 53 files changed, 249 insertions(+), 140 deletions(-) diff --git a/backstopper-jackson/src/test/java/com/nike/backstopper/model/util/JsonUtilWithDefaultErrorContractDTOSupportTest.java b/backstopper-jackson/src/test/java/com/nike/backstopper/model/util/JsonUtilWithDefaultErrorContractDTOSupportTest.java index dec58ac..7166131 100644 --- a/backstopper-jackson/src/test/java/com/nike/backstopper/model/util/JsonUtilWithDefaultErrorContractDTOSupportTest.java +++ b/backstopper-jackson/src/test/java/com/nike/backstopper/model/util/JsonUtilWithDefaultErrorContractDTOSupportTest.java @@ -157,18 +157,12 @@ public void writeValueAsString_serializes_non_Error_objects_like_a_default_Objec assertThat(resultString).isEqualTo(defaultSerialization); } - private static class NonErrorObject { - public final String someString; - public final Integer someInt; - public final Double someDouble; - public final Map someMap; - - private NonErrorObject(String someString, Integer someInt, Double someDouble, Map someMap) { - this.someString = someString; - this.someInt = someInt; - this.someDouble = someDouble; - this.someMap = someMap; - } + private record NonErrorObject( + String someString, + Integer someInt, + Double someDouble, + Map someMap + ) { } @DataProvider(value = { diff --git a/backstopper-reusable-tests-junit5/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/ReflectionBasedJsr303AnnotationTrollerBaseTest.java b/backstopper-reusable-tests-junit5/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/ReflectionBasedJsr303AnnotationTrollerBaseTest.java index e9793f3..19a9433 100644 --- a/backstopper-reusable-tests-junit5/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/ReflectionBasedJsr303AnnotationTrollerBaseTest.java +++ b/backstopper-reusable-tests-junit5/src/test/java/com/nike/backstopper/apierror/contract/jsr303convention/ReflectionBasedJsr303AnnotationTrollerBaseTest.java @@ -116,7 +116,7 @@ public void extractMessageFromAnnotation_throws_wrapped_RuntimeException_if_anno private static class TestClass { - public String fooField = "fooField"; + public final String fooField = "fooField"; public TestClass() {} diff --git a/backstopper-spring-boot3-webmvc/build.gradle b/backstopper-spring-boot3-webmvc/build.gradle index 9f00463..d64f9e0 100644 --- a/backstopper-spring-boot3-webmvc/build.gradle +++ b/backstopper-spring-boot3-webmvc/build.gradle @@ -19,6 +19,8 @@ dependencies { "com.tngtech.java:junit-dataprovider:$junitDataproviderVersion", "jakarta.servlet:jakarta.servlet-api:$servletApiVersion", "io.rest-assured:rest-assured:$restAssuredVersion", + // Pulling in commons-codec manually to avoid vulnerability warning coming from RestAssured transitive dep. + "commons-codec:commons-codec:$commonsCodecVersion", "jakarta.servlet:jakarta.servlet-api:$servletApiVersion", "org.springframework.boot:spring-boot-starter-web:$springboot3_3Version", "org.hibernate.validator:hibernate-validator:$hibernateValidatorVersion", diff --git a/backstopper-spring-web-flux/build.gradle b/backstopper-spring-web-flux/build.gradle index 896edd2..56bea55 100644 --- a/backstopper-spring-web-flux/build.gradle +++ b/backstopper-spring-web-flux/build.gradle @@ -20,6 +20,8 @@ dependencies { "org.assertj:assertj-core:$assertJVersion", "com.tngtech.java:junit-dataprovider:$junitDataproviderVersion", "io.rest-assured:rest-assured:$restAssuredVersion", + // Pulling in commons-codec manually to avoid vulnerability warning coming from RestAssured transitive dep. + "commons-codec:commons-codec:$commonsCodecVersion", "org.springframework.boot:spring-boot-starter-webflux:$springboot3_3Version", "jakarta.validation:jakarta.validation-api:$jakartaValidationVersion", "org.hibernate.validator:hibernate-validator:$hibernateValidatorVersion", diff --git a/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/adapter/RequestInfoForLoggingWebFluxAdapter.java b/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/adapter/RequestInfoForLoggingWebFluxAdapter.java index 3177449..f5835d7 100644 --- a/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/adapter/RequestInfoForLoggingWebFluxAdapter.java +++ b/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/adapter/RequestInfoForLoggingWebFluxAdapter.java @@ -3,6 +3,7 @@ import com.nike.backstopper.handler.RequestInfoForLogging; import org.jetbrains.annotations.NotNull; +import org.springframework.http.HttpMethod; import org.springframework.web.reactive.function.server.ServerRequest; import java.net.URI; @@ -28,6 +29,7 @@ public RequestInfoForLoggingWebFluxAdapter(@NotNull ServerRequest request) { this.request = request; this.requestUri = request.uri(); + //noinspection ConstantValue if (requestUri == null) { throw new NullPointerException("request.uri() cannot be null"); } @@ -40,7 +42,12 @@ public String getRequestUri() { @Override public String getRequestHttpMethod() { - return request.methodName(); + HttpMethod method = request.method(); + //noinspection ConstantValue + if (method == null) { + return null; + } + return method.name(); } @Override @@ -56,6 +63,7 @@ public Map> getHeadersMap() { @Override public String getHeader(String headerName) { List result = request.headers().header(headerName); + //noinspection ConstantValue if (result == null || result.isEmpty()) { return null; } diff --git a/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandler.java b/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandler.java index 8138bdc..3fcfd49 100644 --- a/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandler.java +++ b/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandler.java @@ -114,7 +114,7 @@ protected Mono prepareFrameworkRepresentation( } @Override - public Mono handle(ServerWebExchange exchange, Throwable ex) { + public @NotNull Mono handle(@NotNull ServerWebExchange exchange, @NotNull Throwable ex) { ServerRequest fluxRequest = ServerRequest.create(exchange, messageReaders); RequestInfoForLogging requestInfoForLogging = new RequestInfoForLoggingWebFluxAdapter(fluxRequest); @@ -125,9 +125,8 @@ public Mono handle(ServerWebExchange exchange, Throwable ex) { ); } catch (UnexpectedMajorExceptionHandlingError ohNoException) { - logger.error( - "Unexpected major error while handling exception. " - + SpringWebfluxUnhandledExceptionHandler.class.getName() + " should handle it.", ohNoException + logger.error("Unexpected major error while handling exception. {} should handle it.", + SpringWebfluxUnhandledExceptionHandler.class.getName(), ohNoException ); return Mono.error(ex); } diff --git a/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandlerUtils.java b/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandlerUtils.java index e0b3182..4ef0af8 100644 --- a/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandlerUtils.java +++ b/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandlerUtils.java @@ -60,8 +60,7 @@ public Mono generateServerResponseForError( getErrorResponseContentType( errorContractDTO, httpStatusCode, rawFilteredApiErrors, originalException, request ) - ) - .syncBody(serializeErrorContractToString(errorContractDTO)); + ).bodyValue(serializeErrorContractToString(errorContractDTO)); } protected String serializeErrorContractToString(DefaultErrorContractDTO errorContractDTO) { @@ -76,7 +75,7 @@ protected MediaType getErrorResponseContentType( Throwable originalException, RequestInfoForLogging request ) { - // Default to simply application/json in UTF8. - return MediaType.APPLICATION_JSON_UTF8; + // Default to simply application/json. + return MediaType.APPLICATION_JSON; } } diff --git a/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxUnhandledExceptionHandler.java b/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxUnhandledExceptionHandler.java index ab35bef..7260fdd 100644 --- a/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxUnhandledExceptionHandler.java +++ b/backstopper-spring-web-flux/src/main/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxUnhandledExceptionHandler.java @@ -141,7 +141,7 @@ protected ErrorResponseInfo> generateLastDitchFallbackError } @Override - public Mono handle(ServerWebExchange exchange, Throwable ex) { + public @NotNull Mono handle(@NotNull ServerWebExchange exchange, @NotNull Throwable ex) { ServerRequest fluxRequest = ServerRequest.create(exchange, messageReaders); RequestInfoForLogging requestInfoForLogging = new RequestInfoForLoggingWebFluxAdapter(fluxRequest); diff --git a/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/adapter/RequestInfoForLoggingWebFluxAdapterTest.java b/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/adapter/RequestInfoForLoggingWebFluxAdapterTest.java index c2f8a91..494c714 100644 --- a/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/adapter/RequestInfoForLoggingWebFluxAdapterTest.java +++ b/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/adapter/RequestInfoForLoggingWebFluxAdapterTest.java @@ -10,6 +10,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.web.reactive.function.server.ServerRequest; import java.net.URI; @@ -34,10 +35,12 @@ * @author Nic Munroe */ @RunWith(DataProviderRunner.class) +@SuppressWarnings("ClassEscapesDefinedScope") public class RequestInfoForLoggingWebFluxAdapterTest { private ServerRequest requestMock; private URI requestUri; + HttpMethod httpMethod; private String httpMethodName; private ServerRequest.Headers serverRequestHeadersMock; private HttpHeaders headers; @@ -51,12 +54,14 @@ public void beforeMethod() { requestMock = mock(ServerRequest.class); requestUri = URI.create(String.format("http://localhost:8080%s?%s", REQUEST_PATH, QUERY_STRING)); httpMethodName = UUID.randomUUID().toString(); + httpMethod = mock(HttpMethod.class); headers = new HttpHeaders(); serverRequestHeadersMock = mock(ServerRequest.Headers.class); doReturn(requestUri).when(requestMock).uri(); - doReturn(httpMethodName).when(requestMock).methodName(); + doReturn(httpMethod).when(requestMock).method(); + doReturn(httpMethodName).when(httpMethod).name(); doReturn(serverRequestHeadersMock).when(requestMock).headers(); doReturn(headers).when(serverRequestHeadersMock).asHttpHeaders(); @@ -82,6 +87,7 @@ public void constructor_sets_fields_as_expected() { @Test public void constructor_throws_NullPointerException_if_passed_null_ServerRequest() { // when + @SuppressWarnings("DataFlowIssue") Throwable ex = catchThrowable(() -> new RequestInfoForLoggingWebFluxAdapter(null)); // then @@ -122,6 +128,18 @@ public void getRequestHttpMethod_returns_the_request_methodName() { assertThat(result).isEqualTo(httpMethodName); } + @Test + public void getRequestHttpMethod_returns_null_if_request_method_is_null() { + // given + doReturn(null).when(requestMock).method(); + + // when + String result = adapter.getRequestHttpMethod(); + + // then + assertThat(result).isNull(); + } + @Test public void getQueryString_returns_the_request_query_string() { // when diff --git a/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandlerTest.java b/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandlerTest.java index 50a5868..01f6d98 100644 --- a/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandlerTest.java +++ b/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandlerTest.java @@ -80,14 +80,17 @@ public class SpringWebfluxApiExceptionHandlerTest { private List viewResolvers; private ServerWebExchange serverWebExchangeMock; + @SuppressWarnings("FieldCanBeLocal") private ServerHttpRequest serverHttpRequestMock; private ServerHttpResponse serverHttpResponseMock; private HttpHeaders serverHttpResponseHeadersMock; + @SuppressWarnings("FieldCanBeLocal") private URI uri; private Throwable exMock; @Before + @SuppressWarnings("unchecked") public void beforeMethod() { projectApiErrorsMock = mock(ProjectApiErrors.class); listenerList = new SpringWebFluxApiExceptionHandlerListenerList( @@ -146,6 +149,7 @@ public void constructor_sets_fields_as_expected() { @Test public void constructor_throws_IllegalArgumentException_if_passed_null_ProjectApiErrors() { // when + @SuppressWarnings("DataFlowIssue") Throwable ex = catchThrowable( () -> new SpringWebfluxApiExceptionHandler( null, listenerList, generalUtils, springUtilsMock, viewResolversProviderMock, @@ -162,6 +166,7 @@ public void constructor_throws_IllegalArgumentException_if_passed_null_ProjectAp @Test public void constructor_throws_NullPointerException_if_passed_null_ApiExceptionHandlerListenerList() { // when + @SuppressWarnings("DataFlowIssue") Throwable ex = catchThrowable( () -> new SpringWebfluxApiExceptionHandler( projectApiErrorsMock, null, generalUtils, springUtilsMock, viewResolversProviderMock, @@ -196,6 +201,7 @@ public void constructor_throws_IllegalArgumentException_if_passed_ApiExceptionHa @Test public void constructor_throws_IllegalArgumentException_if_passed_null_ApiExceptionHandlerUtils() { // when + @SuppressWarnings("DataFlowIssue") Throwable ex = catchThrowable( () -> new SpringWebfluxApiExceptionHandler( projectApiErrorsMock, listenerList, null, springUtilsMock, viewResolversProviderMock, @@ -212,6 +218,7 @@ public void constructor_throws_IllegalArgumentException_if_passed_null_ApiExcept @Test public void constructor_throws_NullPointerException_if_passed_null_SpringWebfluxApiExceptionHandlerUtils() { // when + @SuppressWarnings("DataFlowIssue") Throwable ex = catchThrowable( () -> new SpringWebfluxApiExceptionHandler( projectApiErrorsMock, listenerList, generalUtils, null, viewResolversProviderMock, @@ -228,6 +235,7 @@ public void constructor_throws_NullPointerException_if_passed_null_SpringWebflux @Test public void constructor_throws_NullPointerException_if_passed_null_ViewResolver_ObjectProvider() { // when + @SuppressWarnings("DataFlowIssue") Throwable ex = catchThrowable( () -> new SpringWebfluxApiExceptionHandler( projectApiErrorsMock, listenerList, generalUtils, springUtilsMock, null, @@ -244,6 +252,7 @@ public void constructor_throws_NullPointerException_if_passed_null_ViewResolver_ @Test public void constructor_throws_NullPointerException_if_passed_null_ServerCodecConfigurer() { // when + @SuppressWarnings("DataFlowIssue") Throwable ex = catchThrowable( () -> new SpringWebfluxApiExceptionHandler( projectApiErrorsMock, listenerList, generalUtils, springUtilsMock, viewResolversProviderMock, @@ -260,10 +269,12 @@ public void constructor_throws_NullPointerException_if_passed_null_ServerCodecCo @Test public void prepareFrameworkRepresentation_delegates_to_SpringWebfluxApiExceptionHandlerUtils() { // given + @SuppressWarnings("unchecked") Mono expectedResult = mock(Mono.class); DefaultErrorContractDTO errorContractDTOMock = mock(DefaultErrorContractDTO.class); int httpStatusCode = 400; + @SuppressWarnings("unchecked") Collection rawFilteredApiErrors = mock(Collection.class); Throwable originalException = mock(Throwable.class); RequestInfoForLogging request = mock(RequestInfoForLogging.class); @@ -360,7 +371,7 @@ public void handle_returns_unhandled_Mono_if_maybeHandleException_returns_null() } private void verifyMonoIsErrorMono(Mono mono, Throwable expectedCause) { - Throwable ex = catchThrowable(() -> mono.block()); + Throwable ex = catchThrowable(mono::block); assertThat(ex).isNotNull(); @@ -394,8 +405,10 @@ public void processWebFluxResponse_works_as_expected() { .builder("foo", Arrays.asList("bar1", "bar2")) .put("blah", Collections.singletonList(UUID.randomUUID().toString())) .build(); + @SuppressWarnings("unchecked") + Mono monoMock = mock(Mono.class); ErrorResponseInfo> errorResponseInfo = new ErrorResponseInfo<>( - 400, mock(Mono.class), headersToAddToResponse + 400, monoMock, headersToAddToResponse ); // when diff --git a/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandlerUtilsTest.java b/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandlerUtilsTest.java index 0a3d839..35930e4 100644 --- a/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandlerUtilsTest.java +++ b/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxApiExceptionHandlerUtilsTest.java @@ -24,6 +24,7 @@ import reactor.core.publisher.Mono; +import static java.util.Objects.requireNonNull; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -51,6 +52,7 @@ public void generateServerResponseForError_works_as_expected() { // given DefaultErrorContractDTO errorContractDtoMock = mock(DefaultErrorContractDTO.class); int statusCode = 400; + @SuppressWarnings("unchecked") Collection errors = mock(Collection.class); Throwable ex = mock(Throwable.class); RequestInfoForLogging requestMock = mock(RequestInfoForLogging.class); @@ -72,7 +74,7 @@ public void generateServerResponseForError_works_as_expected() { verify(utilsSpy).getErrorResponseContentType(errorContractDtoMock, statusCode, errors, ex, requestMock); verify(utilsSpy).serializeErrorContractToString(errorContractDtoMock); ServerResponse result = resultMono.block(); - assertThat(result.statusCode().value()).isEqualTo(statusCode); + assertThat(requireNonNull(result).statusCode().value()).isEqualTo(statusCode); assertThat(result.headers().getContentType()).isEqualTo(expectedResponseContentType); // Yes this is awful. But figuring out how to spit out the ServerResponse's body to something assertable // in this test is also awful. @@ -124,6 +126,7 @@ public void getErrorResponseContentType_returns_APPLICATION_JSON_UTF8_by_default // given DefaultErrorContractDTO errorContractDtoMock = mock(DefaultErrorContractDTO.class); int statusCode = 400; + @SuppressWarnings("unchecked") Collection errors = mock(Collection.class); Throwable ex = mock(Throwable.class); RequestInfoForLogging requestMock = mock(RequestInfoForLogging.class); @@ -134,7 +137,7 @@ public void getErrorResponseContentType_returns_APPLICATION_JSON_UTF8_by_default ); // then - assertThat(result).isEqualTo(MediaType.APPLICATION_JSON_UTF8); + assertThat(result).isEqualTo(MediaType.APPLICATION_JSON); verifyNoMoreInteractions(errorContractDtoMock, errors, ex, requestMock); } diff --git a/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxUnhandledExceptionHandlerTest.java b/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxUnhandledExceptionHandlerTest.java index 20d73bf..6845465 100644 --- a/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxUnhandledExceptionHandlerTest.java +++ b/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/SpringWebfluxUnhandledExceptionHandlerTest.java @@ -42,6 +42,7 @@ import reactor.core.publisher.Mono; import static java.util.Collections.singletonList; +import static java.util.Objects.requireNonNull; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; import static org.mockito.ArgumentMatchers.any; @@ -71,14 +72,17 @@ public class SpringWebfluxUnhandledExceptionHandlerTest { private List viewResolvers; private ServerWebExchange serverWebExchangeMock; + @SuppressWarnings("FieldCanBeLocal") private ServerHttpRequest serverHttpRequestMock; private ServerHttpResponse serverHttpResponseMock; private HttpHeaders serverHttpResponseHeadersMock; + @SuppressWarnings("FieldCanBeLocal") private URI uri; private Throwable exMock; @Before + @SuppressWarnings("unchecked") public void beforeMethod() { generalUtils = ApiExceptionHandlerUtils.DEFAULT_IMPL; testProjectApiErrors = ProjectApiErrorsForTesting.withProjectSpecificData(null, null); @@ -135,6 +139,7 @@ public void constructor_sets_fields_as_expected() { @Test public void constructor_throws_IllegalArgumentException_if_passed_null_ProjectApiErrors() { // when + @SuppressWarnings("DataFlowIssue") Throwable ex = catchThrowable( () -> new SpringWebfluxUnhandledExceptionHandler( null, generalUtils, springUtilsMock, viewResolversProviderMock, @@ -151,6 +156,7 @@ public void constructor_throws_IllegalArgumentException_if_passed_null_ProjectAp @Test public void constructor_throws_IllegalArgumentException_if_passed_null_ApiExceptionHandlerUtils() { // when + @SuppressWarnings("DataFlowIssue") Throwable ex = catchThrowable( () -> new SpringWebfluxUnhandledExceptionHandler( testProjectApiErrors, null, springUtilsMock, viewResolversProviderMock, @@ -167,6 +173,7 @@ public void constructor_throws_IllegalArgumentException_if_passed_null_ApiExcept @Test public void constructor_throws_NullPointerException_if_passed_null_SpringWebfluxApiExceptionHandlerUtils() { // when + @SuppressWarnings("DataFlowIssue") Throwable ex = catchThrowable( () -> new SpringWebfluxUnhandledExceptionHandler( testProjectApiErrors, generalUtils, null, viewResolversProviderMock, @@ -183,6 +190,7 @@ public void constructor_throws_NullPointerException_if_passed_null_SpringWebflux @Test public void constructor_throws_NullPointerException_if_passed_null_ViewResolver_ObjectProvider() { // when + @SuppressWarnings("DataFlowIssue") Throwable ex = catchThrowable( () -> new SpringWebfluxUnhandledExceptionHandler( testProjectApiErrors, generalUtils, springUtilsMock, null, @@ -199,6 +207,7 @@ public void constructor_throws_NullPointerException_if_passed_null_ViewResolver_ @Test public void constructor_throws_NullPointerException_if_passed_null_ServerCodecConfigurer() { // when + @SuppressWarnings("DataFlowIssue") Throwable ex = catchThrowable( () -> new SpringWebfluxUnhandledExceptionHandler( testProjectApiErrors, generalUtils, springUtilsMock, viewResolversProviderMock, @@ -215,10 +224,12 @@ public void constructor_throws_NullPointerException_if_passed_null_ServerCodecCo @Test public void prepareFrameworkRepresentation_delegates_to_SpringWebfluxApiExceptionHandlerUtils() { // given + @SuppressWarnings("unchecked") Mono expectedResult = mock(Mono.class); DefaultErrorContractDTO errorContractDTOMock = mock(DefaultErrorContractDTO.class); int httpStatusCode = 400; + @SuppressWarnings("unchecked") Collection rawFilteredApiErrors = mock(Collection.class); Throwable originalException = mock(Throwable.class); RequestInfoForLogging request = mock(RequestInfoForLogging.class); @@ -247,7 +258,7 @@ public void generateLastDitchFallbackErrorResponseInfo_returns_expected_value() String errorId = UUID.randomUUID().toString(); Map> headersMap = MapBuilder.builder("error_uid", singletonList(errorId)).build(); - ApiError expectedGenericError = handlerSpy.singletonGenericServiceError.iterator().next(); + handlerSpy.singletonGenericServiceError.iterator().next(); int expectedHttpStatusCode = handlerSpy.genericServiceErrorHttpStatusCode; Map> expectedHeadersMap = new HashMap<>(headersMap); String expectedBodyPayload = JsonUtilWithDefaultErrorContractDTOSupport.writeValueAsString( @@ -263,8 +274,8 @@ public void generateLastDitchFallbackErrorResponseInfo_returns_expected_value() assertThat(result.httpStatusCode).isEqualTo(expectedHttpStatusCode); assertThat(result.headersToAddToResponse).isEqualTo(expectedHeadersMap); ServerResponse serverResponse = result.frameworkRepresentationObj.block(); - assertThat(serverResponse.statusCode().value()).isEqualTo(expectedHttpStatusCode); - assertThat(serverResponse.headers().getContentType()).isEqualTo(MediaType.APPLICATION_JSON_UTF8); + assertThat(requireNonNull(serverResponse).statusCode().value()).isEqualTo(expectedHttpStatusCode); + assertThat(serverResponse.headers().getContentType()).isEqualTo(MediaType.APPLICATION_JSON); // Yes this is awful. But figuring out how to spit out the ServerResponse's body to something assertable // in this test is also awful. assertThat(Glassbox.getInternalState(serverResponse, "entity")).isEqualTo(expectedBodyPayload); @@ -329,7 +340,7 @@ public void handle_calls_handleException_but_returns_unhandled_Mono_if_response_ } private void verifyMonoIsErrorMono(Mono mono, Throwable expectedCause) { - Throwable ex = catchThrowable(() -> mono.block()); + Throwable ex = catchThrowable(mono::block); assertThat(ex).isNotNull(); @@ -348,8 +359,10 @@ public void processWebFluxResponse_works_as_expected() { .builder("foo", Arrays.asList("bar1", "bar2")) .put("blah", Collections.singletonList(UUID.randomUUID().toString())) .build(); + @SuppressWarnings("unchecked") + Mono mockResponse = mock(Mono.class); ErrorResponseInfo> errorResponseInfo = new ErrorResponseInfo<>( - 400, mock(Mono.class), headersToAddToResponse + 400, mockResponse, headersToAddToResponse ); // when diff --git a/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/componenttest/BackstopperSpringWebFluxComponentTest.java b/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/componenttest/BackstopperSpringWebFluxComponentTest.java index c5e6f67..a39cfe5 100644 --- a/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/componenttest/BackstopperSpringWebFluxComponentTest.java +++ b/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/componenttest/BackstopperSpringWebFluxComponentTest.java @@ -27,6 +27,7 @@ import com.tngtech.java.junit.dataprovider.DataProvider; import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import org.jetbrains.annotations.NotNull; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; @@ -128,6 +129,7 @@ import static com.nike.internal.util.testing.TestUtils.findFreePort; import static io.restassured.RestAssured.given; import static java.util.Collections.singleton; +import static java.util.Objects.requireNonNull; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.web.reactive.function.server.RequestPredicates.GET; @@ -137,6 +139,7 @@ * @author Nic Munroe */ @RunWith(DataProviderRunner.class) +@SuppressWarnings("ClassEscapesDefinedScope") public class BackstopperSpringWebFluxComponentTest { private static final int SERVER_PORT = findFreePort(); @@ -177,6 +180,7 @@ public void beforeMethod() { @After public void afterMethod() { + // Do nothing. } @Test @@ -431,10 +435,10 @@ public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert verifyHandlingResult( expectedApiError, Pair.of("exception_message", quotesToApostrophes(ex.getMessage())), - Pair.of("method_parameter", ex.getMethodParameter().toString()), + Pair.of("method_parameter", requireNonNull(ex.getMethodParameter()).toString()), Pair.of("bad_property_name", tme.getPropertyName()), - Pair.of("bad_property_value", tme.getValue().toString()), - Pair.of("required_type", tme.getRequiredType().toString()) + Pair.of("bad_property_value", requireNonNull(tme.getValue()).toString()), + Pair.of("required_type", requireNonNull(tme.getRequiredType()).toString()) ); } @@ -471,10 +475,10 @@ public void verify_TYPE_CONVERSION_ERROR_is_thrown_when_framework_cannot_convert verifyHandlingResult( expectedApiError, Pair.of("exception_message", quotesToApostrophes(ex.getMessage())), - Pair.of("method_parameter", ex.getMethodParameter().toString()), + Pair.of("method_parameter", requireNonNull(ex.getMethodParameter()).toString()), Pair.of("bad_property_name", tme.getPropertyName()), - Pair.of("bad_property_value", tme.getValue().toString()), - Pair.of("required_type", tme.getRequiredType().toString()) + Pair.of("bad_property_value", requireNonNull(tme.getValue()).toString()), + Pair.of("required_type", requireNonNull(tme.getRequiredType()).toString()) ); } @@ -498,7 +502,7 @@ public void verify_ResponseStatusException_with_TypeMismatchException_is_handled ResponseStatusException ex = verifyResponseStatusExceptionSeenByBackstopper( ResponseStatusException.class, 403 ); - TypeMismatchException tme = verifyExceptionHasCauseOfType(ex, TypeMismatchException.class); + verifyExceptionHasCauseOfType(ex, TypeMismatchException.class); verifyHandlingResult( SampleCoreApiError.FORBIDDEN, Pair.of("exception_message", quotesToApostrophes(ex.getMessage())) @@ -532,7 +536,7 @@ public void verify_MALFORMED_REQUEST_is_thrown_when_required_query_param_is_miss verifyHandlingResult( expectedApiError, Pair.of("exception_message", quotesToApostrophes(ex.getMessage())), - Pair.of("method_parameter", ex.getMethodParameter().toString()) + Pair.of("method_parameter", requireNonNull(ex.getMethodParameter()).toString()) ); // Verify no cause, leaving the exception `reason` as the only way we could have gotten the metadata. assertThat(ex).hasNoCause(); @@ -566,7 +570,7 @@ public void verify_MALFORMED_REQUEST_is_thrown_when_required_header_is_missing_a verifyHandlingResult( expectedApiError, Pair.of("exception_message", quotesToApostrophes(ex.getMessage())), - Pair.of("method_parameter", ex.getMethodParameter().toString()) + Pair.of("method_parameter", requireNonNull(ex.getMethodParameter()).toString()) ); // Verify no cause, leaving the exception `reason` as the only way we could have gotten the metadata. assertThat(ex).hasNoCause(); @@ -591,8 +595,8 @@ public void verify_GENERIC_SERVICE_ERROR_returned_if_ServerErrorException_is_thr verifyHandlingResult( SampleCoreApiError.GENERIC_SERVICE_ERROR, Pair.of("exception_message", quotesToApostrophes(ex.getMessage())), - Pair.of("method_parameter", ex.getMethodParameter().toString()), - Pair.of("handler_method", ex.getHandlerMethod().toString()) + Pair.of("method_parameter", requireNonNull(ex.getMethodParameter()).toString()), + Pair.of("handler_method", requireNonNull(ex.getHandlerMethod()).toString()) ); } @@ -616,8 +620,8 @@ public void verify_GENERIC_SERVICE_ERROR_returned_if_ResponseStatusException_wit SampleCoreApiError.GENERIC_SERVICE_ERROR, Pair.of("exception_message", quotesToApostrophes(ex.getMessage())), Pair.of("bad_property_name", cnse.getPropertyName()), - Pair.of("bad_property_value", cnse.getValue().toString()), - Pair.of("required_type", cnse.getRequiredType().toString()) + Pair.of("bad_property_value", requireNonNull(cnse.getValue()).toString()), + Pair.of("required_type", requireNonNull(cnse.getRequiredType()).toString()) ); } @@ -643,7 +647,7 @@ public void verify_sample_post_fails_with_MISSING_EXPECTED_CONTENT_if_passed_emp verifyHandlingResult( SampleCoreApiError.MISSING_EXPECTED_CONTENT, Pair.of("exception_message", quotesToApostrophes(ex.getMessage())), - Pair.of("method_parameter", ex.getMethodParameter().toString()) + Pair.of("method_parameter", requireNonNull(ex.getMethodParameter()).toString()) ); // Verify DecodingException as the cause, leaving the exception `reason` as the only way we could have determined this case. assertThat(ex).hasCauseInstanceOf(DecodingException.class); @@ -680,7 +684,7 @@ public void verify_MALFORMED_REQUEST_is_returned_if_passed_bad_json_body_which_r verifyHandlingResult( SampleCoreApiError.MALFORMED_REQUEST, Pair.of("exception_message", quotesToApostrophes(ex.getMessage())), - Pair.of("method_parameter", ex.getMethodParameter().toString()) + Pair.of("method_parameter", requireNonNull(ex.getMethodParameter()).toString()) ); } @@ -688,7 +692,7 @@ private SampleModel randomizedSampleModel() { return new SampleModel(UUID.randomUUID().toString(), String.valueOf(nextRangeInt(0, 42)), nextRandomColor().name(), false); } - static int nextRangeInt(int lowerBound, int upperBound) { + static int nextRangeInt(@SuppressWarnings("SameParameterValue") int lowerBound, int upperBound) { return (int)Math.round(Math.random() * upperBound) + lowerBound; } @@ -818,6 +822,7 @@ public void verify_generic_ResponseStatusCode_exceptions_result_in_ApiError_from "506" }, splitBy = "\\|") @Test + @SuppressWarnings("ExtractMethodRecommender") public void verify_generic_ResponseStatusCode_exception_with_unknown_status_code_results_in_synthetic_ApiError( int unknownStatusCode ) { @@ -893,7 +898,9 @@ private T verifyExceptionSeenByBackstopper(Class expect assertThat(exceptionSeenByUnhandledBackstopperHandler).isNull(); - return (T) ex; + @SuppressWarnings("unchecked") + T asT = (T) ex; + return asT; } private T verifyResponseStatusExceptionSeenByBackstopper( @@ -910,7 +917,9 @@ private T verifyResponseStatusExceptionSeenB private T verifyExceptionHasCauseOfType(Throwable origEx, Class expectedCauseType) { assertThat(origEx.getCause()).isInstanceOf(expectedCauseType); - return (T) origEx.getCause(); + @SuppressWarnings("unchecked") + T asT = (T) origEx.getCause(); + return asT; } private String quotesToApostrophes(String str) { @@ -918,8 +927,8 @@ private String quotesToApostrophes(String str) { } @SafeVarargs - private final void verifyHandlingResult( - ApiError expectedApiError, Pair ... expectedExtraLoggingPairs + private void verifyHandlingResult( + ApiError expectedApiError, Pair... expectedExtraLoggingPairs ) { ApiExceptionHandlerListenerResult result = normalBackstopperHandlingResult; assertThat(result.shouldHandleResponse).isTrue(); @@ -930,6 +939,7 @@ private final void verifyHandlingResult( @SpringBootApplication @Configuration @Import({BackstopperSpringWebFluxConfig.class, ComponentTestController.class }) + @SuppressWarnings("unused") static class ComponentTestWebFluxApp { @Bean public ProjectApiErrors getProjectApiErrors() { @@ -938,6 +948,7 @@ public ProjectApiErrors getProjectApiErrors() { @Bean public Validator getJsr303Validator() { + //noinspection resource return Validation.buildDefaultValidatorFactory().getValidator(); } @@ -992,7 +1003,7 @@ public SpringWebfluxUnhandledExceptionHandler springWebfluxUnhandledExceptionHan projectApiErrors, generalUtils, springUtils, viewResolversProvider, serverCodecConfigurer ) { @Override - public Mono handle(ServerWebExchange exchange, Throwable ex) { + public @NotNull Mono handle(@NotNull ServerWebExchange exchange, @NotNull Throwable ex) { exceptionSeenByUnhandledBackstopperHandler = ex; return super.handle(exchange, ex); } @@ -1001,6 +1012,7 @@ public Mono handle(ServerWebExchange exchange, Throwable ex) { } @Controller + @SuppressWarnings("unused") static class ComponentTestController { static final String NON_ERROR_ENDPOINT_PATH = "/nonErrorEndpoint"; static final String ERROR_THROWING_ENDPOINT_PATH = "/throwErrorEndpoint"; @@ -1114,7 +1126,7 @@ Mono routerFunctionEndpoint(ServerRequest request) { ); } - return ServerResponse.ok().syncBody(ROUTER_FUNCTION_ENDPOINT_RESPONSE_PAYLOAD); + return ServerResponse.ok().bodyValue(ROUTER_FUNCTION_ENDPOINT_RESPONSE_PAYLOAD); } @GetMapping(path = INT_QUERY_PARAM_REQUIRED_ENDPOINT) @@ -1170,7 +1182,7 @@ public String typeMismatchWithUnexpectedStatusEndpoint() { @ResponseBody public String responseStatusExForSpecificStatusCodeEndpoint(ServerHttpRequest request) { int desiredStatusCode = Integer.parseInt( - request.getHeaders().getFirst("desired-status-code") + requireNonNull(request.getHeaders().getFirst("desired-status-code")) ); throw new ResponseStatusException( HttpStatus.valueOf(desiredStatusCode), @@ -1288,8 +1300,8 @@ protected ProjectSpecificErrorCodeRange getProjectSpecificErrorCodeRange() { public static class ExplodingWebFilter implements WebFilter { @Override - public Mono filter( - ServerWebExchange exchange, WebFilterChain chain + public @NotNull Mono filter( + ServerWebExchange exchange, @NotNull WebFilterChain chain ) { HttpHeaders httpHeaders = exchange.getRequest().getHeaders(); @@ -1319,9 +1331,9 @@ public static class ExplodingHandlerFilterFunction implements HandlerFilterFunction { @Override - public Mono filter( + public @NotNull Mono filter( ServerRequest serverRequest, - HandlerFunction handlerFunction + @NotNull HandlerFunction handlerFunction ) { HttpHeaders httpHeaders = serverRequest.headers().asHttpHeaders(); diff --git a/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/listener/impl/OneOffSpringWebFluxFrameworkExceptionHandlerListenerTest.java b/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/listener/impl/OneOffSpringWebFluxFrameworkExceptionHandlerListenerTest.java index 8f6ab86..187f889 100644 --- a/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/listener/impl/OneOffSpringWebFluxFrameworkExceptionHandlerListenerTest.java +++ b/backstopper-spring-web-flux/src/test/java/com/nike/backstopper/handler/spring/webflux/listener/impl/OneOffSpringWebFluxFrameworkExceptionHandlerListenerTest.java @@ -57,6 +57,7 @@ public void constructor_sets_projectApiErrors_and_utils_to_passed_in_args() { @Test public void constructor_throws_IllegalArgumentException_if_passed_null_projectApiErrors() { // when + @SuppressWarnings("DataFlowIssue") Throwable ex = Assertions.catchThrowable( () -> new OneOffSpringWebFluxFrameworkExceptionHandlerListener(null, ApiExceptionHandlerUtils.DEFAULT_IMPL) ); @@ -68,6 +69,7 @@ public void constructor_throws_IllegalArgumentException_if_passed_null_projectAp @Test public void constructor_throws_IllegalArgumentException_if_passed_null_utils() { // when + @SuppressWarnings("DataFlowIssue") Throwable ex = Assertions.catchThrowable( () -> new OneOffSpringWebFluxFrameworkExceptionHandlerListener(mock(ProjectApiErrors.class), null) ); @@ -76,6 +78,7 @@ public void constructor_throws_IllegalArgumentException_if_passed_null_utils() { assertThat(ex).isInstanceOf(IllegalArgumentException.class); } + @SafeVarargs private void validateResponse( ApiExceptionHandlerListenerResult result, boolean expectedShouldHandle, diff --git a/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandler.java b/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandler.java index daf51a5..c502d1d 100644 --- a/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandler.java +++ b/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandler.java @@ -10,6 +10,7 @@ import com.nike.backstopper.handler.spring.listener.ApiExceptionHandlerListenerList; import com.nike.backstopper.model.DefaultErrorContractDTO; +import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.Ordered; @@ -72,8 +73,12 @@ protected ModelAndView prepareFrameworkRepresentation( } @Override - public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, - Exception ex) { + public ModelAndView resolveException( + @NotNull HttpServletRequest request, + @NotNull HttpServletResponse response, + Object handler, + @NotNull Exception ex + ) { try { ErrorResponseInfo errorResponseInfo = maybeHandleException(ex, request, response); @@ -84,9 +89,9 @@ public ModelAndView resolveException(HttpServletRequest request, HttpServletResp return errorResponseInfo.frameworkRepresentationObj; } catch (UnexpectedMajorExceptionHandlingError ohNoException) { - logger.error( - "Unexpected major error while handling exception. " + SpringUnhandledExceptionHandler.class.getName() - + " should handle it.", ohNoException); + logger.error("Unexpected major error while handling exception. {} should handle it.", + SpringUnhandledExceptionHandler.class.getName(), ohNoException + ); return null; } diff --git a/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerUtils.java b/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerUtils.java index faf9f14..3a310ca 100644 --- a/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerUtils.java +++ b/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerUtils.java @@ -60,7 +60,7 @@ public ModelAndView generateModelAndViewForErrorResponse( * RequestInfoForLogging)} for serializing the error contract. Defaults to {@link * JsonUtilWithDefaultErrorContractDTOSupport#DEFAULT_SMART_MAPPER}. */ - @SuppressWarnings("UnusedParameters") + @SuppressWarnings("unused") protected ObjectMapper getObjectMapperForJsonErrorResponseSerialization( DefaultErrorContractDTO errorContractDTO, int httpStatusCode, Collection rawFilteredApiErrors, Throwable originalException, RequestInfoForLogging request diff --git a/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/SpringContainerErrorController.java b/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/SpringContainerErrorController.java index e668758..120c091 100644 --- a/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/SpringContainerErrorController.java +++ b/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/SpringContainerErrorController.java @@ -86,13 +86,13 @@ public void error(ServletRequest request) throws Throwable { protected static class SpringbootErrorControllerIsNotOnClasspath implements ConfigurationCondition { @Override - public ConfigurationPhase getConfigurationPhase() { + public @NotNull ConfigurationPhase getConfigurationPhase() { return ConfigurationPhase.REGISTER_BEAN; } @Override public boolean matches( - ConditionContext context, AnnotatedTypeMetadata metadata + @NotNull ConditionContext context, @NotNull AnnotatedTypeMetadata metadata ) { // If we're in a Springboot application we want to return false to prevent registration. return !isClassAvailableOnClasspath("org.springframework.boot.web.servlet.error.ErrorController"); diff --git a/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/SpringUnhandledExceptionHandler.java b/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/SpringUnhandledExceptionHandler.java index 99db514..8a32af7 100644 --- a/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/SpringUnhandledExceptionHandler.java +++ b/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/SpringUnhandledExceptionHandler.java @@ -8,6 +8,7 @@ import com.nike.backstopper.handler.UnhandledExceptionHandlerServletApiBase; import com.nike.backstopper.model.DefaultErrorContractDTO; +import org.jetbrains.annotations.NotNull; import org.springframework.core.Ordered; import org.springframework.web.servlet.HandlerExceptionResolver; import org.springframework.web.servlet.ModelAndView; @@ -86,8 +87,12 @@ protected ErrorResponseInfo generateLastDitchFallbackErrorResponse } @Override - public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, - Exception ex) { + public ModelAndView resolveException( + @NotNull HttpServletRequest request, + @NotNull HttpServletResponse response, + Object handler, + @NotNull Exception ex + ) { return handleException(ex, request, response).frameworkRepresentationObj; } diff --git a/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/config/BackstopperSpringWebMvcConfig.java b/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/config/BackstopperSpringWebMvcConfig.java index 9d07e24..69c882f 100644 --- a/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/config/BackstopperSpringWebMvcConfig.java +++ b/backstopper-spring-web-mvc/src/main/java/com/nike/backstopper/handler/spring/config/BackstopperSpringWebMvcConfig.java @@ -27,7 +27,7 @@ * handlers will supersede the built-in spring exception handler chain and will translate ALL errors heading to * the caller so that they conform to the API error contract. See the {@link SpringApiExceptionHandler} and {@link * SpringUnhandledExceptionHandler} classes themselves for more info. - * + *

* This also pulls in {@link SpringContainerErrorController} to handle exceptions that originate in the * Servlet container outside Spring proper so they can also be handled by Backstopper. Note that you'll need to * configure your Servlet container to forward exceptions and errors it handles outside of Spring to {@code /error} for diff --git a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/apierror/testing/base/BaseSpringEnabledValidationTestCase.java b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/apierror/testing/base/BaseSpringEnabledValidationTestCase.java index ba8f49d..b82ad15 100644 --- a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/apierror/testing/base/BaseSpringEnabledValidationTestCase.java +++ b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/apierror/testing/base/BaseSpringEnabledValidationTestCase.java @@ -22,7 +22,6 @@ import org.springframework.test.context.web.WebAppConfiguration; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; -import org.springframework.web.client.RestTemplate; import org.springframework.web.context.WebApplicationContext; import java.io.IOException; @@ -64,9 +63,9 @@ public static class LogBeforeClass extends AbstractTestExecutionListener { @Override public void beforeTestClass(TestContext testContext) throws Exception { - Class testClass = testContext.getTestClass(); + Class testClass = testContext.getTestClass(); Logger logger = LoggerFactory.getLogger(testClass); - logger.info("******** Starting test_class=" + testClass.getName()); + logger.info("******** Starting test_class={}", testClass.getName()); super.beforeTestClass(testContext); } } @@ -75,9 +74,9 @@ public static class LogAfterClass extends AbstractTestExecutionListener { @Override public void afterTestClass(TestContext testContext) throws Exception { - Class testClass = testContext.getTestClass(); + Class testClass = testContext.getTestClass(); Logger logger = LoggerFactory.getLogger(testClass); - logger.info("******** Shutting down test_class=" + testClass.getName()); + logger.info("******** Shutting down test_class={}", testClass.getName()); super.afterTestClass(testContext); } } @@ -85,9 +84,6 @@ public void afterTestClass(TestContext testContext) throws Exception { @Inject protected WebApplicationContext wac; - @Inject - protected RestTemplate restTemplate; - protected MockMvc mockMvc; @Inject @@ -122,7 +118,6 @@ protected void verifyErrorResponse(MvcResult result, ProjectApiErrors projectApi * expected errors (as per the default error handling contract), and that the MvcResult's {@link * org.springframework.test.web.servlet.MvcResult#getResolvedException()} matches the given expectedExceptionType. */ - @SuppressWarnings("ThrowableResultOfMethodCallIgnored") protected void verifyErrorResponse(MvcResult result, ProjectApiErrors projectApiErrors, List expectedErrors, Class expectedExceptionType) throws IOException { diff --git a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/apierror/testing/base/TestCaseValidationSpringConfig.java b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/apierror/testing/base/TestCaseValidationSpringConfig.java index c50d50e..5db754e 100644 --- a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/apierror/testing/base/TestCaseValidationSpringConfig.java +++ b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/apierror/testing/base/TestCaseValidationSpringConfig.java @@ -34,6 +34,7 @@ @Configuration @ComponentScan(basePackages = "com.nike.backstopper") // Enable app-wide JSR-330 annotation-driven dependency injection. @Import(BackstopperSpringWebMvcConfig.class) +@SuppressWarnings("unused") public class TestCaseValidationSpringConfig extends WebMvcConfigurationSupport { public static final ApiError INVALID_COUNT_VALUE = new ApiErrorBase("INVALID_COUNT_VALUE", 99042, "Invalid count value", 400, null); diff --git a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/ClientfacingErrorITest.java b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/ClientfacingErrorITest.java index 768c601..07447b9 100644 --- a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/ClientfacingErrorITest.java +++ b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/ClientfacingErrorITest.java @@ -48,6 +48,7 @@ import jakarta.validation.constraints.Pattern; import static java.util.Collections.singletonList; +import static java.util.Objects.requireNonNull; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; @@ -64,6 +65,7 @@ public class ClientfacingErrorITest extends BaseSpringEnabledValidationTestCase { @Inject + @SuppressWarnings("unused") private ProjectApiErrors projectApiErrors; @Test @@ -259,7 +261,7 @@ public void verify_SpringContainerErrorController_is_registered_and_listening_on MvcResult result = this.mockMvc.perform(get("/error")).andReturn(); verifyErrorResponse(result, projectApiErrors, projectApiErrors.getGenericServiceError(), ApiException.class); ApiException apiEx = (ApiException) result.getResolvedException(); - Assertions.assertThat(apiEx.getMessage()).isEqualTo( + Assertions.assertThat(requireNonNull(apiEx).getMessage()).isEqualTo( "Synthetic exception for unhandled container status code: null" ); Assertions.assertThat(apiEx.getExtraDetailsForLogging()).isEqualTo(singletonList( @@ -278,6 +280,7 @@ private static class DummyRequestObject implements Serializable { @Pattern(regexp = "\\d{4}-\\d{2}-\\d{2}", message="TYPE_CONVERSION_ERROR") public String startDate; + @SuppressWarnings("unused") // Needed for deserialization. public DummyRequestObject() {} public DummyRequestObject(String count, String offset, String startDate) { this.count = count; @@ -289,6 +292,7 @@ public DummyRequestObject(String count, String offset, String startDate) { private static class DummyResponseObject implements Serializable { public String someField = "foo"; + @SuppressWarnings("unused") // Needed for deserialization. public DummyResponseObject() {} public DummyResponseObject(String someField) { @@ -298,6 +302,7 @@ public DummyResponseObject(String someField) { @Controller @RequestMapping("/clientFacingErrorTestDummy") + @SuppressWarnings({"unused", "ClassEscapesDefinedScope"}) public static class DummyController { @Inject @@ -347,8 +352,16 @@ public void throwServerTimeoutException() { @RequestMapping("/throwServerUnknownHttpStatusCodeException") public void throwServerUnknownHttpStatusCodeException() { + @SuppressWarnings("DataFlowIssue") UnknownHttpStatusCodeException serverResponseEx = new UnknownHttpStatusCodeException(142, null, null, null, null); - throw new ServerUnknownHttpStatusCodeException(new Exception("Intentional test exception"), "FOO", serverResponseEx, serverResponseEx.getRawStatusCode(), serverResponseEx.getResponseHeaders(), serverResponseEx.getResponseBodyAsString()); + throw new ServerUnknownHttpStatusCodeException( + new Exception("Intentional test exception"), + "FOO", + serverResponseEx, + serverResponseEx.getStatusCode().value(), + serverResponseEx.getResponseHeaders(), + serverResponseEx.getResponseBodyAsString() + ); } @RequestMapping("/throwServerUnreachableException") @@ -386,7 +399,7 @@ public DummyResponseObject validateDummyRequestObject(@RequestBody @Valid DummyR @RequestMapping("/validateRequiredInteger") @ResponseBody - public DummyResponseObject validateRequiredInteger(@RequestParam(required = true) Integer someInt) { + public DummyResponseObject validateRequiredInteger(@RequestParam Integer someInt) { return new DummyResponseObject(String.valueOf(someInt)); } diff --git a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerTest.java b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerTest.java index 2f125a7..a69649b 100644 --- a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerTest.java +++ b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerTest.java @@ -49,6 +49,7 @@ public void resolveException_returns_null_if_maybeHandleException_returns_null() doReturn(null).when(handlerSpy).maybeHandleException(isNull(), isNull(), isNull()); // when + @SuppressWarnings("DataFlowIssue") ModelAndView result = handlerSpy.resolveException(null, null, null, null); // then @@ -64,6 +65,7 @@ public void resolveException_returns_null_if_maybeHandleException_throws_Unexpec .when(handlerSpy).maybeHandleException(isNull(), isNull(), isNull()); // when + @SuppressWarnings("DataFlowIssue") ModelAndView result = handlerSpy.resolveException(null, null, null, null); // then diff --git a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerUtilsTest.java b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerUtilsTest.java index b2e921f..487a42a 100644 --- a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerUtilsTest.java +++ b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringApiExceptionHandlerUtilsTest.java @@ -4,18 +4,11 @@ import com.nike.backstopper.apierror.testutil.BarebonesCoreApiErrorForTesting; import com.nike.backstopper.model.DefaultErrorContractDTO; -import com.fasterxml.jackson.core.JsonProcessingException; - -import org.junit.Before; import org.junit.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; import org.springframework.web.servlet.ModelAndView; import java.util.Arrays; -import jakarta.servlet.http.HttpServletResponse; - import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; @@ -26,14 +19,6 @@ */ public class SpringApiExceptionHandlerUtilsTest extends BaseSpringEnabledValidationTestCase { - @Mock - private HttpServletResponse responseMock; - - @Before - public void setupMethod() { - MockitoAnnotations.initMocks(this); - } - @Test public void generateModelAndViewForErrorResponseShouldGenerateModelAndViewWithErrorContractAsOnlyModelObject() { DefaultErrorContractDTO diff --git a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringContainerErrorControllerTest.java b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringContainerErrorControllerTest.java index 29fa22d..2d353c4 100644 --- a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringContainerErrorControllerTest.java +++ b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringContainerErrorControllerTest.java @@ -47,6 +47,7 @@ public void constructor_sets_fields_as_expected() { @Test public void constructor_throws_NPE_if_passed_null_ProjectApiErrors() { // when + @SuppressWarnings("DataFlowIssue") Throwable ex = catchThrowable( () -> new SpringContainerErrorController(null, unhandledContainerErrorHelperMock) ); @@ -60,6 +61,7 @@ public void constructor_throws_NPE_if_passed_null_ProjectApiErrors() { @Test public void constructor_throws_NPE_if_passed_null_UnhandledServletContainerErrorHelper() { // when + @SuppressWarnings("DataFlowIssue") Throwable ex = catchThrowable( () -> new SpringContainerErrorController(projectApiErrorsMock, null) ); @@ -71,6 +73,7 @@ public void constructor_throws_NPE_if_passed_null_UnhandledServletContainerError } @Test + @SuppressWarnings("ThrowableNotThrown") public void error_method_throws_result_of_calling_UnhandledServletContainerErrorHelper() { // given SpringContainerErrorController impl = new SpringContainerErrorController( diff --git a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringUnhandledExceptionHandlerTest.java b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringUnhandledExceptionHandlerTest.java index 869784c..357d3ab 100644 --- a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringUnhandledExceptionHandlerTest.java +++ b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/SpringUnhandledExceptionHandlerTest.java @@ -29,6 +29,7 @@ import jakarta.servlet.http.HttpServletResponse; import static java.util.Collections.singletonList; +import static java.util.Objects.requireNonNull; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -44,6 +45,7 @@ public class SpringUnhandledExceptionHandlerTest { private SpringUnhandledExceptionHandler handlerSpy; private ProjectApiErrors testProjectApiErrors; + @SuppressWarnings("FieldCanBeLocal") private ApiExceptionHandlerUtils generalUtils; private SpringApiExceptionHandlerUtils springUtilsSpy; @@ -78,7 +80,8 @@ public void generateLastDitchFallbackErrorResponseInfo_returns_expected_value() assertThat(response.httpStatusCode).isEqualTo(expectedHttpStatusCode); assertThat(response.headersToAddToResponse).isEqualTo(expectedHeadersMap); assertThat(response.frameworkRepresentationObj.getView()).isInstanceOf(MappingJackson2JsonView.class); - ObjectMapper objectMapperUsed = ((MappingJackson2JsonView)response.frameworkRepresentationObj.getView()).getObjectMapper(); + ObjectMapper objectMapperUsed = + ((MappingJackson2JsonView) requireNonNull(response.frameworkRepresentationObj.getView())).getObjectMapper(); assertThat(objectMapperUsed).isSameAs(JsonUtilWithDefaultErrorContractDTOSupport.DEFAULT_SMART_MAPPER); assertThat(response.frameworkRepresentationObj.getModel()).hasSize(1); Object modelObj = response.frameworkRepresentationObj.getModel().values().iterator().next(); diff --git a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringWebMvcFrameworkExceptionHandlerListenerTest.java b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringWebMvcFrameworkExceptionHandlerListenerTest.java index 1057b65..7197683 100644 --- a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringWebMvcFrameworkExceptionHandlerListenerTest.java +++ b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringWebMvcFrameworkExceptionHandlerListenerTest.java @@ -73,6 +73,7 @@ public void constructor_sets_projectApiErrors_and_utils_to_passed_in_args() { @Test public void constructor_throws_IllegalArgumentException_if_passed_null_projectApiErrors() { // when + @SuppressWarnings("DataFlowIssue") Throwable ex = Assertions.catchThrowable( () -> new OneOffSpringWebMvcFrameworkExceptionHandlerListener(null, ApiExceptionHandlerUtils.DEFAULT_IMPL) ); @@ -84,6 +85,7 @@ public void constructor_throws_IllegalArgumentException_if_passed_null_projectAp @Test public void constructor_throws_IllegalArgumentException_if_passed_null_utils() { // when + @SuppressWarnings("DataFlowIssue") Throwable ex = Assertions.catchThrowable( () -> new OneOffSpringWebMvcFrameworkExceptionHandlerListener(mock(ProjectApiErrors.class), null) ); @@ -116,7 +118,7 @@ private void validateResponse( private void validateResponse( ApiExceptionHandlerListenerResult result, - boolean expectedShouldHandle, + @SuppressWarnings("SameParameterValue") boolean expectedShouldHandle, Collection expectedErrors, List> expectedExtraDetailsForLogging ) { @@ -229,6 +231,7 @@ public void shouldHandleException_returns_UNSUPPORTED_MEDIA_TYPE_for_HttpMediaTy validateResponse(result, true, singletonList(testProjectApiErrors.getUnsupportedMediaTypeApiError())); } + @SuppressWarnings("unused") public void methodWithAnnotatedParams( @RequestHeader int headerParam, @RequestParam int queryParam, diff --git a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/service/FailFastServersideValidationServiceITest.java b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/service/FailFastServersideValidationServiceITest.java index bfe97fc..485deba 100644 --- a/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/service/FailFastServersideValidationServiceITest.java +++ b/backstopper-spring-web-mvc/src/test/java/com/nike/backstopper/service/FailFastServersideValidationServiceITest.java @@ -8,8 +8,6 @@ import com.nike.backstopper.apierror.testutil.ProjectApiErrorsForTesting; import com.nike.backstopper.exception.ServersideValidationError; -import com.fasterxml.jackson.databind.ObjectMapper; - import org.junit.Test; import org.springframework.http.MediaType; import org.springframework.stereotype.Controller; @@ -38,9 +36,6 @@ */ public class FailFastServersideValidationServiceITest extends BaseSpringEnabledValidationTestCase { - @Inject - private ObjectMapper objectMapper; - private static final ApiError SOME_STRING_ERROR = new ApiErrorBase("SOME_STRING_ERROR", 99042, "some string error", 400, null); private static final ApiError SUB_OBJECT_ERROR = new ApiErrorBase("SUB_OBJECT_ERROR", 99043, "sub object error", 400, null); private static final ApiError SUBOBJECT_FIELD_ERROR = new ApiErrorBase("SUBOBJECT_FIELD_ERROR", 99044, "subobject field", 400, null); @@ -82,6 +77,7 @@ private static class ValidateMe implements Serializable { @NotNull(message = "SUB_OBJECT_ERROR") public final ValidateMeSubobject subObject; + @SuppressWarnings("unused") // Needed for deserialization. private ValidateMe() { this(null, null); } @@ -96,6 +92,7 @@ private static class ValidateMeSubobject implements Serializable { @NotNull(message = "SUBOBJECT_FIELD_ERROR") public final String someSubObjectField; + @SuppressWarnings("unused") // Needed for deserialization. private ValidateMeSubobject() { this(null); } @@ -107,6 +104,7 @@ public ValidateMeSubobject(String someSubObjectField) { @Controller @RequestMapping("/ffsvsControllerDummy") + @SuppressWarnings({"unused", "ClassEscapesDefinedScope"}) public static class DummyController { @Inject diff --git a/backstopper-spring-web/build.gradle b/backstopper-spring-web/build.gradle index f5afe0a..d0e0c58 100644 --- a/backstopper-spring-web/build.gradle +++ b/backstopper-spring-web/build.gradle @@ -13,6 +13,7 @@ dependencies { project(":backstopper-core").sourceSets.test.output, "org.jetbrains:annotations:$jetbrainsAnnotationsVersion", "junit:junit:$junitVersion", + "org.hamcrest:hamcrest-all:$hamcrestVersion", "org.mockito:mockito-core:$mockitoVersion", "ch.qos.logback:logback-classic:$logbackVersion", "org.assertj:assertj-core:$assertJVersion", diff --git a/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListener.java b/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListener.java index e0a2d09..a0c1902 100644 --- a/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListener.java +++ b/backstopper-spring-web/src/main/java/com/nike/backstopper/handler/spring/listener/impl/OneOffSpringCommonFrameworkExceptionHandlerListener.java @@ -20,6 +20,7 @@ import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.context.request.async.AsyncRequestTimeoutException; import org.springframework.web.method.annotation.MethodArgumentConversionNotSupportedException; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import org.springframework.web.server.MethodNotAllowedException; @@ -94,11 +95,7 @@ public abstract class OneOffSpringCommonFrameworkExceptionHandlerListener implem )); // Support 503 cases from competing dependencies using classname matching. - protected final Set DEFAULT_TO_503_CLASSNAMES = singleton( - // AsyncRequestTimeoutException didn't show up until more recent versions of Spring, so we'll check for it - // by classname to prevent unnecessarily breaking existing Backstopper users on older versions of Spring. - "org.springframework.web.context.request.async.AsyncRequestTimeoutException" - ); + protected final Set DEFAULT_TO_503_CLASSNAMES = Collections.emptySet(); /** * @param projectApiErrors The {@link ProjectApiErrors} that should be used by this instance when finding {@link @@ -164,7 +161,7 @@ public ApiExceptionHandlerListenerResult shouldHandleException(Throwable ex) { return handleHttpMessageConversionException((HttpMessageConversionException)ex, extraDetailsForLogging); } - if (isA503TemporaryProblemExceptionClassname(exClassname)) { + if (ex instanceof AsyncRequestTimeoutException || isA503TemporaryProblemExceptionClassname(exClassname)) { return handleError(projectApiErrors.getTemporaryServiceProblemApiError(), extraDetailsForLogging); } diff --git a/build.gradle b/build.gradle index 34315b6..45e8164 100644 --- a/build.gradle +++ b/build.gradle @@ -115,7 +115,11 @@ ext { javassistVersion = '3.23.2-GA' jettyVersion = '11.0.24' + // RestAssured has a vulnerability warning due to pulling in commons-codec:commons-codec:1.11, so we are manually + // bumping it to remove the vuln warning. When we update RestAssured versions, see if we can get rid of this + // hack fix. restAssuredVersion = '5.5.0' + commonsCodecVersion = '1.17.1' // JACOCO PROPERTIES jacocoToolVersion = '0.8.12' diff --git a/nike-internal-util/build.gradle b/nike-internal-util/build.gradle index c7f993f..5d8e1f5 100644 --- a/nike-internal-util/build.gradle +++ b/nike-internal-util/build.gradle @@ -5,6 +5,9 @@ groupId = nikeInternalUtilGroupId group = nikeInternalUtilGroupId dependencies { + compileOnly( + "org.jetbrains:annotations:$jetbrainsAnnotationsVersion" + ) testImplementation( "junit:junit:$junitVersion", "org.mockito:mockito-core:$mockitoVersion", diff --git a/nike-internal-util/src/main/java/com/nike/internal/util/ImmutablePair.java b/nike-internal-util/src/main/java/com/nike/internal/util/ImmutablePair.java index db9839a..bba670a 100644 --- a/nike-internal-util/src/main/java/com/nike/internal/util/ImmutablePair.java +++ b/nike-internal-util/src/main/java/com/nike/internal/util/ImmutablePair.java @@ -4,6 +4,8 @@ // entire library for just a few utilities. See the license notification in NOTICE.txt at the root of this project // for license info. +import java.io.Serial; + /** *

An immutable pair consisting of two {@code Object} elements.

* @@ -22,6 +24,7 @@ public final class ImmutablePair extends Pair { /** Serialization version */ + @Serial private static final long serialVersionUID = 4954918890077093841L; /** Left object */ diff --git a/nike-internal-util/src/main/java/com/nike/internal/util/MapBuilder.java b/nike-internal-util/src/main/java/com/nike/internal/util/MapBuilder.java index 66214e4..04d9e96 100644 --- a/nike-internal-util/src/main/java/com/nike/internal/util/MapBuilder.java +++ b/nike-internal-util/src/main/java/com/nike/internal/util/MapBuilder.java @@ -32,6 +32,7 @@ public MapBuilder put(K key, V value) { return this; } + @SuppressWarnings("UnusedReturnValue") public MapBuilder putAll(Map otherMap) { for (Map.Entry entry : otherMap.entrySet()) { put(entry.getKey(), entry.getValue()); diff --git a/nike-internal-util/src/main/java/com/nike/internal/util/Pair.java b/nike-internal-util/src/main/java/com/nike/internal/util/Pair.java index 0314fab..fdc2f41 100644 --- a/nike-internal-util/src/main/java/com/nike/internal/util/Pair.java +++ b/nike-internal-util/src/main/java/com/nike/internal/util/Pair.java @@ -1,5 +1,8 @@ package com.nike.internal.util; +import org.jetbrains.annotations.NotNull; + +import java.io.Serial; import java.io.Serializable; import java.util.Map; import java.util.Objects; @@ -28,6 +31,7 @@ public abstract class Pair implements Map.Entry, Comparable>, Serializable { /** Serialization version */ + @Serial private static final long serialVersionUID = 4954918890077093841L; /** @@ -100,7 +104,7 @@ public R getValue() { * @return negative if this is less, zero if equal, positive if greater */ @Override - public int compareTo(final Pair other) { + public int compareTo(final @NotNull Pair other) { if (this == other) return 0; @@ -134,7 +138,6 @@ protected int compareObj(T thisObj, T otherObj) { * @param obj the object to compare to, null returns false * @return true if the elements of the pair are equal */ - @SuppressWarnings( "deprecation" ) // ObjectUtils.equals(Object, Object) has been deprecated in 3.2 @Override public boolean equals(final Object obj) { if (obj == this) { diff --git a/nike-internal-util/src/main/java/com/nike/internal/util/StringUtils.java b/nike-internal-util/src/main/java/com/nike/internal/util/StringUtils.java index ba308b8..2974c0a 100644 --- a/nike-internal-util/src/main/java/com/nike/internal/util/StringUtils.java +++ b/nike-internal-util/src/main/java/com/nike/internal/util/StringUtils.java @@ -13,9 +13,9 @@ public class StringUtils { /** - * Intentionally protected - use the static methods. + * Intentionally private - use the static methods. */ - protected StringUtils() { + private StringUtils() { // do nothing } diff --git a/nike-internal-util/src/main/java/com/nike/internal/util/testing/Glassbox.java b/nike-internal-util/src/main/java/com/nike/internal/util/testing/Glassbox.java index 0b3e891..131bfa5 100644 --- a/nike-internal-util/src/main/java/com/nike/internal/util/testing/Glassbox.java +++ b/nike-internal-util/src/main/java/com/nike/internal/util/testing/Glassbox.java @@ -4,7 +4,7 @@ /** * A copy of the Mockito 1.x Whitebox class - they dropped this class in Mockito 2.x, but it's very handy. - * + *

* NOTE: This is intended for use during testing, _not_ in production code! In particular there is no caching of * {@link Field} - it is extracted every time for the same class. This usually isn't noticeable for tests, but can * be much too slow for high volume low latency production purposes. diff --git a/nike-internal-util/src/main/java/com/nike/internal/util/testing/TestUtils.java b/nike-internal-util/src/main/java/com/nike/internal/util/testing/TestUtils.java index f2b9e27..26f0583 100644 --- a/nike-internal-util/src/main/java/com/nike/internal/util/testing/TestUtils.java +++ b/nike-internal-util/src/main/java/com/nike/internal/util/testing/TestUtils.java @@ -15,7 +15,7 @@ private TestUtils() { /** * Finds an unused port on the machine hosting the currently running JVM. - * + *

* Does not throw any checked {@link IOException} that occurs while trying to find a free port. If one occurs, * it will be wrapped in a {@link RuntimeException}. */ diff --git a/nike-internal-util/src/test/java/com/nike/internal/util/PairTest.java b/nike-internal-util/src/test/java/com/nike/internal/util/PairTest.java index dbc5f2b..9c08b12 100644 --- a/nike-internal-util/src/test/java/com/nike/internal/util/PairTest.java +++ b/nike-internal-util/src/test/java/com/nike/internal/util/PairTest.java @@ -69,6 +69,7 @@ public void compareTo_returns_0_for_same_instance_comparison() { Pair pair = PairForTests.of("foo", "bar"); // expect + //noinspection EqualsWithItself assertThat(pair.compareTo(pair), is(0)); } diff --git a/nike-internal-util/src/test/java/com/nike/internal/util/StringUtilsTest.java b/nike-internal-util/src/test/java/com/nike/internal/util/StringUtilsTest.java index 89826c5..bc6d348 100644 --- a/nike-internal-util/src/test/java/com/nike/internal/util/StringUtilsTest.java +++ b/nike-internal-util/src/test/java/com/nike/internal/util/StringUtilsTest.java @@ -20,11 +20,6 @@ @RunWith(DataProviderRunner.class) public class StringUtilsTest { - @Test - public void use_protected_constructor_for_no_good_reason_other_than_code_coverage() { - new StringUtils(); - } - @Test @DataProvider(value = { "foo | bar | , | foo,bar", diff --git a/nike-internal-util/src/test/java/com/nike/internal/util/testing/GlassboxTest.java b/nike-internal-util/src/test/java/com/nike/internal/util/testing/GlassboxTest.java index 1366a1d..cec8d1e 100644 --- a/nike-internal-util/src/test/java/com/nike/internal/util/testing/GlassboxTest.java +++ b/nike-internal-util/src/test/java/com/nike/internal/util/testing/GlassboxTest.java @@ -12,12 +12,11 @@ */ public class GlassboxTest { - @SuppressWarnings({"FieldCanBeLocal", "FieldMayBeFinal", "unused"}) private static class SomeObject { public final String pubFinalFoo; private final String privFinalFoo; - public String pubFoo; - private String privFoo; + public final String pubFoo; + private final String privFoo; private SomeObject(String pubFinalFoo, String privFinalFoo, String pubFoo, String privFoo) { this.pubFinalFoo = pubFinalFoo; diff --git a/samples/sample-spring-boot3-webflux/build.gradle b/samples/sample-spring-boot3-webflux/build.gradle index 76a3d98..486ee3c 100644 --- a/samples/sample-spring-boot3-webflux/build.gradle +++ b/samples/sample-spring-boot3-webflux/build.gradle @@ -41,8 +41,10 @@ dependencies { } apply plugin: "application" -mainClassName = "com.nike.backstopper.springboot3webfluxsample.Main" +application { + mainClassName = "com.nike.backstopper.springboot3webfluxsample.Main" +} run { - systemProperties = System.getProperties() + systemProperties(System.getProperties()) } diff --git a/samples/sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample/config/SampleSpringboot3WebFluxSpringConfig.java b/samples/sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample/config/SampleSpringboot3WebFluxSpringConfig.java index 00ab501..7299ff2 100644 --- a/samples/sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample/config/SampleSpringboot3WebFluxSpringConfig.java +++ b/samples/sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample/config/SampleSpringboot3WebFluxSpringConfig.java @@ -7,6 +7,7 @@ import com.nike.backstopper.springboot3webfluxsample.error.SampleProjectApiError; import com.nike.backstopper.springboot3webfluxsample.error.SampleProjectApiErrorsImpl; +import org.jetbrains.annotations.NotNull; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -46,6 +47,7 @@ @Import(BackstopperSpringWebFluxConfig.class) // Instead of @Import(BackstopperSpringWebFluxConfig.class), you could component scan the com.nike.backstopper // package like this if you prefer component scanning: @ComponentScan(basePackages = "com.nike.backstopper") +@SuppressWarnings("unused") public class SampleSpringboot3WebFluxSpringConfig { /** @@ -67,6 +69,7 @@ public ProjectApiErrors getProjectApiErrors() { */ @Bean public Validator getJsr303Validator() { + //noinspection resource return Validation.buildDefaultValidatorFactory().getValidator(); } @@ -96,8 +99,8 @@ public RouterFunction sampleRouterFunction(SampleController samp public static class ExplodingWebFilter implements WebFilter { @Override - public Mono filter( - ServerWebExchange exchange, WebFilterChain chain + public @NotNull Mono filter( + ServerWebExchange exchange, @NotNull WebFilterChain chain ) { HttpHeaders httpHeaders = exchange.getRequest().getHeaders(); @@ -127,9 +130,9 @@ public static class ExplodingHandlerFilterFunction implements HandlerFilterFunction { @Override - public Mono filter( + public @NotNull Mono filter( ServerRequest serverRequest, - HandlerFunction handlerFunction + @NotNull HandlerFunction handlerFunction ) { HttpHeaders httpHeaders = serverRequest.headers().asHttpHeaders(); diff --git a/samples/sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample/controller/SampleController.java b/samples/sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample/controller/SampleController.java index 3facb09..4f72fc9 100644 --- a/samples/sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample/controller/SampleController.java +++ b/samples/sample-spring-boot3-webflux/src/main/java/com/nike/backstopper/springboot3webfluxsample/controller/SampleController.java @@ -129,7 +129,7 @@ public void triggerUnhandledError() { } public Mono getSampleModelRouterFunction(ServerRequest request) { - return ServerResponse.ok().syncBody( + return ServerResponse.ok().bodyValue( new SampleModel( UUID.randomUUID().toString(), String.valueOf(nextRangeInt(0, 42)), diff --git a/samples/sample-spring-boot3-webmvc/build.gradle b/samples/sample-spring-boot3-webmvc/build.gradle index ee2ea28..787fec8 100644 --- a/samples/sample-spring-boot3-webmvc/build.gradle +++ b/samples/sample-spring-boot3-webmvc/build.gradle @@ -41,8 +41,10 @@ dependencies { } apply plugin: "application" -mainClassName = "com.nike.backstopper.springboot3webmvcsample.Main" +application { + mainClassName = "com.nike.backstopper.springboot3webmvcsample.Main" +} run { - systemProperties = System.getProperties() + systemProperties(System.getProperties()) } diff --git a/samples/sample-spring-boot3-webmvc/src/main/java/com/nike/backstopper/springboot3webmvcsample/config/SampleSpringboot3WebMvcSpringConfig.java b/samples/sample-spring-boot3-webmvc/src/main/java/com/nike/backstopper/springboot3webmvcsample/config/SampleSpringboot3WebMvcSpringConfig.java index a462163..0de2c22 100644 --- a/samples/sample-spring-boot3-webmvc/src/main/java/com/nike/backstopper/springboot3webmvcsample/config/SampleSpringboot3WebMvcSpringConfig.java +++ b/samples/sample-spring-boot3-webmvc/src/main/java/com/nike/backstopper/springboot3webmvcsample/config/SampleSpringboot3WebMvcSpringConfig.java @@ -7,6 +7,7 @@ import com.nike.backstopper.springboot3webmvcsample.error.SampleProjectApiError; import com.nike.backstopper.springboot3webmvcsample.error.SampleProjectApiErrorsImpl; +import org.jetbrains.annotations.NotNull; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -39,6 +40,7 @@ @Import(BackstopperSpringboot3WebMvcConfig.class) // Instead of @Import(BackstopperSpringboot3WebMvcConfig.class), you could component scan the com.nike.backstopper // package like this if you prefer component scanning: @ComponentScan(basePackages = "com.nike.backstopper") +@SuppressWarnings("unused") public class SampleSpringboot3WebMvcSpringConfig { /** @@ -60,6 +62,7 @@ public ProjectApiErrors getProjectApiErrors() { */ @Bean public Validator getJsr303Validator() { + //noinspection resource return Validation.buildDefaultValidatorFactory().getValidator(); } @@ -70,7 +73,7 @@ public Validator getJsr303Validator() { * a real app. */ @Bean - public FilterRegistrationBean explodingServletFilter() { + public FilterRegistrationBean explodingServletFilter() { FilterRegistrationBean frb = new FilterRegistrationBean<>(new ExplodingFilter()); frb.setOrder(Ordered.HIGHEST_PRECEDENCE); return frb; @@ -80,7 +83,7 @@ private static class ExplodingFilter extends OncePerRequestFilter { @Override protected void doFilterInternal( - HttpServletRequest request, HttpServletResponse response, FilterChain filterChain + HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain ) throws ServletException, IOException { if ("true".equals(request.getHeader("throw-servlet-filter-exception"))) { throw ApiException diff --git a/samples/sample-spring-boot3-webmvc/src/main/java/com/nike/backstopper/springboot3webmvcsample/error/SampleProjectApiError.java b/samples/sample-spring-boot3-webmvc/src/main/java/com/nike/backstopper/springboot3webmvcsample/error/SampleProjectApiError.java index 492af40..d22f171 100644 --- a/samples/sample-spring-boot3-webmvc/src/main/java/com/nike/backstopper/springboot3webmvcsample/error/SampleProjectApiError.java +++ b/samples/sample-spring-boot3-webmvc/src/main/java/com/nike/backstopper/springboot3webmvcsample/error/SampleProjectApiError.java @@ -18,7 +18,7 @@ * conform to the range specified in {@link SampleProjectApiErrorsImpl#getProjectSpecificErrorCodeRange()} or an * exception will be thrown on app startup, and unit tests should fail. The one exception to this rule is a "core * error wrapper" - an instance that shares the same error code, message, and HTTP status code as a - * {@link SampleProjectApiErrorsImpl#getCoreApiErrors()} instance (in this case that means a wrapper around + * {@code SampleProjectApiErrorsImpl.getCoreApiErrors()} instance (in this case that means a wrapper around * {@link SampleCoreApiError}). * * @author Nic Munroe diff --git a/samples/sample-spring-web-mvc/build.gradle b/samples/sample-spring-web-mvc/build.gradle index e03b693..9304615 100644 --- a/samples/sample-spring-web-mvc/build.gradle +++ b/samples/sample-spring-web-mvc/build.gradle @@ -25,12 +25,16 @@ dependencies { "org.mockito:mockito-core:$mockitoVersion", "org.assertj:assertj-core:$assertJVersion", "io.rest-assured:rest-assured:$restAssuredVersion", + // Pulling in commons-codec manually to avoid vulnerability warning coming from RestAssured transitive dep. + "commons-codec:commons-codec:$commonsCodecVersion", ) } apply plugin: "application" -mainClassName = "com.nike.backstopper.springsample.Main" +application { + mainClassName = "com.nike.backstopper.springsample.Main" +} run { - systemProperties = System.getProperties() + systemProperties(System.getProperties()) } diff --git a/testonly/testonly-spring-6_0-webmvc/build.gradle b/testonly/testonly-spring-6_0-webmvc/build.gradle index a5586df..70ca91b 100644 --- a/testonly/testonly-spring-6_0-webmvc/build.gradle +++ b/testonly/testonly-spring-6_0-webmvc/build.gradle @@ -25,5 +25,7 @@ dependencies { "org.mockito:mockito-core:$mockitoVersion", "org.assertj:assertj-core:$assertJVersion", "io.rest-assured:rest-assured:$restAssuredVersion", + // Pulling in commons-codec manually to avoid vulnerability warning coming from RestAssured transitive dep. + "commons-codec:commons-codec:$commonsCodecVersion", ) } diff --git a/testonly/testonly-spring-6_1-webmvc/build.gradle b/testonly/testonly-spring-6_1-webmvc/build.gradle index 09f1fb0..4fa7f14 100644 --- a/testonly/testonly-spring-6_1-webmvc/build.gradle +++ b/testonly/testonly-spring-6_1-webmvc/build.gradle @@ -25,5 +25,7 @@ dependencies { "org.mockito:mockito-core:$mockitoVersion", "org.assertj:assertj-core:$assertJVersion", "io.rest-assured:rest-assured:$restAssuredVersion", + // Pulling in commons-codec manually to avoid vulnerability warning coming from RestAssured transitive dep. + "commons-codec:commons-codec:$commonsCodecVersion", ) } diff --git a/testonly/testonly-spring-webflux-reusable-test-support/build.gradle b/testonly/testonly-spring-webflux-reusable-test-support/build.gradle index b6e7903..5d67018 100644 --- a/testonly/testonly-spring-webflux-reusable-test-support/build.gradle +++ b/testonly/testonly-spring-webflux-reusable-test-support/build.gradle @@ -13,6 +13,8 @@ dependencies { "org.hibernate.validator:hibernate-validator:$hibernateValidatorVersion", "org.jetbrains:annotations:$jetbrainsAnnotationsVersion", "io.rest-assured:rest-assured:$restAssuredVersion", + // Pulling in commons-codec manually to avoid vulnerability warning coming from RestAssured transitive dep. + "commons-codec:commons-codec:$commonsCodecVersion", "org.assertj:assertj-core:$assertJVersion", ) testImplementation( diff --git a/testonly/testonly-spring-webmvc-reusable-test-support/build.gradle b/testonly/testonly-spring-webmvc-reusable-test-support/build.gradle index a8fb3e8..14035f5 100644 --- a/testonly/testonly-spring-webmvc-reusable-test-support/build.gradle +++ b/testonly/testonly-spring-webmvc-reusable-test-support/build.gradle @@ -16,6 +16,8 @@ dependencies { "com.fasterxml.jackson.core:jackson-core:$jacksonVersion", "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion", "io.rest-assured:rest-assured:$restAssuredVersion", + // Pulling in commons-codec manually to avoid vulnerability warning coming from RestAssured transitive dep. + "commons-codec:commons-codec:$commonsCodecVersion", "org.assertj:assertj-core:$assertJVersion", ) testImplementation( diff --git a/testonly/testonly-spring-webmvc-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/testutil/ExplodingServletFilter.java b/testonly/testonly-spring-webmvc-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/testutil/ExplodingServletFilter.java index 712fb0d..a26f45f 100644 --- a/testonly/testonly-spring-webmvc-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/testutil/ExplodingServletFilter.java +++ b/testonly/testonly-spring-webmvc-reusable-test-support/src/main/java/testonly/componenttest/spring/reusable/testutil/ExplodingServletFilter.java @@ -2,6 +2,7 @@ import com.nike.backstopper.exception.ApiException; +import org.jetbrains.annotations.NotNull; import org.springframework.web.filter.OncePerRequestFilter; import java.io.IOException; @@ -23,7 +24,7 @@ public class ExplodingServletFilter extends OncePerRequestFilter { @Override protected void doFilterInternal( - HttpServletRequest request, HttpServletResponse response, FilterChain filterChain + HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain ) throws ServletException, IOException { if ("true".equals(request.getHeader("throw-servlet-filter-exception"))) { throw ApiException From 8786e328c0491c8606e22a8fa613075d7698ac50 Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Fri, 13 Sep 2024 12:34:33 -0700 Subject: [PATCH 39/42] Fix gradle warnings for gradle 9 --- backstopper-reusable-tests-junit5/build.gradle | 8 ++++++++ samples/sample-spring-boot3-webflux/build.gradle | 2 +- samples/sample-spring-boot3-webmvc/build.gradle | 2 +- samples/sample-spring-web-mvc/build.gradle | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/backstopper-reusable-tests-junit5/build.gradle b/backstopper-reusable-tests-junit5/build.gradle index ec68749..dd301e1 100644 --- a/backstopper-reusable-tests-junit5/build.gradle +++ b/backstopper-reusable-tests-junit5/build.gradle @@ -28,7 +28,15 @@ dependencies { "org.jetbrains:annotations:$jetbrainsAnnotationsVersion", ) testImplementation( + "org.junit.jupiter:junit-jupiter-api:$junit5Version", + "org.junit.jupiter:junit-jupiter-engine:$junit5Version", + "org.junit.jupiter:junit-jupiter-params:$junit5Version", "org.slf4j:slf4j-api:$slf4jVersion", "ch.qos.logback:logback-classic:$logbackVersion", ) + // Make gradle happy for gradle 9. + // See: https://docs.gradle.org/8.10/userguide/upgrading_version_8.html#test_framework_implementation_dependencies + testRuntimeOnly( + "org.junit.platform:junit-platform-launcher" + ) } diff --git a/samples/sample-spring-boot3-webflux/build.gradle b/samples/sample-spring-boot3-webflux/build.gradle index 486ee3c..ea2b570 100644 --- a/samples/sample-spring-boot3-webflux/build.gradle +++ b/samples/sample-spring-boot3-webflux/build.gradle @@ -42,7 +42,7 @@ dependencies { apply plugin: "application" application { - mainClassName = "com.nike.backstopper.springboot3webfluxsample.Main" + setMainClass("com.nike.backstopper.springboot3webfluxsample.Main") } run { diff --git a/samples/sample-spring-boot3-webmvc/build.gradle b/samples/sample-spring-boot3-webmvc/build.gradle index 787fec8..d7ce29b 100644 --- a/samples/sample-spring-boot3-webmvc/build.gradle +++ b/samples/sample-spring-boot3-webmvc/build.gradle @@ -42,7 +42,7 @@ dependencies { apply plugin: "application" application { - mainClassName = "com.nike.backstopper.springboot3webmvcsample.Main" + setMainClass("com.nike.backstopper.springboot3webmvcsample.Main") } run { diff --git a/samples/sample-spring-web-mvc/build.gradle b/samples/sample-spring-web-mvc/build.gradle index 9304615..dcf9f30 100644 --- a/samples/sample-spring-web-mvc/build.gradle +++ b/samples/sample-spring-web-mvc/build.gradle @@ -32,7 +32,7 @@ dependencies { apply plugin: "application" application { - mainClassName = "com.nike.backstopper.springsample.Main" + setMainClass("com.nike.backstopper.springsample.Main") } run { From b50f901f06765e802056b225399e5882d1ba4501 Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Fri, 13 Sep 2024 13:09:13 -0700 Subject: [PATCH 40/42] Update dependencies --- backstopper-reusable-tests-junit5/build.gradle | 8 ++------ build.gradle | 15 ++++----------- samples/sample-spring-boot3-webflux/build.gradle | 9 ++++++--- samples/sample-spring-boot3-webmvc/build.gradle | 4 +--- samples/sample-spring-web-mvc/build.gradle | 4 +--- testonly/testonly-spring-6_0-webmvc/build.gradle | 4 +--- testonly/testonly-spring-6_1-webmvc/build.gradle | 4 +--- .../testonly-springboot3_0-webflux/build.gradle | 4 +--- .../testonly-springboot3_0-webmvc/build.gradle | 4 +--- .../testonly-springboot3_1-webflux/build.gradle | 4 +--- .../testonly-springboot3_1-webmvc/build.gradle | 4 +--- .../testonly-springboot3_2-webflux/build.gradle | 4 +--- .../testonly-springboot3_2-webmvc/build.gradle | 4 +--- .../testonly-springboot3_3-webflux/build.gradle | 4 +--- .../testonly-springboot3_3-webmvc/build.gradle | 4 +--- 15 files changed, 24 insertions(+), 56 deletions(-) diff --git a/backstopper-reusable-tests-junit5/build.gradle b/backstopper-reusable-tests-junit5/build.gradle index dd301e1..20f6a15 100644 --- a/backstopper-reusable-tests-junit5/build.gradle +++ b/backstopper-reusable-tests-junit5/build.gradle @@ -16,9 +16,7 @@ dependencies { project(":backstopper-custom-validators"), "com.fasterxml.jackson.core:jackson-core:$jacksonVersion", "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion", - "org.junit.jupiter:junit-jupiter-api:$junit5Version", - "org.junit.jupiter:junit-jupiter-engine:$junit5Version", - "org.junit.jupiter:junit-jupiter-params:$junit5Version", + "org.junit.jupiter:junit-jupiter:$junit5Version", "org.mockito:mockito-core:$mockito3Version", "org.assertj:assertj-core:$assertJVersion", "org.reflections:reflections:$orgReflectionsUpdatedVersion", @@ -28,9 +26,7 @@ dependencies { "org.jetbrains:annotations:$jetbrainsAnnotationsVersion", ) testImplementation( - "org.junit.jupiter:junit-jupiter-api:$junit5Version", - "org.junit.jupiter:junit-jupiter-engine:$junit5Version", - "org.junit.jupiter:junit-jupiter-params:$junit5Version", + "org.junit.jupiter:junit-jupiter:$junit5Version", "org.slf4j:slf4j-api:$slf4jVersion", "ch.qos.logback:logback-classic:$logbackVersion", ) diff --git a/build.gradle b/build.gradle index 45e8164..8fcc653 100644 --- a/build.gradle +++ b/build.gradle @@ -82,7 +82,7 @@ configure(subprojects.findAll { it.name.startsWith("sample") || it.name.startsWi // ========== PROPERTIES FOR GRADLE BUILD - DEPENDENCY VERSIONS / ETC ext { - slf4jVersion = '1.7.36' + slf4jVersion = '2.0.16' jakartaInjectVersion = '2.0.1' jakartaValidationVersion = '3.0.2' servletApiVersion = '6.0.0' // Compatible with Jakarta EE 10 @@ -93,26 +93,19 @@ ext { springboot3_1Version = '3.1.12' springboot3_2Version = '3.2.9' springboot3_3Version = '3.3.3' - jersey1Version = '1.19.2' - jersey2Version = '2.23.2' - jaxRsVersion = '2.0.1' - jaxbApiVersion = '2.4.0-b180830.0359' junitVersion = '4.13.2' - junit5Version = '5.8.2' + junit5Version = '5.11.0' mockitoVersion = '5.13.0' logbackVersion = '1.5.8' jacksonVersion = '2.17.2' - assertJVersion = '3.23.1' + assertJVersion = '3.26.3' junitDataproviderVersion = '1.13.1' hamcrestVersion = '1.3' hibernateValidatorVersion = '8.0.1.Final' // Compatible with Jakarta EE 10 glassfishExpresslyVersion = '5.0.0' // Provides EL impl support for hibernate validator, compatible with Jakarta EE 10 - jetbrainsAnnotationsVersion = '16.0.3' - - orgReflectionsVersion = '0.9.11' - javassistVersion = '3.23.2-GA' + jetbrainsAnnotationsVersion = '24.1.0' jettyVersion = '11.0.24' // RestAssured has a vulnerability warning due to pulling in commons-codec:commons-codec:1.11, so we are manually diff --git a/samples/sample-spring-boot3-webflux/build.gradle b/samples/sample-spring-boot3-webflux/build.gradle index ea2b570..d8e477c 100644 --- a/samples/sample-spring-boot3-webflux/build.gradle +++ b/samples/sample-spring-boot3-webflux/build.gradle @@ -31,13 +31,16 @@ dependencies { ) testImplementation( project(":backstopper-reusable-tests-junit5"), - "org.junit.jupiter:junit-jupiter-api:$junit5Version", - "org.junit.jupiter:junit-jupiter-engine:$junit5Version", - "org.junit.jupiter:junit-jupiter-params:$junit5Version", + "org.junit.jupiter:junit-jupiter:$junit5Version", "org.mockito:mockito-core:$mockitoVersion", "org.assertj:assertj-core:$assertJVersion", "io.rest-assured:rest-assured", ) + // Make gradle happy for gradle 9. + // See: https://docs.gradle.org/8.10/userguide/upgrading_version_8.html#test_framework_implementation_dependencies + testRuntimeOnly( + "org.junit.platform:junit-platform-launcher" + ) } apply plugin: "application" diff --git a/samples/sample-spring-boot3-webmvc/build.gradle b/samples/sample-spring-boot3-webmvc/build.gradle index d7ce29b..3437a4f 100644 --- a/samples/sample-spring-boot3-webmvc/build.gradle +++ b/samples/sample-spring-boot3-webmvc/build.gradle @@ -31,9 +31,7 @@ dependencies { ) testImplementation( project(":backstopper-reusable-tests-junit5"), - "org.junit.jupiter:junit-jupiter-api:$junit5Version", - "org.junit.jupiter:junit-jupiter-engine:$junit5Version", - "org.junit.jupiter:junit-jupiter-params:$junit5Version", + "org.junit.jupiter:junit-jupiter:$junit5Version", "org.mockito:mockito-core:$mockitoVersion", "org.assertj:assertj-core:$assertJVersion", "io.rest-assured:rest-assured", diff --git a/samples/sample-spring-web-mvc/build.gradle b/samples/sample-spring-web-mvc/build.gradle index dcf9f30..e6db94b 100644 --- a/samples/sample-spring-web-mvc/build.gradle +++ b/samples/sample-spring-web-mvc/build.gradle @@ -19,9 +19,7 @@ dependencies { ) testImplementation( project(":backstopper-reusable-tests-junit5"), - "org.junit.jupiter:junit-jupiter-api:$junit5Version", - "org.junit.jupiter:junit-jupiter-engine:$junit5Version", - "org.junit.jupiter:junit-jupiter-params:$junit5Version", + "org.junit.jupiter:junit-jupiter:$junit5Version", "org.mockito:mockito-core:$mockitoVersion", "org.assertj:assertj-core:$assertJVersion", "io.rest-assured:rest-assured:$restAssuredVersion", diff --git a/testonly/testonly-spring-6_0-webmvc/build.gradle b/testonly/testonly-spring-6_0-webmvc/build.gradle index 70ca91b..01433dd 100644 --- a/testonly/testonly-spring-6_0-webmvc/build.gradle +++ b/testonly/testonly-spring-6_0-webmvc/build.gradle @@ -19,9 +19,7 @@ dependencies { testImplementation( project(":backstopper-reusable-tests-junit5"), project(":testonly:testonly-spring-webmvc-reusable-test-support"), - "org.junit.jupiter:junit-jupiter-api:$junit5Version", - "org.junit.jupiter:junit-jupiter-engine:$junit5Version", - "org.junit.jupiter:junit-jupiter-params:$junit5Version", + "org.junit.jupiter:junit-jupiter:$junit5Version", "org.mockito:mockito-core:$mockitoVersion", "org.assertj:assertj-core:$assertJVersion", "io.rest-assured:rest-assured:$restAssuredVersion", diff --git a/testonly/testonly-spring-6_1-webmvc/build.gradle b/testonly/testonly-spring-6_1-webmvc/build.gradle index 4fa7f14..ff17f96 100644 --- a/testonly/testonly-spring-6_1-webmvc/build.gradle +++ b/testonly/testonly-spring-6_1-webmvc/build.gradle @@ -19,9 +19,7 @@ dependencies { testImplementation( project(":backstopper-reusable-tests-junit5"), project(":testonly:testonly-spring-webmvc-reusable-test-support"), - "org.junit.jupiter:junit-jupiter-api:$junit5Version", - "org.junit.jupiter:junit-jupiter-engine:$junit5Version", - "org.junit.jupiter:junit-jupiter-params:$junit5Version", + "org.junit.jupiter:junit-jupiter:$junit5Version", "org.mockito:mockito-core:$mockitoVersion", "org.assertj:assertj-core:$assertJVersion", "io.rest-assured:rest-assured:$restAssuredVersion", diff --git a/testonly/testonly-springboot3_0-webflux/build.gradle b/testonly/testonly-springboot3_0-webflux/build.gradle index 48d7a2e..4a10ba1 100644 --- a/testonly/testonly-springboot3_0-webflux/build.gradle +++ b/testonly/testonly-springboot3_0-webflux/build.gradle @@ -30,9 +30,7 @@ dependencies { project(":backstopper-reusable-tests-junit5"), project(":testonly:testonly-spring-webflux-reusable-test-support"), "org.jetbrains:annotations:$jetbrainsAnnotationsVersion", - "org.junit.jupiter:junit-jupiter-api:$junit5Version", - "org.junit.jupiter:junit-jupiter-engine:$junit5Version", - "org.junit.jupiter:junit-jupiter-params:$junit5Version", + "org.junit.jupiter:junit-jupiter:$junit5Version", "org.mockito:mockito-core:$mockitoVersion", "org.assertj:assertj-core:$assertJVersion", "io.rest-assured:rest-assured", diff --git a/testonly/testonly-springboot3_0-webmvc/build.gradle b/testonly/testonly-springboot3_0-webmvc/build.gradle index 8a9a82e..90d89d6 100644 --- a/testonly/testonly-springboot3_0-webmvc/build.gradle +++ b/testonly/testonly-springboot3_0-webmvc/build.gradle @@ -29,9 +29,7 @@ dependencies { testImplementation( project(":backstopper-reusable-tests-junit5"), project(":testonly:testonly-spring-webmvc-reusable-test-support"), - "org.junit.jupiter:junit-jupiter-api:$junit5Version", - "org.junit.jupiter:junit-jupiter-engine:$junit5Version", - "org.junit.jupiter:junit-jupiter-params:$junit5Version", + "org.junit.jupiter:junit-jupiter:$junit5Version", "org.mockito:mockito-core:$mockitoVersion", "org.assertj:assertj-core:$assertJVersion", "io.rest-assured:rest-assured", diff --git a/testonly/testonly-springboot3_1-webflux/build.gradle b/testonly/testonly-springboot3_1-webflux/build.gradle index 111346f..c3dbc79 100644 --- a/testonly/testonly-springboot3_1-webflux/build.gradle +++ b/testonly/testonly-springboot3_1-webflux/build.gradle @@ -30,9 +30,7 @@ dependencies { project(":backstopper-reusable-tests-junit5"), project(":testonly:testonly-spring-webflux-reusable-test-support"), "org.jetbrains:annotations:$jetbrainsAnnotationsVersion", - "org.junit.jupiter:junit-jupiter-api:$junit5Version", - "org.junit.jupiter:junit-jupiter-engine:$junit5Version", - "org.junit.jupiter:junit-jupiter-params:$junit5Version", + "org.junit.jupiter:junit-jupiter:$junit5Version", "org.mockito:mockito-core:$mockitoVersion", "org.assertj:assertj-core:$assertJVersion", "io.rest-assured:rest-assured", diff --git a/testonly/testonly-springboot3_1-webmvc/build.gradle b/testonly/testonly-springboot3_1-webmvc/build.gradle index d0cc632..d36711c 100644 --- a/testonly/testonly-springboot3_1-webmvc/build.gradle +++ b/testonly/testonly-springboot3_1-webmvc/build.gradle @@ -29,9 +29,7 @@ dependencies { testImplementation( project(":backstopper-reusable-tests-junit5"), project(":testonly:testonly-spring-webmvc-reusable-test-support"), - "org.junit.jupiter:junit-jupiter-api:$junit5Version", - "org.junit.jupiter:junit-jupiter-engine:$junit5Version", - "org.junit.jupiter:junit-jupiter-params:$junit5Version", + "org.junit.jupiter:junit-jupiter:$junit5Version", "org.mockito:mockito-core:$mockitoVersion", "org.assertj:assertj-core:$assertJVersion", "io.rest-assured:rest-assured", diff --git a/testonly/testonly-springboot3_2-webflux/build.gradle b/testonly/testonly-springboot3_2-webflux/build.gradle index 0f1a56a..f4d054f 100644 --- a/testonly/testonly-springboot3_2-webflux/build.gradle +++ b/testonly/testonly-springboot3_2-webflux/build.gradle @@ -30,9 +30,7 @@ dependencies { project(":backstopper-reusable-tests-junit5"), project(":testonly:testonly-spring-webflux-reusable-test-support"), "org.jetbrains:annotations:$jetbrainsAnnotationsVersion", - "org.junit.jupiter:junit-jupiter-api:$junit5Version", - "org.junit.jupiter:junit-jupiter-engine:$junit5Version", - "org.junit.jupiter:junit-jupiter-params:$junit5Version", + "org.junit.jupiter:junit-jupiter:$junit5Version", "org.mockito:mockito-core:$mockitoVersion", "org.assertj:assertj-core:$assertJVersion", "io.rest-assured:rest-assured", diff --git a/testonly/testonly-springboot3_2-webmvc/build.gradle b/testonly/testonly-springboot3_2-webmvc/build.gradle index 306e0b1..7e4d2ec 100644 --- a/testonly/testonly-springboot3_2-webmvc/build.gradle +++ b/testonly/testonly-springboot3_2-webmvc/build.gradle @@ -29,9 +29,7 @@ dependencies { testImplementation( project(":backstopper-reusable-tests-junit5"), project(":testonly:testonly-spring-webmvc-reusable-test-support"), - "org.junit.jupiter:junit-jupiter-api:$junit5Version", - "org.junit.jupiter:junit-jupiter-engine:$junit5Version", - "org.junit.jupiter:junit-jupiter-params:$junit5Version", + "org.junit.jupiter:junit-jupiter:$junit5Version", "org.mockito:mockito-core:$mockitoVersion", "org.assertj:assertj-core:$assertJVersion", "io.rest-assured:rest-assured", diff --git a/testonly/testonly-springboot3_3-webflux/build.gradle b/testonly/testonly-springboot3_3-webflux/build.gradle index 2feb71d..8b8338a 100644 --- a/testonly/testonly-springboot3_3-webflux/build.gradle +++ b/testonly/testonly-springboot3_3-webflux/build.gradle @@ -30,9 +30,7 @@ dependencies { project(":backstopper-reusable-tests-junit5"), project(":testonly:testonly-spring-webflux-reusable-test-support"), "org.jetbrains:annotations:$jetbrainsAnnotationsVersion", - "org.junit.jupiter:junit-jupiter-api:$junit5Version", - "org.junit.jupiter:junit-jupiter-engine:$junit5Version", - "org.junit.jupiter:junit-jupiter-params:$junit5Version", + "org.junit.jupiter:junit-jupiter:$junit5Version", "org.mockito:mockito-core:$mockitoVersion", "org.assertj:assertj-core:$assertJVersion", "io.rest-assured:rest-assured", diff --git a/testonly/testonly-springboot3_3-webmvc/build.gradle b/testonly/testonly-springboot3_3-webmvc/build.gradle index 028934e..a671cf7 100644 --- a/testonly/testonly-springboot3_3-webmvc/build.gradle +++ b/testonly/testonly-springboot3_3-webmvc/build.gradle @@ -29,9 +29,7 @@ dependencies { testImplementation( project(":backstopper-reusable-tests-junit5"), project(":testonly:testonly-spring-webmvc-reusable-test-support"), - "org.junit.jupiter:junit-jupiter-api:$junit5Version", - "org.junit.jupiter:junit-jupiter-engine:$junit5Version", - "org.junit.jupiter:junit-jupiter-params:$junit5Version", + "org.junit.jupiter:junit-jupiter:$junit5Version", "org.mockito:mockito-core:$mockitoVersion", "org.assertj:assertj-core:$assertJVersion", "io.rest-assured:rest-assured", From 35b07da1fb2d3f16ffc685a845ea2d98b102c0b9 Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Fri, 13 Sep 2024 13:19:40 -0700 Subject: [PATCH 41/42] Update GHA workflow --- .github/workflows/build.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 58e1e69..4647986 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,14 +11,15 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up JDK 11 - uses: actions/setup-java@v2 + - uses: actions/checkout@v4 + - name: Set up JDK 17 + uses: actions/setup-java@v4 with: - java-version: 11 distribution: 'temurin' - - name: Validate Gradle wrapper - uses: gradle/wrapper-validation-action@e6e38bacfdf1a337459f332974bb2327a31aaf4b + java-version: '17' + cache: 'gradle' + - name: Validate Gradle Wrapper Checksums + uses: gradle/actions/wrapper-validation@v3 - name: Build with Gradle run: ./gradlew clean build - name: Upload coverage report to CodeCov @@ -29,7 +30,7 @@ jobs: verbose: true token: ${{ secrets.CODECOV_TOKEN }} - name: Upload reports and test results to GitHub - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: reports-and-test-results path: | From 9f385eeb46e80a8890b6e4d6f943fe587c59e1df Mon Sep 17 00:00:00 2001 From: Nic Munroe Date: Fri, 13 Sep 2024 13:33:23 -0700 Subject: [PATCH 42/42] Relax project code coverage requirements a little --- .codecov.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.codecov.yml b/.codecov.yml index 17fb278..1f309f9 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -5,7 +5,7 @@ coverage: project: default: enabled: yes - target: 90% + target: 85% patch: default: enabled: yes